Merge pull request #5060 from paulbalandan/4.2

Merge branch '4.2'
This commit is contained in:
John Paul E. Balandan, CPA 2021-09-08 23:04:17 +08:00 committed by GitHub
commit 3ea4bac65b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
96 changed files with 5508 additions and 759 deletions

75
.github/workflows/test-deptrac.yml vendored Normal file
View File

@ -0,0 +1,75 @@
# When a PR is opened or a push is made, perform an
# architectural inspection on the code using Deptrac.
name: Deptrac
on:
pull_request:
branches:
- 'develop'
- '4.*'
paths:
- 'app/**'
- 'system/**'
- 'composer.json'
- 'depfile.yaml'
- '.github/workflows/test-deptrac.yml'
push:
branches:
- 'develop'
- '4.*'
paths:
- 'app/**'
- 'system/**'
- 'composer.json'
- 'depfile.yaml'
- '.github/workflows/test-deptrac.yml'
jobs:
build:
name: Architectural Inspection
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.0'
tools: composer, phive
extensions: intl, json, mbstring, gd, mysqlnd, xdebug, xml, sqlite3
- name: Validate composer.json
run: composer validate --strict
- name: Get composer cache directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Create composer cache directory
run: mkdir -p ${{ steps.composer-cache.outputs.dir }}
- name: Cache composer dependencies
uses: actions/cache@v2
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ runner.os }}-composer-
- name: Create Deptrac cache directory
run: mkdir -p build/
- name: Cache Deptrac results
uses: actions/cache@v2
with:
path: build
key: ${{ runner.os }}-deptrac-${{ github.sha }}
restore-keys: ${{ runner.os }}-deptrac-
- name: Install dependencies
run: composer update --ansi --no-interaction
- name: Run architectural inspection
run: |
sudo phive --no-progress install --global qossmic/deptrac --trust-gpg-keys B8F640134AB1782E
deptrac analyze --cache-file=build/deptrac.cache

View File

@ -40,4 +40,4 @@ jobs:
- name: Detect code duplication
run: |
sudo phive --no-progress install --global --trust-gpg-keys 4AA394086372C20A phpcpd
phpcpd --exclude system/Test --exclude system/ThirdParty --exclude system/Database/SQLSRV/Builder.php app/ public/ system/
phpcpd --exclude system/Test --exclude system/ThirdParty --exclude system/Database/SQLSRV/Builder.php --exclude system/Database/SQLSRV/Forge.php app/ public/ system/

View File

@ -2,19 +2,19 @@
// ========================================================================== */
// Themes
$t-dark: #252525;
$t-dark: #252525;
$t-light: #FFFFFF;
// Glossy colors
$g-blue: #5BC0DE;
$g-gray: #434343;
$g-green: #9ACE25;
$g-blue: #5BC0DE;
$g-gray: #434343;
$g-green: #9ACE25;
$g-orange: #DD8615;
$g-red: #DD4814;
$g-red: #DD4814;
// Matt colors
$m-blue: #D8EAF0;
$m-gray: #DFDFDF;
$m-green: #DFF0D8;
$m-blue: #D8EAF0;
$m-gray: #DFDFDF;
$m-green: #DFF0D8;
$m-orange: #FDC894;
$m-red: #EF9090;
$m-red: #EF9090;

View File

@ -2,12 +2,13 @@
// ========================================================================== */
@mixin border-radius($radius) {
border-radius: $radius;
-moz-border-radius: $radius;
-webkit-border-radius: $radius;
border-radius: $radius;
-moz-border-radius: $radius;
-webkit-border-radius: $radius;
}
@mixin box-shadow($left, $top, $radius, $color) {
box-shadow: $left $top $radius $color;
-moz-box-shadow: $left $top $radius $color;
-webkit-box-shadow: $left $top $radius $color;
box-shadow: $left $top $radius $color;
-moz-box-shadow: $left $top $radius $color;
-webkit-box-shadow: $left $top $radius $color;
}

View File

@ -12,11 +12,14 @@
// ========================================================================== */
#debug-icon {
background-color: $t-dark;
@include box-shadow(0, 0, 4px, $m-gray);
a:active, a:link, a:visited {
color: $g-orange;
}
background-color: $t-dark;
@include box-shadow(0, 0, 4px, $m-gray);
a:active,
a:link,
a:visited {
color: $g-orange;
}
}
@ -24,119 +27,130 @@
// ========================================================================== */
#debug-bar {
background-color: $t-dark;
color: $m-gray;
background-color: $t-dark;
color: $m-gray;
// Reset to prevent conflict with other CSS files
h1,
h2,
h3,
p,
a,
button,
table,
thead,
tr,
td,
button,
.toolbar {
background-color: transparent;
color: $m-gray;
}
// Reset to prevent conflict with other CSS files
h1,
h2,
h3,
p,
a,
button,
table,
thead,
tr,
td,
button,
.toolbar {
background-color: transparent;
color: $m-gray;
}
// Buttons
button {
background-color: $t-dark;
}
// Buttons
button {
background-color: $t-dark;
}
// Tables
table {
strong {
color: $m-orange;
}
tbody tr {
&:hover {
background-color: $g-gray;
}
&.current {
background-color: $m-orange;
td {
color: $t-dark;
}
&:hover td {
background-color: $g-red;
color: $t-light;
}
}
}
}
// Tables
table {
strong {
color: $m-orange;
}
// The toolbar
.toolbar {
background-color: $g-gray;
@include box-shadow(0, 0, 4px, $g-gray);
img {
filter: brightness(0) invert(1);
}
}
tbody tr {
&:hover {
background-color: $g-gray;
}
// Fixed top
&.fixed-top {
& .toolbar {
@include box-shadow(0, 0, 4px, $g-gray);
}
.tab {
@include box-shadow(0, 1px, 4px, $g-gray);
}
}
&.current {
background-color: $m-orange;
// "Muted" elements
.muted {
color: $m-gray;
td {
color: $g-gray;
}
&:hover td {
color: $m-gray;
}
}
td {
color: $t-dark;
}
// The toolbar preferences
#toolbar-position,
#toolbar-theme, {
filter: brightness(0) invert(0.6);
}
&:hover td {
background-color: $g-red;
color: $t-light;
}
}
}
}
// The toolbar menus
.ci-label {
&.active {
background-color: $t-dark;
}
&:hover {
background-color: $t-dark;
}
.badge {
background-color: $g-blue;
color: $m-gray;
}
}
// The toolbar
.toolbar {
background-color: $g-gray;
@include box-shadow(0, 0, 4px, $g-gray);
// The tabs container
.tab {
background-color: $t-dark;
@include box-shadow(0, -1px, 4px, $g-gray);
}
img {
filter: brightness(0) invert(1);
}
}
// The "Timeline" tab
.timeline {
th,
td {
border-color: $g-gray;
}
.timer {
background-color: $g-orange;
}
}
// Fixed top
&.fixed-top {
.toolbar {
@include box-shadow(0, 0, 4px, $g-gray);
}
.tab {
@include box-shadow(0, 1px, 4px, $g-gray);
}
}
// "Muted" elements
.muted {
color: $m-gray;
td {
color: $g-gray;
}
&:hover td {
color: $m-gray;
}
}
// The toolbar preferences
#toolbar-position,
#toolbar-theme {
filter: brightness(0) invert(0.6);
}
// The toolbar menus
.ci-label {
&.active {
background-color: $t-dark;
}
&:hover {
background-color: $t-dark;
}
.badge {
background-color: $g-blue;
color: $m-gray;
}
}
// The tabs container
.tab {
background-color: $t-dark;
@include box-shadow(0, -1px, 4px, $g-gray);
}
// The "Timeline" tab
.timeline {
th,
td {
border-color: $g-gray;
}
.timer {
background-color: $g-orange;
}
}
}
@ -144,9 +158,10 @@
// ========================================================================== */
.debug-view.show-view {
border-color: $g-orange;
border-color: $g-orange;
}
.debug-view-path {
background-color: $m-orange;
color: $g-gray;
background-color: $m-orange;
color: $g-gray;
}

View File

@ -12,11 +12,14 @@
// ========================================================================== */
#debug-icon {
background-color: $t-light;
@include box-shadow(0, 0, 4px, $m-gray);
a:active, a:link, a:visited {
color: $g-orange;
}
background-color: $t-light;
@include box-shadow(0, 0, 4px, $m-gray);
a:active,
a:link,
a:visited {
color: $g-orange;
}
}
@ -24,116 +27,126 @@
// ========================================================================== */
#debug-bar {
background-color: $t-light;
color: $g-gray;
background-color: $t-light;
color: $g-gray;
// Reset to prevent conflict with other CSS files */
h1,
h2,
h3,
p,
a,
button,
table,
thead,
tr,
td,
button,
.toolbar {
background-color: transparent;
color: $g-gray;
}
// Reset to prevent conflict with other CSS files
h1,
h2,
h3,
p,
a,
button,
table,
thead,
tr,
td,
button,
.toolbar {
background-color: transparent;
color: $g-gray;
}
// Buttons
button {
background-color: $t-light;
}
// Buttons
button {
background-color: $t-light;
}
// Tables
table {
strong {
color: $m-orange;
}
tbody tr {
&:hover {
background-color: $m-gray;
}
&.current {
background-color: $m-orange;
&:hover td {
background-color: $g-red;
color: $t-light;
}
}
}
}
// Tables
table {
strong {
color: $m-orange;
}
// The toolbar
.toolbar {
background-color: $t-light;
@include box-shadow(0, 0, 4px, $m-gray);
img {
filter: brightness(0) invert(0.4);
}
}
tbody tr {
&:hover {
background-color: $m-gray;
}
// Fixed top
&.fixed-top {
& .toolbar {
@include box-shadow(0, 0, 4px, $m-gray);
}
.tab {
@include box-shadow(0, 1px, 4px, $m-gray);
}
}
&.current {
background-color: $m-orange;
// "Muted" elements
.muted {
color: $g-gray;
td {
color: $m-gray;
}
&:hover td {
color: $g-gray;
}
}
&:hover td {
background-color: $g-red;
color: $t-light;
}
}
}
}
// The toolbar preferences
#toolbar-position,
#toolbar-theme, {
filter: brightness(0) invert(0.6);
}
// The toolbar
.toolbar {
background-color: $t-light;
@include box-shadow(0, 0, 4px, $m-gray);
// The toolbar menus
.ci-label {
&.active {
background-color: $m-gray;
}
&:hover {
background-color: $m-gray;
}
.badge {
background-color: $g-blue;
color: $t-light;
}
}
img {
filter: brightness(0) invert(0.4);
}
}
// The tabs container
.tab {
background-color: $t-light;
@include box-shadow(0, -1px, 4px, $m-gray);
}
// Fixed top
&.fixed-top {
.toolbar {
@include box-shadow(0, 0, 4px, $m-gray);
}
// The "Timeline" tab
.timeline {
th,
td {
border-color: $m-gray;
}
.timer {
background-color: $g-orange;
}
}
.tab {
@include box-shadow(0, 1px, 4px, $m-gray);
}
}
// "Muted" elements
.muted {
color: $g-gray;
td {
color: $m-gray;
}
&:hover td {
color: $g-gray;
}
}
// The toolbar preferences
#toolbar-position,
#toolbar-theme {
filter: brightness(0) invert(0.6);
}
// The toolbar menus
.ci-label {
&.active {
background-color: $m-gray;
}
&:hover {
background-color: $m-gray;
}
.badge {
background-color: $g-blue;
color: $t-light;
}
}
// The tabs container
.tab {
background-color: $t-light;
@include box-shadow(0, -1px, 4px, $m-gray);
}
// The "Timeline" tab
.timeline {
th,
td {
border-color: $m-gray;
}
.timer {
background-color: $g-orange;
}
}
}
@ -141,9 +154,10 @@
// ========================================================================== */
.debug-view.show-view {
border-color: $g-orange;
border-color: $g-orange;
}
.debug-view-path {
background-color: $m-orange;
color: $g-gray;
background-color: $m-orange;
color: $g-gray;
}

View File

@ -1,9 +1,10 @@
/*! CodeIgniter 4 - Debug bar
* ============================================================================
* Forum: https://forum.codeigniter.com
* Github: https://github.com/codeigniter4/codeigniter4
* Slack: https://codeigniterchat.slack.com
* Website: https://codeigniter.com
/**
* This file is part of the CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
// IMPORTS
@ -17,38 +18,38 @@
// ========================================================================== */
#debug-icon {
// Position
bottom: 0;
position: fixed;
right: 0;
z-index: 10000;
// Position
bottom: 0;
position: fixed;
right: 0;
z-index: 10000;
// Size
height: 36px;
width: 36px;
// Size
height: 36px;
width: 36px;
// Spacing
margin: 0px;
padding: 0px;
// Spacing
margin: 0px;
padding: 0px;
// Content
clear: both;
text-align: center;
// Content
clear: both;
text-align: center;
a svg {
margin: 8px;
max-width: 20px;
max-height: 20px;
}
a svg {
margin: 8px;
max-width: 20px;
max-height: 20px;
}
&.fixed-top {
bottom: auto;
top: 0;
}
&.fixed-top {
bottom: auto;
top: 0;
}
.debug-bar-ndisplay {
display: none;
}
.debug-bar-ndisplay {
display: none;
}
}
@ -56,299 +57,352 @@
// ========================================================================== */
#debug-bar {
// Position
bottom: 0;
left: 0;
position: fixed;
right: 0;
z-index: 10000;
// Position
bottom: 0;
left: 0;
position: fixed;
right: 0;
z-index: 10000;
// Size
height: 36px;
// Size
height: 36px;
// Spacing
line-height: 36px;
// Spacing
line-height: 36px;
// Typography
font-family: $base-font;
font-size: $base-size;
font-weight: 400;
// Typography
font-family: $base-font;
font-size: $base-size;
font-weight: 400;
// General elements
h1 {
bottom: 0;
display: inline-block;
font-size: $base-size - 2;
font-weight: normal;
margin: 0 16px 0 0;
padding: 0;
position: absolute;
right: 30px;
text-align: left;
top: 0;
// General elements
h1 {
bottom: 0;
display: inline-block;
font-size: $base-size - 2;
font-weight: normal;
margin: 0 16px 0 0;
padding: 0;
position: absolute;
right: 30px;
text-align: left;
top: 0;
svg {
width: 16px;
margin-right: 5px;
}
}
svg {
width: 16px;
margin-right: 5px;
}
}
h2 {
font-size: $base-size;
margin: 0;
padding: 5px 0 10px 0;
h2 {
font-size: $base-size;
margin: 0;
padding: 5px 0 10px 0;
span {
font-size: 13px;
}
}
span {
font-size: 13px;
}
}
h3 {
font-size: $base-size - 4;
font-weight: 200;
margin: 0 0 0 10px;
padding: 0;
text-transform: uppercase;
}
h3 {
font-size: $base-size - 4;
font-weight: 200;
margin: 0 0 0 10px;
padding: 0;
text-transform: uppercase;
}
p {
font-size: $base-size - 4;
margin: 0 0 0 15px;
padding: 0;
}
p {
font-size: $base-size - 4;
margin: 0 0 0 15px;
padding: 0;
}
a {
text-decoration: none;
a {
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
&:hover {
text-decoration: underline;
}
}
button {
border: 1px solid;
@include border-radius(4px);
cursor: pointer;
line-height: 15px;
button {
border: 1px solid;
@include border-radius(4px);
cursor: pointer;
line-height: 15px;
&:hover {
text-decoration: underline;
}
}
&:hover {
text-decoration: underline;
}
}
table {
border-collapse: collapse;
font-size: $base-size - 2;
line-height: normal;
margin: 5px 10px 15px 10px; // Tables indentation
width: calc(100% - 10px); // Make sure it still fits the container, even with the margins
table {
border-collapse: collapse;
font-size: $base-size - 2;
line-height: normal;
strong {
font-weight: 500;
}
// Tables indentation
margin: 5px 10px 15px 10px;
th {
display: table-cell;
font-weight: 600;
padding-bottom: 0.7em;
text-align: left;
}
// Make sure it still fits the container, even with the margins
width: calc(100% - 10px);
tr {
border: none;
}
strong {
font-weight: 500;
}
td {
border: none;
display: table-cell;
margin: 0;
text-align: left;
th {
display: table-cell;
font-weight: 600;
padding-bottom: 0.7em;
text-align: left;
}
&:first-child {
max-width: 20%;
tr {
border: none;
}
&.narrow {
width: 7em;
}
}
}
}
td {
border: none;
display: table-cell;
margin: 0;
text-align: left;
td[data-debugbar-route] {
form {
display: none;
}
&:first-child {
max-width: 20%;
&:hover {
form {
display: block;
}
&.narrow {
width: 7em;
}
}
}
}
&>div {
display: none;
}
}
td[data-debugbar-route] {
form {
display: none;
}
input[type=text] {
padding: 2px;
}
}
&:hover {
form {
display: block;
}
// The toolbar
.toolbar {
display: flex;
overflow: hidden;
overflow-y: auto;
padding: 0 12px 0 12px;
/* give room for OS X scrollbar */
white-space: nowrap;
z-index: 10000;
}
&>div {
display: none;
}
}
// Fixed top
&.fixed-top {
bottom: auto;
top: 0;
input[type=text] {
padding: 2px;
}
}
.tab {
bottom: auto;
top: 36px;
}
}
// The toolbar
.toolbar {
display: flex;
overflow: hidden;
overflow-y: auto;
padding: 0 12px 0 12px;
// Give room for OS X scrollbar
white-space: nowrap;
z-index: 10000;
}
// The toolbar preferences
#toolbar-position,
#toolbar-theme {
a {
// float: left;
padding: 0 6px;
display: inline-flex;
vertical-align: top;
// Fixed top
&.fixed-top {
bottom: auto;
top: 0;
&:hover {
text-decoration: none;
}
}
}
.tab {
bottom: auto;
top: 36px;
}
}
// The "Open/Close" toggle
#debug-bar-link {
bottom: 0;
display: inline-block;
font-size: $base-size;
line-height: 36px;
padding: 6px;
position: absolute;
right: 10px;
top: 0;
width: 24px;
}
// The toolbar preferences
#toolbar-position,
#toolbar-theme {
a {
padding: 0 6px;
display: inline-flex;
vertical-align: top;
// The toolbar menus
.ci-label {
display: inline-flex;
font-size: $base-size - 2;
// vertical-align: baseline;
&:hover {
text-decoration: none;
}
}
}
&:hover {
cursor: pointer;
}
// The "Open/Close" toggle
#debug-bar-link {
bottom: 0;
display: inline-block;
font-size: $base-size;
line-height: 36px;
padding: 6px;
position: absolute;
right: 10px;
top: 0;
width: 24px;
}
a {
color: inherit;
display: flex;
letter-spacing: normal;
padding: 0 10px;
text-decoration: none;
align-items: center;
}
// The toolbar menus
.ci-label {
display: inline-flex;
font-size: $base-size - 2;
// The toolbar icons
img {
// clear: left;
// display: inline-block;
// float: left;
margin: 6px 3px 6px 0;
width: 16px !important;
}
&:hover {
cursor: pointer;
}
// The toolbar notification badges
.badge {
@include border-radius(12px);
display: inline-block;
font-size: 75%;
font-weight: bold;
line-height: 12px;
margin-left: 5px;
padding: 2px 5px;
text-align: center;
vertical-align: baseline;
white-space: nowrap;
}
}
a {
color: inherit;
display: flex;
letter-spacing: normal;
padding: 0 10px;
text-decoration: none;
align-items: center;
}
// The tabs container
.tab {
bottom: 35px;
display: none;
left: 0;
max-height: 62%;
overflow: hidden;
overflow-y: auto;
padding: 1em 2em;
position: fixed;
right: 0;
z-index: 9999;
}
// The toolbar icons
img {
margin: 6px 3px 6px 0;
width: 16px !important;
}
// The "Timeline" tab
.timeline {
margin-left: 0;
width: 100%;
// The toolbar notification badges
.badge {
@include border-radius(12px);
display: inline-block;
font-size: 75%;
font-weight: bold;
line-height: 12px;
margin-left: 5px;
padding: 2px 5px;
text-align: center;
vertical-align: baseline;
white-space: nowrap;
}
}
th {
border-left: 1px solid;
font-size: $base-size - 4;
font-weight: 200;
padding: 5px 5px 10px 5px;
position: relative;
text-align: left;
// The tabs container
.tab {
bottom: 35px;
display: none;
left: 0;
max-height: 62%;
overflow: hidden;
overflow-y: auto;
padding: 1em 2em;
position: fixed;
right: 0;
z-index: 9999;
}
&:first-child {
border-left: 0;
}
}
// The "Timeline" tab
.timeline {
margin-left: 0;
width: 100%;
td {
border-left: 1px solid;
padding: 5px;
position: relative;
th {
border-left: 1px solid;
font-size: $base-size - 4;
font-weight: 200;
padding: 5px 5px 10px 5px;
position: relative;
text-align: left;
&:first-child {
border-left: 0;
}
}
&:first-child {
border-left: 0;
}
}
.timer {
@include border-radius(4px);
display: inline-block;
padding: 5px;
position: absolute;
top: 30%;
}
}
td {
border-left: 1px solid;
padding: 5px;
position: relative;
// The "Routes" tab
.route-params,
.route-params-item {
vertical-align: top;
&:first-child {
border-left: 0;
max-width: none;
}
td:first-child {
font-style: italic;
padding-left: 1em;
text-align: right;
}
}
&.child-container {
padding: 0px;
.timeline {
margin: 0px;
td {
&:first-child {
&:not(.child-container) {
padding-left: calc(5px + 10px * var(--level));
}
}
}
}
}
}
.timer {
@include border-radius(4px);
display: inline-block;
padding: 5px;
position: absolute;
top: 30%;
}
.timeline-parent {
cursor: pointer;
td {
&:first-child {
nav {
background: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMCAxNTAiPjxwYXRoIGQ9Ik02IDdoMThsLTkgMTV6bTAgMzBoMThsLTkgMTV6bTAgNDVoMThsLTktMTV6bTAgMzBoMThsLTktMTV6bTAgMTJsMTggMThtLTE4IDBsMTgtMTgiIGZpbGw9IiM1NTUiLz48cGF0aCBkPSJNNiAxMjZsMTggMThtLTE4IDBsMTgtMTgiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlPSIjNTU1Ii8+PC9zdmc+") no-repeat scroll 0 0/15px 75px transparent;
background-position: 0 25%;
display: inline-block;
height: 15px;
width: 15px;
margin-right: 3px;
vertical-align: middle;
}
}
}
}
.timeline-parent-open {
background-color: #DFDFDF;
td {
&:first-child {
nav {
background-position: 0 75%;
}
}
}
}
.child-row {
&:hover {
background: transparent;
}
}
}
// The "Routes" tab
.route-params,
.route-params-item {
vertical-align: top;
td:first-child {
font-style: italic;
padding-left: 1em;
text-align: right;
}
}
}
@ -356,21 +410,21 @@
// ========================================================================== */
.debug-view.show-view {
border: 1px solid;
margin: 4px;
border: 1px solid;
margin: 4px;
}
.debug-view-path {
font-family: monospace;
font-size: $base-size - 4;
letter-spacing: normal;
min-height: 16px;
padding: 2px;
text-align: left;
font-family: monospace;
font-size: $base-size - 4;
letter-spacing: normal;
min-height: 16px;
padding: 2px;
text-align: left;
}
.show-view .debug-view-path {
display: block !important;
display: block !important;
}
@ -378,17 +432,17 @@
// ========================================================================== */
@media screen and (max-width: 1024px) {
#debug-bar {
.ci-label {
img {
margin: unset
}
}
}
#debug-bar {
.ci-label {
img {
margin: unset
}
}
}
.hide-sm {
display: none !important;
}
.hide-sm {
display: none !important;
}
}
@ -400,22 +454,22 @@
// If the browser supports "prefers-color-scheme" and the scheme is "Dark"
@media (prefers-color-scheme: dark) {
@import '_theme-dark';
@import '_theme-dark';
}
// If we force the "Dark" theme
#toolbarContainer.dark {
@import '_theme-dark';
@import '_theme-dark';
td[data-debugbar-route] input[type=text] {
background: #000;
color: #fff;
}
td[data-debugbar-route] input[type=text] {
background: #000;
color: #fff;
}
}
// If we force the "Light" theme
#toolbarContainer.light {
@import '_theme-light';
@import '_theme-light';
}
@ -423,41 +477,41 @@
// ========================================================================== */
.debug-bar-width30 {
width: 30%;
width: 30%;
}
.debug-bar-width10 {
width: 10%;
width: 10%;
}
.debug-bar-width70p {
width: 70px;
width: 70px;
}
.debug-bar-width140p {
width: 140px;
width: 140px;
}
.debug-bar-width20e {
width: 20em;
width: 20em;
}
.debug-bar-width6r {
width: 6rem;
width: 6rem;
}
.debug-bar-ndisplay {
display: none;
display: none;
}
.debug-bar-alignRight {
text-align: right;
text-align: right;
}
.debug-bar-alignLeft {
text-align: left;
text-align: left;
}
.debug-bar-noverflow {
overflow: hidden;
}
overflow: hidden;
}

View File

@ -97,6 +97,20 @@ class Cache extends BaseConfig
*/
public $ttl = 60;
/**
* --------------------------------------------------------------------------
* Reserved Characters
* --------------------------------------------------------------------------
*
* A string of reserved characters that will not be allowed in keys or tags.
* Strings that violate this restriction will cause handlers to throw.
* Default: {}()/\@:
* Note: The default set is required for PSR-6 compliance.
*
* @var string
*/
public $reservedCharacters = '{}()/\@:';
/**
* --------------------------------------------------------------------------
* File settings

View File

@ -27,6 +27,7 @@ class Generators extends BaseConfig
*/
public $views = [
'make:command' => 'CodeIgniter\Commands\Generators\Views\command.tpl.php',
'make:config' => 'CodeIgniter\Commands\Generators\Views\config.tpl.php',
'make:controller' => 'CodeIgniter\Commands\Generators\Views\controller.tpl.php',
'make:entity' => 'CodeIgniter\Commands\Generators\Views\entity.tpl.php',
'make:filter' => 'CodeIgniter\Commands\Generators\Views\filter.tpl.php',

28
app/Config/Publisher.php Normal file
View File

@ -0,0 +1,28 @@
<?php
namespace Config;
use CodeIgniter\Config\Publisher as BasePublisher;
/**
* Publisher Configuration
*
* Defines basic security restrictions for the Publisher class
* to prevent abuse by injecting malicious files into a project.
*/
class Publisher extends BasePublisher
{
/**
* A list of allowed destinations with a (pseudo-)regex
* of allowed files for each destination.
* Attempts to publish to directories not in this list will
* result in a PublisherException. Files that do no fit the
* pattern will cause copy/merge to fail.
*
* @var array<string,string>
*/
public $restrictions = [
ROOTPATH => '*',
FCPATH => '#\.(?css|js|map|htm?|xml|json|webmanifest|tff|eot|woff?|gif|jpe?g|tiff?|png|webp|bmp|ico|svg)$#i',
];
}

View File

@ -163,7 +163,7 @@
color: rgba(200, 200, 200, 1);
padding: .25rem 1.75rem;
}
@media (max-width: 559px) {
@media (max-width: 629px) {
header ul {
padding: 0;
}

229
depfile.yaml Normal file
View File

@ -0,0 +1,229 @@
# Defines the layers for each framework
# component and their allowed interactions.
# The following components are exempt
# due to their global nature:
# - CLI & Commands
# - Config
# - Debug
# - Exception
# - Service
# - Validation\FormatRules
paths:
- ./app
- ./system
exclude_files:
- '#.*test.*#i'
layers:
- name: API
collectors:
- type: className
regex: ^Codeigniter\\API\\.*
- name: Cache
collectors:
- type: className
regex: ^Codeigniter\\Cache\\.*
- name: Controller
collectors:
- type: className
regex: ^CodeIgniter\\Controller$
- name: Cookie
collectors:
- type: className
regex: ^Codeigniter\\Cookie\\.*
- name: Database
collectors:
- type: className
regex: ^Codeigniter\\Database\\.*
- name: Email
collectors:
- type: className
regex: ^Codeigniter\\Email\\.*
- name: Encryption
collectors:
- type: className
regex: ^Codeigniter\\Encryption\\.*
- name: Entity
collectors:
- type: className
regex: ^Codeigniter\\Entity\\.*
- name: Events
collectors:
- type: className
regex: ^Codeigniter\\Events\\.*
- name: Files
collectors:
- type: className
regex: ^Codeigniter\\Files\\.*
- name: Filters
collectors:
- type: bool
must:
- type: className
regex: ^Codeigniter\\Filters\\Filter.*
- name: Format
collectors:
- type: className
regex: ^Codeigniter\\Format\\.*
- name: Honeypot
collectors:
- type: className
regex: ^Codeigniter\\.*Honeypot.* # includes the Filter
- name: HTTP
collectors:
- type: bool
must:
- type: className
regex: ^Codeigniter\\HTTP\\.*
must_not:
- type: className
regex: (Exception|URI)
- name: I18n
collectors:
- type: className
regex: ^Codeigniter\\I18n\\.*
- name: Images
collectors:
- type: className
regex: ^Codeigniter\\Images\\.*
- name: Language
collectors:
- type: className
regex: ^Codeigniter\\Language\\.*
- name: Log
collectors:
- type: className
regex: ^Codeigniter\\Log\\.*
- name: Model
collectors:
- type: className
regex: ^Codeigniter\\.*Model$
- name: Modules
collectors:
- type: className
regex: ^Codeigniter\\Modules\\.*
- name: Pager
collectors:
- type: className
regex: ^Codeigniter\\Pager\\.*
- name: Publisher
collectors:
- type: className
regex: ^Codeigniter\\Publisher\\.*
- name: RESTful
collectors:
- type: className
regex: ^Codeigniter\\RESTful\\.*
- name: Router
collectors:
- type: className
regex: ^Codeigniter\\Router\\.*
- name: Security
collectors:
- type: className
regex: ^Codeigniter\\Security\\.*
- name: Session
collectors:
- type: className
regex: ^Codeigniter\\Session\\.*
- name: Throttle
collectors:
- type: className
regex: ^Codeigniter\\Throttle\\.*
- name: Typography
collectors:
- type: className
regex: ^Codeigniter\\Typography\\.*
- name: URI
collectors:
- type: className
regex: ^CodeIgniter\\HTTP\\URI$
- name: Validation
collectors:
- type: bool
must:
- type: className
regex: ^Codeigniter\\Validation\\.*
must_not:
- type: className
regex: ^Codeigniter\\Validation\\FormatRules$
- name: View
collectors:
- type: className
regex: ^Codeigniter\\View\\.*
ruleset:
API:
- Format
- HTTP
Controller:
- HTTP
- Validation
Database:
- Entity
- Events
Email:
- Events
Entity:
- I18n
Filters:
- HTTP
Honeypot:
- Filters
- HTTP
HTTP:
- Cookie
- Files
- URI
Images:
- Files
Model:
- Database
- I18n
- Pager
- Validation
Pager:
- URI
- View
Publisher:
- Files
- URI
RESTful:
- +API
- +Controller
Router:
- HTTP
Security:
- Cookie
- HTTP
Session:
- Cookie
- Database
Throttle:
- Cache
Validation:
- HTTP
View:
- Cache
skip_violations:
# Individual class exemptions
CodeIgniter\Entity\Cast\URICast:
- CodeIgniter\HTTP\URI
CodeIgniter\Log\Handlers\ChromeLoggerHandler:
- CodeIgniter\HTTP\ResponseInterface
CodeIgniter\View\Table:
- CodeIgniter\Database\BaseResult
CodeIgniter\View\Plugins:
- CodeIgniter\HTTP\URI
# BC changes that should be fixed
CodeIgniter\HTTP\ResponseTrait:
- CodeIgniter\Pager\PagerInterface
CodeIgniter\HTTP\ResponseInterface:
- CodeIgniter\Pager\PagerInterface
CodeIgniter\HTTP\Response:
- CodeIgniter\Pager\PagerInterface
CodeIgniter\HTTP\RedirectResponse:
- CodeIgniter\Pager\PagerInterface
CodeIgniter\HTTP\DownloadResponse:
- CodeIgniter\Pager\PagerInterface
CodeIgniter\Validation\Validation:
- CodeIgniter\View\RendererInterface

View File

@ -228,38 +228,77 @@ class CLI
}
if (is_string($options)) {
$extraOutput = ' [' . static::color($options, 'white') . ']';
$extraOutput = ' [' . static::color($options, 'green') . ']';
$default = $options;
}
if (is_array($options) && $options) {
$opts = $options;
$extraOutputDefault = static::color($opts[0], 'white');
$extraOutputDefault = static::color($opts[0], 'green');
unset($opts[0]);
if (empty($opts)) {
$extraOutput = $extraOutputDefault;
} else {
$extraOutput = ' [' . $extraOutputDefault . ', ' . implode(', ', $opts) . ']';
$validation[] = 'in_list[' . implode(',', $options) . ']';
$extraOutput = '[' . $extraOutputDefault . ', ' . implode(', ', $opts) . ']';
$validation[] = 'in_list[' . implode(', ', $options) . ']';
}
$default = $options[0];
}
static::fwrite(STDOUT, $field . $extraOutput . ': ');
static::fwrite(STDOUT, $field . (trim($field) ? ' ' : '') . $extraOutput . ': ');
// Read the input from keyboard.
$input = trim(static::input()) ?: $default;
if ($validation) {
while (! static::validate($field, $input, $validation)) {
while (! static::validate(trim($field), $input, $validation)) {
$input = static::prompt($field, $options, $validation);
}
}
return empty($input) ? '' : $input;
return $input;
}
/**
* prompt(), but based on the option's key
*
* @param array|string $text Output "field" text or an one or two value array where the first value is the text before listing the options
* and the second value the text before asking to select one option. Provide empty string to omit
* @param array $options A list of options (array(key => description)), the first option will be the default value
* @param array|string|null $validation Validation rules
*
* @return string The selected key of $options
*
* @codeCoverageIgnore
*/
public static function promptByKey($text, array $options, $validation = null): string
{
if (is_string($text)) {
$text = [$text];
} elseif (! is_array($text)) {
throw new InvalidArgumentException('$text can only be of type string|array');
}
if (! $options) {
throw new InvalidArgumentException('No options to select from were provided');
}
if ($line = array_shift($text)) {
CLI::write($line);
}
// +2 for the square brackets around the key
$keyMaxLength = max(array_map('mb_strwidth', array_keys($options))) + 2;
foreach ($options as $key => $description) {
$name = str_pad(' [' . $key . '] ', $keyMaxLength + 4, ' ');
CLI::write(CLI::color($name, 'green') . CLI::wrap($description, 125, $keyMaxLength + 4));
}
return static::prompt(PHP_EOL . array_shift($text), array_keys($options), $validation);
}
/**

View File

@ -215,14 +215,14 @@ trait GeneratorTrait
// Trims input, normalize separators, and ensure that all paths are in Pascalcase.
$class = ltrim(implode('\\', array_map('pascalize', explode('\\', str_replace('/', '\\', trim($class))))), '\\/');
// Gets the namespace from input.
$namespace = trim(str_replace('/', '\\', $this->getOption('namespace') ?? APP_NAMESPACE), '\\');
// Gets the namespace from input. Don't forget the ending backslash!
$namespace = trim(str_replace('/', '\\', $this->getOption('namespace') ?? APP_NAMESPACE), '\\') . '\\';
if (strncmp($class, $namespace, strlen($namespace)) === 0) {
return $class; // @codeCoverageIgnore
}
return $namespace . '\\' . $this->directory . '\\' . str_replace('/', '\\', $class);
return $namespace . $this->directory . '\\' . str_replace('/', '\\', $class);
}
/**

View File

@ -13,6 +13,7 @@ namespace CodeIgniter\Cache;
use CodeIgniter\Cache\Exceptions\CacheException;
use CodeIgniter\Exceptions\CriticalError;
use CodeIgniter\Test\Mock\MockCache;
use Config\Cache;
/**
@ -20,6 +21,20 @@ use Config\Cache;
*/
class CacheFactory
{
/**
* The class to use when mocking
*
* @var string
*/
public static $mockClass = MockCache::class;
/**
* The service to inject the mock as
*
* @var string
*/
public static $mockServiceName = 'cache';
/**
* Attempts to create the desired cache handler, based upon the
*
@ -42,14 +57,12 @@ class CacheFactory
throw CacheException::forHandlerNotFound();
}
// Get an instance of our handler.
$adapter = new $config->validHandlers[$handler]($config);
if (! $adapter->isSupported()) {
$adapter = new $config->validHandlers[$backup]($config);
if (! $adapter->isSupported()) {
// Log stuff here, don't throw exception. No need to raise a fuss.
// Fall back to the dummy adapter.
$adapter = new $config->validHandlers['dummy']();
}
@ -60,7 +73,6 @@ class CacheFactory
try {
$adapter->initialize();
} catch (CriticalError $e) {
// log the fact that an exception occurred as well what handler we are resorting to
log_message('critical', $e->getMessage() . ' Resorting to using ' . $backup . ' handler.');
// get the next best cache handler (or dummy if the $backup also fails)

View File

@ -22,8 +22,10 @@ use InvalidArgumentException;
abstract class BaseHandler implements CacheInterface
{
/**
* Reserved characters that cannot be used in a key or tag.
* Reserved characters that cannot be used in a key or tag. May be overridden by the config.
* From https://github.com/symfony/cache-contracts/blob/c0446463729b89dd4fa62e9aeecc80287323615d/ItemInterface.php#L43
*
* @deprecated in favor of the Cache config
*/
public const RESERVED_CHARACTERS = '{}()/\@:';
@ -58,8 +60,10 @@ abstract class BaseHandler implements CacheInterface
if ($key === '') {
throw new InvalidArgumentException('Cache key cannot be empty.');
}
if (strpbrk($key, self::RESERVED_CHARACTERS) !== false) {
throw new InvalidArgumentException('Cache key contains reserved characters ' . self::RESERVED_CHARACTERS);
$reserved = config('Cache')->reservedCharacters ?? self::RESERVED_CHARACTERS;
if ($reserved && strpbrk($key, $reserved) !== false) {
throw new InvalidArgumentException('Cache key contains reserved characters ' . $reserved);
}
// If the key with prefix exceeds the length then return the hashed version

View File

@ -210,7 +210,7 @@ class FileHandler extends BaseHandler
}
return [
'expire' => $data['time'] + $data['ttl'],
'expire' => $data['ttl'] > 0 ? $data['time'] + $data['ttl'] : null,
'mtime' => filemtime($this->path . $key),
'data' => $data['data'],
];

View File

@ -127,7 +127,9 @@ class PredisHandler extends BaseHandler
return false;
}
$this->redis->expireat($key, time() + $ttl);
if ($ttl) {
$this->redis->expireat($key, time() + $ttl);
}
return true;
}

View File

@ -357,7 +357,7 @@ class CodeIgniter
{
$routeFilter = $this->tryToRouteIt($routes);
// Run "before" filters
// Start up the filters
$filters = Services::filters();
// If any filters were specified within the routes file,
@ -371,7 +371,10 @@ class CodeIgniter
// Never run filters when running through Spark cli
if (! defined('SPARKED')) {
// Run "before" filters
$this->benchmark->start('before_filters');
$possibleResponse = $filters->run($uri, 'before');
$this->benchmark->stop('before_filters');
// If a ResponseInterface instance is returned then send it back to the client and stop
if ($possibleResponse instanceof ResponseInterface) {
@ -410,8 +413,11 @@ class CodeIgniter
// Never run filters when running through Spark cli
if (! defined('SPARKED')) {
$filters->setResponse($this->response);
// Run "after" filters
$this->benchmark->start('after_filters');
$response = $filters->run($uri, 'after');
$this->benchmark->stop('after_filters');
} else {
$response = $this->response;
@ -490,7 +496,9 @@ class CodeIgniter
*/
protected function startBenchmark()
{
$this->startTime = microtime(true);
if ($this->startTime === null) {
$this->startTime = microtime(true);
}
$this->benchmark = Services::timer();
$this->benchmark->start('total_execution', $this->startTime);

View File

@ -101,10 +101,11 @@ class MigrationGenerator extends BaseCommand
$table = $this->getOption('table');
$DBGroup = $this->getOption('dbgroup');
$data['session'] = true;
$data['table'] = is_string($table) ? $table : 'ci_sessions';
$data['DBGroup'] = is_string($DBGroup) ? $DBGroup : 'default';
$data['matchIP'] = config('App')->sessionMatchIP;
$data['session'] = true;
$data['table'] = is_string($table) ? $table : 'ci_sessions';
$data['DBGroup'] = is_string($DBGroup) ? $DBGroup : 'default';
$data['DBDriver'] = config('Database')->{$data['DBGroup']}['DBDriver'];
$data['matchIP'] = config('App')->sessionMatchIP;
}
return $this->parseTemplate($class, [], [], $data);

View File

@ -12,17 +12,23 @@ class {class} extends Migration
public function up()
{
$this->forge->addField([
'id' => ['type' => 'VARCHAR', 'constraint' => 128, 'null' => false],
'id' => ['type' => 'VARCHAR', 'constraint' => 128, 'null' => false],
<?php if ($DBDriver === 'MySQLi'): ?>
'ip_address' => ['type' => 'VARCHAR', 'constraint' => 45, 'null' => false],
'timestamp' => ['type' => 'INT', 'unsigned' => true, 'null' => false, 'default' => 0],
'data' => ['type' => 'TEXT', 'null' => false, 'default' => ''],
'timestamp timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL',
'data' => ['type' => 'BLOB', 'null' => false],
<?php elseif ($DBDriver === 'Postgre'): ?>
'ip_address inet NOT NULL',
'timestamp timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL',
"data bytea DEFAULT '' NOT NULL",
<?php endif; ?>
]);
<?php if ($matchIP) : ?>
$this->forge->addKey(['id', 'ip_address'], true);
<?php else: ?>
$this->forge->addKey('id', true);
<?php endif ?>
$this->forge->addKey('timestamp');
<?php if ($matchIP) : ?>
$this->forge->addKey(['id', 'ip_address'], true);
<?php else: ?>
$this->forge->addKey('id', true);
<?php endif ?>
$this->forge->addKey('timestamp');
$this->forge->createTable('<?= $table ?>', true);
}

View File

@ -0,0 +1,104 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Commands\Utilities;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use CodeIgniter\Publisher\Publisher;
/**
* Discovers all Publisher classes from the "Publishers/" directory
* across namespaces. Executes `publish()` from each instance, parsing
* each result.
*/
class Publish extends BaseCommand
{
/**
* The group the command is lumped under
* when listing commands.
*
* @var string
*/
protected $group = 'CodeIgniter';
/**
* The Command's name
*
* @var string
*/
protected $name = 'publish';
/**
* The Command's short description
*
* @var string
*/
protected $description = 'Discovers and executes all predefined Publisher classes.';
/**
* The Command's usage
*
* @var string
*/
protected $usage = 'publish [<directory>]';
/**
* The Command's arguments
*
* @var array<string, string>
*/
protected $arguments = [
'directory' => '[Optional] The directory to scan within each namespace. Default: "Publishers".',
];
/**
* the Command's Options
*
* @var array
*/
protected $options = [];
/**
* Displays the help for the spark cli script itself.
*/
public function run(array $params)
{
$directory = array_shift($params) ?? 'Publishers';
if ([] === $publishers = Publisher::discover($directory)) {
CLI::write(lang('Publisher.publishMissing', [$directory]));
return;
}
foreach ($publishers as $publisher) {
if ($publisher->publish()) {
CLI::write(lang('Publisher.publishSuccess', [
get_class($publisher),
count($publisher->getPublished()),
$publisher->getDestination(),
]), 'green');
} else {
CLI::error(lang('Publisher.publishFailure', [
get_class($publisher),
$publisher->getDestination(),
]), 'light_gray', 'red');
foreach ($publisher->getErrors() as $file => $exception) {
CLI::write($file);
CLI::error($exception->getMessage());
CLI::newLine();
}
}
}
}
}

View File

@ -644,22 +644,14 @@ if (! function_exists('is_cli')) {
/**
* Check if PHP was invoked from the command line.
*
* @codeCoverageIgnore Cannot be tested fully as PHPUnit always run in CLI
* @codeCoverageIgnore Cannot be tested fully as PHPUnit always run in php-cli
*/
function is_cli(): bool
{
if (PHP_SAPI === 'cli') {
return true;
}
if (defined('STDIN')) {
return true;
}
if (stristr(PHP_SAPI, 'cgi') && getenv('TERM')) {
return true;
}
if (! isset($_SERVER['REMOTE_ADDR'], $_SERVER['HTTP_USER_AGENT']) && isset($_SERVER['argv']) && count($_SERVER['argv']) > 0) {
return true;
}
@ -725,8 +717,23 @@ if (! function_exists('lang')) {
*/
function lang(string $line, array $args = [], ?string $locale = null)
{
return Services::language($locale)
->getLine($line, $args);
$language = Services::language();
// Get active locale
$activeLocale = $language->getLocale();
if ($locale && $locale !== $activeLocale) {
$language->setLocale($locale);
}
$line = $language->getLine($line, $args);
if ($locale && $locale !== $activeLocale) {
// Reset to active locale
$language->setLocale($activeLocale);
}
return $line;
}
}
@ -819,9 +826,7 @@ if (! function_exists('redirect')) {
/**
* Convenience method that works with the current global $request and
* $router instances to redirect using named/reverse-routed routes
* to determine the URL to go to. If nothing is found, will treat
* as a traditional redirect and pass the string in, letting
* $response->redirect() determine the correct method and code.
* to determine the URL to go to.
*
* If more control is needed, you must use $response->redirect explicitly.
*

View File

@ -0,0 +1,42 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Config;
/**
* Publisher Configuration
*
* Defines basic security restrictions for the Publisher class
* to prevent abuse by injecting malicious files into a project.
*/
class Publisher extends BaseConfig
{
/**
* A list of allowed destinations with a (pseudo-)regex
* of allowed files for each destination.
* Attempts to publish to directories not in this list will
* result in a PublisherException. Files that do no fit the
* pattern will cause copy/merge to fail.
*
* @var array<string,string>
*/
public $restrictions = [
ROOTPATH => '*',
FCPATH => '#\.(?css|js|map|htm?|xml|json|webmanifest|tff|eot|woff?|gif|jpe?g|tiff?|png|webp|bmp|ico|svg)$#i',
];
/**
* Disables Registrars to prevent modules from altering the restrictions.
*/
final protected function registerProperties()
{
}
}

View File

@ -1339,12 +1339,12 @@ class BaseBuilder
* Allows key/value pairs to be set for insert(), update() or replace().
*
* @param array|object|string $key Field name, or an array of field/value pairs
* @param string|null $value Field value, if $key is a single field
* @param mixed $value Field value, if $key is a single field
* @param bool|null $escape Whether to escape values and identifiers
*
* @return $this
*/
public function set($key, ?string $value = '', ?bool $escape = null)
public function set($key, $value = '', ?bool $escape = null)
{
$key = $this->objectToArray($key);

View File

@ -351,12 +351,25 @@ class Forge
throw new InvalidArgumentException('Field information is required for that operation.');
}
$this->fields[] = $field;
$fieldName = explode(' ', $field, 2)[0];
$fieldName = trim($fieldName, '`\'"');
$this->fields[$fieldName] = $field;
}
}
if (is_array($field)) {
$this->fields = array_merge($this->fields, $field);
foreach ($field as $idx => $f) {
if (is_string($f)) {
$this->addField($f);
continue;
}
if (is_array($f)) {
$this->fields = array_merge($this->fields, [$idx => $f]);
}
}
}
return $this;
@ -365,21 +378,37 @@ class Forge
/**
* Add Foreign Key
*
* @param string|string[] $fieldName
* @param string|string[] $tableField
*
* @throws DatabaseException
*
* @return Forge
*/
public function addForeignKey(string $fieldName = '', string $tableName = '', string $tableField = '', string $onUpdate = '', string $onDelete = '')
public function addForeignKey($fieldName = '', string $tableName = '', $tableField = '', string $onUpdate = '', string $onDelete = '')
{
if (! isset($this->fields[$fieldName])) {
throw new DatabaseException(lang('Database.fieldNotExists', [$fieldName]));
$fieldName = (array) $fieldName;
$tableField = (array) $tableField;
$errorNames = [];
foreach ($fieldName as $name) {
if (! isset($this->fields[$name])) {
$errorNames[] = $name;
}
}
$this->foreignKeys[$fieldName] = [
'table' => $tableName,
'field' => $tableField,
'onDelete' => strtoupper($onDelete),
'onUpdate' => strtoupper($onUpdate),
if ($errorNames !== []) {
$errorNames[0] = implode(', ', $errorNames);
throw new DatabaseException(lang('Database.fieldNotExists', $errorNames));
}
$this->foreignKeys[] = [
'field' => $fieldName,
'referenceTable' => $tableName,
'referenceField' => $tableField,
'onDelete' => strtoupper($onDelete),
'onUpdate' => strtoupper($onUpdate),
];
return $this;
@ -761,7 +790,7 @@ class Forge
$fields = [];
foreach ($this->fields as $key => $attributes) {
if (is_int($key) && ! is_array($attributes)) {
if (! is_array($attributes)) {
$fields[] = ['_literal' => $attributes];
continue;
@ -996,11 +1025,15 @@ class Forge
'SET DEFAULT',
];
foreach ($this->foreignKeys as $field => $fkey) {
$nameIndex = $table . '_' . $field . '_foreign';
foreach ($this->foreignKeys as $fkey) {
$nameIndex = $table . '_' . implode('_', $fkey['field']) . '_foreign';
$nameIndexFilled = $this->db->escapeIdentifiers($nameIndex);
$foreignKeyFilled = implode(', ', $this->db->escapeIdentifiers($fkey['field']));
$referenceTableFilled = $this->db->escapeIdentifiers($this->db->DBPrefix . $fkey['referenceTable']);
$referenceFieldFilled = implode(', ', $this->db->escapeIdentifiers($fkey['referenceField']));
$sql .= ",\n\tCONSTRAINT " . $this->db->escapeIdentifiers($nameIndex)
. ' FOREIGN KEY(' . $this->db->escapeIdentifiers($field) . ') REFERENCES ' . $this->db->escapeIdentifiers($this->db->DBPrefix . $fkey['table']) . ' (' . $this->db->escapeIdentifiers($fkey['field']) . ')';
$formatSql = ",\n\tCONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s(%s)";
$sql .= sprintf($formatSql, $nameIndexFilled, $foreignKeyFilled, $referenceTableFilled, $referenceFieldFilled);
if ($fkey['onDelete'] !== false && in_array($fkey['onDelete'], $allowActions, true)) {
$sql .= ' ON DELETE ' . $fkey['onDelete'];

View File

@ -58,6 +58,17 @@ class Connection extends BaseConnection
*/
public $mysqli;
/**
* MySQLi constant
*
* For unbuffered queries use `MYSQLI_USE_RESULT`.
*
* Default mode for buffered queries uses `MYSQLI_STORE_RESULT`.
*
* @var int
*/
public $resultMode = MYSQLI_STORE_RESULT;
/**
* Connect to the database.
*
@ -143,7 +154,6 @@ class Connection extends BaseConnection
}
}
$clientFlags += MYSQLI_CLIENT_SSL;
$this->mysqli->ssl_set(
$ssl['key'] ?? null,
$ssl['cert'] ?? null,
@ -152,6 +162,8 @@ class Connection extends BaseConnection
$ssl['cipher'] ?? null
);
}
$clientFlags += MYSQLI_CLIENT_SSL;
}
try {
@ -277,7 +289,7 @@ class Connection extends BaseConnection
}
try {
return $this->connID->query($this->prepQuery($sql));
return $this->connID->query($this->prepQuery($sql), $this->resultMode);
} catch (mysqli_sql_exception $e) {
log_message('error', $e->getMessage());

View File

@ -170,6 +170,16 @@ class Builder extends BaseBuilder
return $this->keyPermission ? $this->addIdentity($fullTableName, $statement) : $statement;
}
/**
* Insert batch statement
*
* Generates a platform-specific insert string from the supplied data.
*/
protected function _insertBatch(string $table, array $keys, array $values): string
{
return 'INSERT ' . $this->compileIgnore('insert') . 'INTO ' . $this->getFullName($table) . ' (' . implode(', ', $keys) . ') VALUES ' . implode(', ', $values);
}
/**
* Generates a platform-specific update string from the supplied data
*/
@ -183,12 +193,48 @@ class Builder extends BaseBuilder
$fullTableName = $this->getFullName($table);
$statement = 'UPDATE ' . (empty($this->QBLimit) ? '' : 'TOP(' . $this->QBLimit . ') ') . $fullTableName . ' SET '
. implode(', ', $valstr) . $this->compileWhereHaving('QBWhere') . $this->compileOrderBy();
$statement = sprintf('UPDATE %s%s SET ', empty($this->QBLimit) ? '' : 'TOP(' . $this->QBLimit . ') ', $fullTableName);
$statement .= implode(', ', $valstr)
. $this->compileWhereHaving('QBWhere')
. $this->compileOrderBy();
return $this->keyPermission ? $this->addIdentity($fullTableName, $statement) : $statement;
}
/**
* Update_Batch statement
*
* Generates a platform-specific batch update string from the supplied data
*/
protected function _updateBatch(string $table, array $values, string $index): string
{
$ids = [];
$final = [];
foreach ($values as $val) {
$ids[] = $val[$index];
foreach (array_keys($val) as $field) {
if ($field !== $index) {
$final[$field][] = 'WHEN ' . $index . ' = ' . $val[$index] . ' THEN ' . $val[$field];
}
}
}
$cases = '';
foreach ($final as $k => $v) {
$cases .= $k . " = CASE \n"
. implode("\n", $v) . "\n"
. 'ELSE ' . $k . ' END, ';
}
$this->where($index . ' IN(' . implode(',', $ids) . ')', null, false);
return 'UPDATE ' . $this->compileIgnore('update') . ' ' . $this->getFullName($table) . ' SET ' . substr($cases, 0, -2) . $this->compileWhereHaving('QBWhere');
}
/**
* Increments a numeric column by the specified value.
*
@ -203,6 +249,7 @@ class Builder extends BaseBuilder
} else {
$values = [$column => "{$column} + {$value}"];
}
$sql = $this->_update($this->QBFrom[0], $values);
return $this->db->query($sql, $this->binds, false);
@ -222,6 +269,7 @@ class Builder extends BaseBuilder
} else {
$values = [$column => "{$column} + {$value}"];
}
$sql = $this->_update($this->QBFrom[0], $values);
return $this->db->query($sql, $this->binds, false);
@ -304,9 +352,10 @@ class Builder extends BaseBuilder
return $sql;
}
$this->db->simpleQuery('SET IDENTITY_INSERT ' . $this->db->escapeIdentifiers($table) . ' ON');
$this->db->simpleQuery('SET IDENTITY_INSERT ' . $this->getFullName($table) . ' ON');
$result = $this->db->query($sql, $this->binds, false);
$this->db->simpleQuery('SET IDENTITY_INSERT ' . $this->db->escapeIdentifiers($table) . ' OFF');
$this->db->simpleQuery('SET IDENTITY_INSERT ' . $this->getFullName($table) . ' OFF');
return $result;
}
@ -410,6 +459,40 @@ class Builder extends BaseBuilder
return $this;
}
/**
* "Count All" query
*
* Generates a platform-specific query string that counts all records in
* the particular table
*
* @param bool $reset Are we want to clear query builder values?
*
* @return int|string when $test = true
*/
public function countAll(bool $reset = true)
{
$table = $this->QBFrom[0];
$sql = $this->countString . $this->db->escapeIdentifiers('numrows') . ' FROM ' . $this->getFullName($table);
if ($this->testMode) {
return $sql;
}
$query = $this->db->query($sql, null, false);
if (empty($query->getResult())) {
return 0;
}
$query = $query->getRow();
if ($reset === true) {
$this->resetSelect();
}
return (int) $query->numrows;
}
/**
* Delete statement
*/
@ -504,9 +587,10 @@ class Builder extends BaseBuilder
}
$sql .= $this->compileWhereHaving('QBWhere')
. $this->compileGroupBy()
. $this->compileWhereHaving('QBHaving')
. $this->compileOrderBy(); // ORDER BY
. $this->compileGroupBy()
. $this->compileWhereHaving('QBHaving')
. $this->compileOrderBy(); // ORDER BY
// LIMIT
if ($this->QBLimit) {
$sql = $this->_limit($sql . "\n");

View File

@ -215,7 +215,7 @@ class Connection extends BaseConnection
*/
protected function _indexData(string $table): array
{
$sql = 'EXEC sp_helpindex ' . $this->escape($table);
$sql = 'EXEC sp_helpindex ' . $this->escape($this->schema . '.' . $table);
if (($query = $this->query($sql)) === false) {
throw new DatabaseException(lang('Database.failGetIndexData'));

View File

@ -11,6 +11,7 @@
namespace CodeIgniter\Database\SQLSRV;
use CodeIgniter\Database\BaseConnection;
use CodeIgniter\Database\Forge as BaseForge;
/**
@ -23,7 +24,7 @@ class Forge extends BaseForge
*
* @var string
*/
protected $dropConstraintStr = 'ALTER TABLE %s DROP CONSTRAINT %s';
protected $dropConstraintStr;
/**
* CREATE DATABASE IF statement
@ -61,7 +62,7 @@ class Forge extends BaseForge
*
* @var string
*/
protected $renameTableStr = 'EXEC sp_rename %s , %s ;';
protected $renameTableStr;
/**
* UNSIGNED support
@ -80,21 +81,32 @@ class Forge extends BaseForge
*
* @var string
*/
protected $createTableIfStr = "IF NOT EXISTS (SELECT * FROM sysobjects WHERE ID = object_id(N'%s') AND OBJECTPROPERTY(id, N'IsUserTable') = 1)\nCREATE TABLE";
protected $createTableIfStr;
/**
* CREATE TABLE statement
*
* @var string
*/
protected $createTableStr = "%s %s (%s\n) ";
protected $createTableStr;
/**
* DROP TABLE IF statement
*
* @var string
*/
protected $_drop_table_if = "IF EXISTS (SELECT * FROM sysobjects WHERE ID = object_id(N'%s') AND OBJECTPROPERTY(id, N'IsUserTable') = 1)\nDROP TABLE";
public function __construct(BaseConnection $db)
{
parent::__construct($db);
$this->createTableIfStr = 'IF NOT EXISTS'
. '(SELECT t.name, s.name as schema_name, t.type_desc '
. 'FROM sys.tables t '
. 'INNER JOIN sys.schemas s on s.schema_id = t.schema_id '
. "WHERE s.name=N'" . $this->db->schema . "' "
. "AND t.name=REPLACE(N'%s', '\"', '') "
. "AND t.type_desc='USER_TABLE')\nCREATE TABLE ";
$this->createTableStr = '%s ' . $this->db->escapeIdentifiers($this->db->schema) . ".%s (%s\n) ";
$this->renameTableStr = 'EXEC sp_rename [' . $this->db->escapeIdentifiers($this->db->schema) . '.%s] , %s ;';
$this->dropConstraintStr = 'ALTER TABLE ' . $this->db->escapeIdentifiers($this->db->schema) . '.%s DROP CONSTRAINT %s';
}
/**
* CREATE TABLE attributes
@ -111,9 +123,6 @@ class Forge extends BaseForge
*/
protected function _alterTable(string $alterType, string $table, $field)
{
if ($alterType === 'ADD') {
return parent::_alterTable($alterType, $table, $field);
}
// Handle DROP here
if ($alterType === 'DROP') {
@ -133,7 +142,7 @@ class Forge extends BaseForge
}
}
$sql = 'ALTER TABLE [' . $table . '] DROP ';
$sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table) . ' DROP ';
$fields = array_map(static function ($item) {
return 'COLUMN [' . trim($item) . ']';
@ -142,10 +151,19 @@ class Forge extends BaseForge
return $sql . implode(',', $fields);
}
$sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table);
$sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table);
$sql .= ($alterType === 'ADD') ? 'ADD ' : ' ';
$sqls = [];
if ($alterType === 'ADD') {
foreach ($field as $data) {
$sqls[] = $sql . ($data['_literal'] !== false ? $data['_literal'] : $this->_processColumn($data));
}
return $sqls;
}
foreach ($field as $data) {
if ($data['_literal'] !== false) {
return false;
@ -198,6 +216,46 @@ class Forge extends BaseForge
return $this->db->simpleQuery($sql);
}
/**
* Process indexes
*
* @return array|string
*/
protected function _processIndexes(string $table)
{
$sqls = [];
for ($i = 0, $c = count($this->keys); $i < $c; $i++) {
$this->keys[$i] = (array) $this->keys[$i];
for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++) {
if (! isset($this->fields[$this->keys[$i][$i2]])) {
unset($this->keys[$i][$i2]);
}
}
if (count($this->keys[$i]) <= 0) {
continue;
}
if (in_array($i, $this->uniqueKeys, true)) {
$sqls[] = 'ALTER TABLE '
. $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table)
. ' ADD CONSTRAINT ' . $this->db->escapeIdentifiers($table . '_' . implode('_', $this->keys[$i]))
. ' UNIQUE (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i])) . ');';
continue;
}
$sqls[] = 'CREATE INDEX '
. $this->db->escapeIdentifiers($table . '_' . implode('_', $this->keys[$i]))
. ' ON ' . $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table)
. ' (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i])) . ');';
}
return $sqls;
}
/**
* Process column
*/
@ -224,12 +282,15 @@ class Forge extends BaseForge
$allowActions = ['CASCADE', 'SET NULL', 'NO ACTION', 'RESTRICT', 'SET DEFAULT'];
foreach ($this->foreignKeys as $field => $fkey) {
$nameIndex = $table . '_' . $field . '_foreign';
foreach ($this->foreignKeys as $fkey) {
$nameIndex = $table . '_' . implode('_', $fkey['field']) . '_foreign';
$nameIndexFilled = $this->db->escapeIdentifiers($nameIndex);
$foreignKeyFilled = implode(', ', $this->db->escapeIdentifiers($fkey['field']));
$referenceTableFilled = $this->db->escapeIdentifiers($this->db->DBPrefix . $fkey['referenceTable']);
$referenceFieldFilled = implode(', ', $this->db->escapeIdentifiers($fkey['referenceField']));
$sql .= ",\n\t CONSTRAINT " . $this->db->escapeIdentifiers($nameIndex)
. ' FOREIGN KEY (' . $this->db->escapeIdentifiers($field) . ') '
. ' REFERENCES ' . $this->db->escapeIdentifiers($this->db->getPrefix() . $fkey['table']) . ' (' . $this->db->escapeIdentifiers($fkey['field']) . ')';
$formatSql = ",\n\tCONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s(%s)";
$sql .= sprintf($formatSql, $nameIndexFilled, $foreignKeyFilled, $referenceTableFilled, $referenceFieldFilled);
if ($fkey['onDelete'] !== false && in_array($fkey['onDelete'], $allowActions, true)) {
$sql .= ' ON DELETE ' . $fkey['onDelete'];
@ -245,8 +306,6 @@ class Forge extends BaseForge
/**
* Process primary keys
*
* @param string $table Table name
*/
protected function _processPrimaryKeys(string $table): string
{
@ -293,6 +352,10 @@ class Forge extends BaseForge
$attributes['TYPE'] = 'DATETIME';
break;
case 'BOOLEAN':
$attributes['TYPE'] = 'BIT';
break;
default:
break;
}

View File

@ -12,6 +12,7 @@
namespace CodeIgniter\Database\SQLSRV;
use CodeIgniter\Database\BaseUtils;
use CodeIgniter\Database\ConnectionInterface;
use CodeIgniter\Database\Exceptions\DatabaseException;
/**
@ -33,6 +34,13 @@ class Utils extends BaseUtils
*/
protected $optimizeTable = 'ALTER INDEX all ON %s REORGANIZE';
public function __construct(ConnectionInterface &$db)
{
parent::__construct($db);
$this->optimizeTable = 'ALTER INDEX all ON ' . $this->db->schema . '.%s REORGANIZE';
}
/**
* Platform dependent version of the backup function.
*

View File

@ -198,6 +198,10 @@ class Forge extends BaseForge
$attributes['TYPE'] = 'TEXT';
break;
case 'BOOLEAN':
$attributes['TYPE'] = 'INT';
break;
default:
break;
}

View File

@ -149,7 +149,7 @@ class Toolbar
$data['vars']['response'] = [
'statusCode' => $response->getStatusCode(),
'reason' => esc($response->getReason()),
'reason' => esc($response->getReasonPhrase()),
'contentType' => esc($response->getHeaderLine('content-type')),
];
@ -166,17 +166,37 @@ class Toolbar
* Called within the view to display the timeline itself.
*/
protected function renderTimeline(array $collectors, float $startTime, int $segmentCount, int $segmentDuration, array &$styles): string
{
$rows = $this->collectTimelineData($collectors);
$styleCount = 0;
// Use recursive render function
return $this->renderTimelineRecursive($rows, $startTime, $segmentCount, $segmentDuration, $styles, $styleCount);
}
/**
* Recursively renders timeline elements and their children.
*/
protected function renderTimelineRecursive(array $rows, float $startTime, int $segmentCount, int $segmentDuration, array &$styles, int &$styleCount, int $level = 0, bool $isChild = false): string
{
$displayTime = $segmentCount * $segmentDuration;
$rows = $this->collectTimelineData($collectors);
$output = '';
$styleCount = 0;
$output = '';
foreach ($rows as $row) {
$output .= '<tr>';
$output .= "<td>{$row['name']}</td>";
$output .= "<td>{$row['component']}</td>";
$output .= "<td class='debug-bar-alignRight'>" . number_format($row['duration'] * 1000, 2) . ' ms</td>';
$hasChildren = isset($row['children']) && ! empty($row['children']);
$open = $row['name'] === 'Controller';
if ($hasChildren) {
$output .= '<tr class="timeline-parent' . ($open ? ' timeline-parent-open' : '') . '" id="timeline-' . $styleCount . '_parent" onclick="ciDebugBar.toggleChildRows(\'timeline-' . $styleCount . '\');">';
} else {
$output .= '<tr>';
}
$output .= '<td class="' . ($isChild ? 'debug-bar-width30' : '') . '" style="--level: ' . $level . ';">' . ($hasChildren ? '<nav></nav>' : '') . $row['name'] . '</td>';
$output .= '<td class="' . ($isChild ? 'debug-bar-width10' : '') . '">' . $row['component'] . '</td>';
$output .= '<td class="' . ($isChild ? 'debug-bar-width10 ' : '') . 'debug-bar-alignRight">' . number_format($row['duration'] * 1000, 2) . ' ms</td>';
$output .= "<td class='debug-bar-noverflow' colspan='{$segmentCount}'>";
$offset = ((((float) $row['start'] - $startTime) * 1000) / $displayTime) * 100;
@ -189,6 +209,19 @@ class Toolbar
$output .= '</tr>';
$styleCount++;
// Add children if any
if ($hasChildren) {
$output .= '<tr class="child-row" id="timeline-' . ($styleCount - 1) . '_children" style="' . ($open ? '' : 'display: none;') . '">';
$output .= '<td colspan="' . ($segmentCount + 3) . '" class="child-container">';
$output .= '<table class="timeline">';
$output .= '<tbody>';
$output .= $this->renderTimelineRecursive($row['children'], $startTime, $segmentCount, $segmentDuration, $styles, $styleCount, $level + 1, true);
$output .= '</tbody>';
$output .= '</table>';
$output .= '</td>';
$output .= '</tr>';
}
}
return $output;
@ -213,10 +246,52 @@ class Toolbar
}
// Sort it
$sortArray = [
array_column($data, 'start'), SORT_NUMERIC, SORT_ASC,
array_column($data, 'duration'), SORT_NUMERIC, SORT_DESC,
&$data,
];
array_multisort(...$sortArray);
// Add end time to each element
array_walk($data, static function (&$row) {
$row['end'] = $row['start'] + $row['duration'];
});
// Group it
$data = $this->structureTimelineData($data);
return $data;
}
/**
* Arranges the already sorted timeline data into a parent => child structure.
*/
protected function structureTimelineData(array $elements): array
{
// We define ourselves as the first element of the array
$element = array_shift($elements);
// If we have children behind us, collect and attach them to us
while (! empty($elements) && $elements[array_key_first($elements)]['end'] <= $element['end']) {
$element['children'][] = array_shift($elements);
}
// Make sure our children know whether they have children, too
if (isset($element['children'])) {
$element['children'] = $this->structureTimelineData($element['children']);
}
// If we have no younger siblings, we can return
if (empty($elements)) {
return [$element];
}
// Make sure our younger siblings know their relatives, too
return array_merge([$element], $this->structureTimelineData($elements));
}
/**
* Returns an array of data from all of the modules
* that should be displayed in the 'Vars' tab.

View File

@ -1,9 +1,10 @@
/*! CodeIgniter 4 - Debug bar
* ============================================================================
* Forum: https://forum.codeigniter.com
* Github: https://github.com/codeigniter4/codeigniter4
* Slack: https://codeigniterchat.slack.com
* Website: https://codeigniter.com
/**
* This file is part of the CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
#debug-icon {
bottom: 0;
@ -117,7 +118,6 @@
overflow: hidden;
overflow-y: auto;
padding: 0 12px 0 12px;
/* give room for OS X scrollbar */
white-space: nowrap;
z-index: 10000; }
#debug-bar.fixed-top {
@ -200,7 +200,14 @@
padding: 5px;
position: relative; }
#debug-bar .timeline td:first-child {
border-left: 0; }
border-left: 0;
max-width: none; }
#debug-bar .timeline td.child-container {
padding: 0px; }
#debug-bar .timeline td.child-container .timeline {
margin: 0px; }
#debug-bar .timeline td.child-container .timeline td:first-child:not(.child-container) {
padding-left: calc(5px + 10px * var(--level)); }
#debug-bar .timeline .timer {
border-radius: 4px;
-moz-border-radius: 4px;
@ -209,6 +216,22 @@
padding: 5px;
position: absolute;
top: 30%; }
#debug-bar .timeline .timeline-parent {
cursor: pointer; }
#debug-bar .timeline .timeline-parent td:first-child nav {
background: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMCAxNTAiPjxwYXRoIGQ9Ik02IDdoMThsLTkgMTV6bTAgMzBoMThsLTkgMTV6bTAgNDVoMThsLTktMTV6bTAgMzBoMThsLTktMTV6bTAgMTJsMTggMThtLTE4IDBsMTgtMTgiIGZpbGw9IiM1NTUiLz48cGF0aCBkPSJNNiAxMjZsMTggMThtLTE4IDBsMTgtMTgiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlPSIjNTU1Ii8+PC9zdmc+") no-repeat scroll 0 0/15px 75px transparent;
background-position: 0 25%;
display: inline-block;
height: 15px;
width: 15px;
margin-right: 3px;
vertical-align: middle; }
#debug-bar .timeline .timeline-parent-open {
background-color: #DFDFDF; }
#debug-bar .timeline .timeline-parent-open td:first-child nav {
background-position: 0 75%; }
#debug-bar .timeline .child-row:hover {
background: transparent; }
#debug-bar .route-params,
#debug-bar .route-params-item {
vertical-align: top; }
@ -244,7 +267,9 @@
box-shadow: 0 0 4px #DFDFDF;
-moz-box-shadow: 0 0 4px #DFDFDF;
-webkit-box-shadow: 0 0 4px #DFDFDF; }
#debug-icon a:active, #debug-icon a:link, #debug-icon a:visited {
#debug-icon a:active,
#debug-icon a:link,
#debug-icon a:visited {
color: #DD8615; }
#debug-bar {
@ -330,7 +355,9 @@
box-shadow: 0 0 4px #DFDFDF;
-moz-box-shadow: 0 0 4px #DFDFDF;
-webkit-box-shadow: 0 0 4px #DFDFDF; }
#debug-icon a:active, #debug-icon a:link, #debug-icon a:visited {
#debug-icon a:active,
#debug-icon a:link,
#debug-icon a:visited {
color: #DD8615; }
#debug-bar {
background-color: #252525;
@ -414,7 +441,9 @@
box-shadow: 0 0 4px #DFDFDF;
-moz-box-shadow: 0 0 4px #DFDFDF;
-webkit-box-shadow: 0 0 4px #DFDFDF; }
#toolbarContainer.dark #debug-icon a:active, #toolbarContainer.dark #debug-icon a:link, #toolbarContainer.dark #debug-icon a:visited {
#toolbarContainer.dark #debug-icon a:active,
#toolbarContainer.dark #debug-icon a:link,
#toolbarContainer.dark #debug-icon a:visited {
color: #DD8615; }
#toolbarContainer.dark #debug-bar {
@ -505,7 +534,9 @@
box-shadow: 0 0 4px #DFDFDF;
-moz-box-shadow: 0 0 4px #DFDFDF;
-webkit-box-shadow: 0 0 4px #DFDFDF; }
#toolbarContainer.light #debug-icon a:active, #toolbarContainer.light #debug-icon a:link, #toolbarContainer.light #debug-icon a:visited {
#toolbarContainer.light #debug-icon a:active,
#toolbarContainer.light #debug-icon a:link,
#toolbarContainer.light #debug-icon a:visited {
color: #DD8615; }
#toolbarContainer.light #debug-bar {

View File

@ -141,6 +141,28 @@ var ciDebugBar = {
}
},
/**
* Toggle display of timeline child elements
*
* @param obj
*/
toggleChildRows : function (obj) {
if (typeof obj == 'string')
{
par = document.getElementById(obj + '_parent')
obj = document.getElementById(obj + '_children');
}
if (par && obj)
{
obj.style.display = obj.style.display == 'none' ? '' : 'none';
par.classList.toggle('timeline-parent-open');
}
},
//--------------------------------------------------------------------
/**
* Toggle tool bar from full to icon and icon to full
*/

View File

@ -41,7 +41,7 @@ class Entity implements JsonSerializable
*
* Example:
* $datamap = [
* 'db_name' => 'class_name'
* 'class_name' => 'db_name'
* ];
*/
protected $datamap = [];

View File

@ -0,0 +1,25 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Exceptions;
/**
* Exception for automatic logging.
*/
class TestException extends CriticalError
{
use DebugTraceableTrait;
public static function forInvalidMockClass(string $name)
{
return new static(lang('Test.invalidMockClass', [$name]));
}
}

View File

@ -23,4 +23,24 @@ class FileException extends RuntimeException implements ExceptionInterface
{
return new static(lang('Files.cannotMove', [$from, $to, $error]));
}
/**
* Throws when an item is expected to be a directory but is not or is missing.
*
* @param string $caller The method causing the exception
*/
public static function forExpectedDirectory(string $caller)
{
return new static(lang('Files.expectedDirectory', [$caller]));
}
/**
* Throws when an item is expected to be a file but is not or is missing.
*
* @param string $caller The method causing the exception
*/
public static function forExpectedFile(string $caller)
{
return new static(lang('Files.expectedFile', [$caller]));
}
}

View File

@ -0,0 +1,367 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Files;
use CodeIgniter\Files\Exceptions\FileException;
use CodeIgniter\Files\Exceptions\FileNotFoundException;
use Countable;
use Generator;
use InvalidArgumentException;
use IteratorAggregate;
/**
* File Collection Class
*
* Representation for a group of files, with utilities for locating,
* filtering, and ordering them.
*/
class FileCollection implements Countable, IteratorAggregate
{
/**
* The current list of file paths.
*
* @var string[]
*/
protected $files = [];
//--------------------------------------------------------------------
// Support Methods
//--------------------------------------------------------------------
/**
* Resolves a full path and verifies it is an actual directory.
*
* @throws FileException
*/
final protected static function resolveDirectory(string $directory): string
{
if (! is_dir($directory = set_realpath($directory))) {
$caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1];
throw FileException::forExpectedDirectory($caller['function']);
}
return $directory;
}
/**
* Resolves a full path and verifies it is an actual file.
*
* @throws FileException
*/
final protected static function resolveFile(string $file): string
{
if (! is_file($file = set_realpath($file))) {
$caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1];
throw FileException::forExpectedFile($caller['function']);
}
return $file;
}
/**
* Removes files that are not part of the given directory (recursive).
*
* @param string[] $files
*
* @return string[]
*/
final protected static function filterFiles(array $files, string $directory): array
{
$directory = self::resolveDirectory($directory);
return array_filter($files, static function (string $value) use ($directory): bool {
return strpos($value, $directory) === 0;
});
}
/**
* Returns any files whose `basename` matches the given pattern.
*
* @param string[] $files
* @param string $pattern Regex or pseudo-regex string
*
* @return string[]
*/
final protected static function matchFiles(array $files, string $pattern): array
{
// Convert pseudo-regex into their true form
if (@preg_match($pattern, '') === false) {
$pattern = str_replace(
['#', '.', '*', '?'],
['\#', '\.', '.*', '.'],
$pattern
);
$pattern = "#{$pattern}#";
}
return array_filter($files, static function ($value) use ($pattern) {
return (bool) preg_match($pattern, basename($value));
});
}
//--------------------------------------------------------------------
// Class Core
//--------------------------------------------------------------------
/**
* Loads the Filesystem helper and adds any initial files.
*
* @param string[] $files
*/
public function __construct(array $files = [])
{
helper(['filesystem']);
$this->add($files)->define();
}
/**
* Applies any initial inputs after the constructor.
* This method is a stub to be implemented by child classes.
*/
protected function define(): void
{
}
/**
* Optimizes and returns the current file list.
*
* @return string[]
*/
public function get(): array
{
$this->files = array_unique($this->files);
sort($this->files, SORT_STRING);
return $this->files;
}
/**
* Sets the file list directly, files are still subject to verification.
* This works as a "reset" method with [].
*
* @param string[] $files The new file list to use
*
* @return $this
*/
public function set(array $files)
{
$this->files = [];
return $this->addFiles($files);
}
/**
* Adds an array/single file or directory to the list.
*
* @param string|string[] $paths
*
* @return $this
*/
public function add($paths, bool $recursive = true)
{
$paths = (array) $paths;
foreach ($paths as $path) {
if (! is_string($path)) {
throw new InvalidArgumentException('FileCollection paths must be strings.');
}
try {
// Test for a directory
self::resolveDirectory($path);
} catch (FileException $e) {
return $this->addFile($path);
}
$this->addDirectory($path, $recursive);
}
return $this;
}
//--------------------------------------------------------------------
// File Handling
//--------------------------------------------------------------------
/**
* Verifies and adds files to the list.
*
* @param string[] $files
*
* @return $this
*/
public function addFiles(array $files)
{
foreach ($files as $file) {
$this->addFile($file);
}
return $this;
}
/**
* Verifies and adds a single file to the file list.
*
* @return $this
*/
public function addFile(string $file)
{
$this->files[] = self::resolveFile($file);
return $this;
}
/**
* Removes files from the list.
*
* @param string[] $files
*
* @return $this
*/
public function removeFiles(array $files)
{
$this->files = array_diff($this->files, $files);
return $this;
}
/**
* Removes a single file from the list.
*
* @return $this
*/
public function removeFile(string $file)
{
return $this->removeFiles([$file]);
}
//--------------------------------------------------------------------
// Directory Handling
//--------------------------------------------------------------------
/**
* Verifies and adds files from each
* directory to the list.
*
* @param string[] $directories
*
* @return $this
*/
public function addDirectories(array $directories, bool $recursive = false)
{
foreach ($directories as $directory) {
$this->addDirectory($directory, $recursive);
}
return $this;
}
/**
* Verifies and adds all files from a directory.
*
* @return $this
*/
public function addDirectory(string $directory, bool $recursive = false)
{
$directory = self::resolveDirectory($directory);
// Map the directory to depth 2 to so directories become arrays
foreach (directory_map($directory, 2, true) as $key => $path) {
if (is_string($path)) {
$this->addFile($directory . $path);
} elseif ($recursive && is_array($path)) {
$this->addDirectory($directory . $key, true);
}
}
return $this;
}
//--------------------------------------------------------------------
// Filtering
//--------------------------------------------------------------------
/**
* Removes any files from the list that match the supplied pattern
* (within the optional scope).
*
* @param string $pattern Regex or pseudo-regex string
* @param string|null $scope The directory to limit the scope
*
* @return $this
*/
public function removePattern(string $pattern, ?string $scope = null)
{
if ($pattern === '') {
return $this;
}
// Start with all files or those in scope
$files = $scope === null ? $this->files : self::filterFiles($this->files, $scope);
// Remove any files that match the pattern
return $this->removeFiles(self::matchFiles($files, $pattern));
}
/**
* Keeps only the files from the list that match
* (within the optional scope).
*
* @param string $pattern Regex or pseudo-regex string
* @param string|null $scope A directory to limit the scope
*
* @return $this
*/
public function retainPattern(string $pattern, ?string $scope = null)
{
if ($pattern === '') {
return $this;
}
// Start with all files or those in scope
$files = $scope === null ? $this->files : self::filterFiles($this->files, $scope);
// Matches the pattern within the scoped files and remove their inverse.
return $this->removeFiles(array_diff($files, self::matchFiles($files, $pattern)));
}
//--------------------------------------------------------------------
// Interface Methods
//--------------------------------------------------------------------
/**
* Returns the current number of files in the collection.
* Fulfills Countable.
*/
public function count(): int
{
return count($this->files);
}
/**
* Yields as an Iterator for the current files.
* Fulfills IteratorAggregate.
*
* @throws FileNotFoundException
*
* @return Generator<File>
*/
public function getIterator(): Generator
{
foreach ($this->get() as $file) {
yield new File($file, true);
}
}
}

View File

@ -535,6 +535,10 @@ class IncomingRequest extends Request
$data = dot_array_search($index, $this->getJSON(true));
if ($data === null) {
return null;
}
if (! is_array($data)) {
$filter = $filter ?? FILTER_DEFAULT;
$flags = is_array($flags) ? $flags : (is_numeric($flags) ? (int) $flags : 0);

View File

@ -9,8 +9,10 @@
* the LICENSE file that was distributed with this source code.
*/
use CodeIgniter\Exceptions\TestException;
use CodeIgniter\Model;
use CodeIgniter\Test\Fabricator;
use Config\Services;
// CodeIgniter Test Helpers
@ -20,16 +22,14 @@ if (! function_exists('fake')) {
*
* @param Model|object|string $model Instance or name of the model
* @param array|null $overrides Overriding data to pass to Fabricator::setOverrides()
* @param mixed $persist
* @param bool $persist
*
* @return array|object
*/
function fake($model, ?array $overrides = null, $persist = true)
{
// Get a model-appropriate Fabricator instance
$fabricator = new Fabricator($model);
// Set overriding data, if necessary
if ($overrides) {
$fabricator->setOverrides($overrides);
}
@ -41,3 +41,28 @@ if (! function_exists('fake')) {
return $fabricator->make();
}
}
if (! function_exists('mock')) {
/**
* Used within our test suite to mock certain system tools.
*
* @param string $className Fully qualified class name
*/
function mock(string $className)
{
$mockClass = $className::$mockClass;
$mockService = $className::$mockServiceName ?? '';
if (empty($mockClass) || ! class_exists($mockClass)) {
throw TestException::forInvalidMockClass($mockClass);
}
$mock = new $mockClass();
if (! empty($mockService)) {
Services::injectMock($mockService, $mock);
}
return $mock;
}
}

View File

@ -11,6 +11,8 @@
// Files language settings
return [
'fileNotFound' => 'File not found: {0}',
'cannotMove' => 'Could not move file {0} to {1} ({2}).',
'fileNotFound' => 'File not found: {0}',
'cannotMove' => 'Could not move file {0} to {1} ({2}).',
'expectedDirectory' => '{0} expects a valid directory.',
'expectedFile' => '{0} expects a valid file.',
];

View File

@ -0,0 +1,22 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
// Publisher language settings
return [
'collision' => 'Publisher encountered an unexpected {0} while copying {1} to {2}.',
'destinationNotAllowed' => 'Destination is not on the allowed list of Publisher directories: {0}',
'fileNotAllowed' => '{0} fails the following restriction for {1}: {2}',
// Publish Command
'publishMissing' => 'No Publisher classes detected in {0} across all namespaces.',
'publishSuccess' => '{0} published {1} file(s) to {2}.',
'publishFailure' => '{0} failed to publish to {1}!',
];

View File

@ -0,0 +1,15 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
// Testing language settings
return [
'invalidMockClass' => '{0} is not a valid Mock class',
];

View File

@ -557,13 +557,13 @@ class Model extends BaseModel
* data here. This allows it to be used with any of the other
* builder methods and still get validated data, like replace.
*
* @param array|string $key Field name, or an array of field/value pairs
* @param string|null $value Field value, if $key is a single field
* @param bool|null $escape Whether to escape values and identifiers
* @param mixed $key Field name, or an array of field/value pairs
* @param mixed $value Field value, if $key is a single field
* @param bool|null $escape Whether to escape values and identifiers
*
* @return $this
*/
public function set($key, ?string $value = '', ?bool $escape = null)
public function set($key, $value = '', ?bool $escape = null)
{
$data = is_array($key) ? $key : [$key => $value];

View File

@ -0,0 +1,49 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Publisher\Exceptions;
use CodeIgniter\Exceptions\FrameworkException;
/**
* Publisher Exception Class
*
* Handles exceptions related to actions taken by a Publisher.
*/
class PublisherException extends FrameworkException
{
/**
* Throws when a file should be overwritten yet cannot.
*
* @param string $from The source file
* @param string $to The destination file
*/
public static function forCollision(string $from, string $to)
{
return new static(lang('Publisher.collision', [filetype($to), $from, $to]));
}
/**
* Throws when given a destination that is not in the list of allowed directories.
*/
public static function forDestinationNotAllowed(string $destination)
{
return new static(lang('Publisher.destinationNotAllowed', [$destination]));
}
/**
* Throws when a file fails to match the allowed pattern for its destination.
*/
public static function forFileNotAllowed(string $file, string $directory, string $pattern)
{
return new static(lang('Publisher.fileNotAllowed', [$file, $directory, $pattern]));
}
}

View File

@ -0,0 +1,438 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Publisher;
use CodeIgniter\Autoloader\FileLocator;
use CodeIgniter\Files\FileCollection;
use CodeIgniter\HTTP\URI;
use CodeIgniter\Publisher\Exceptions\PublisherException;
use RuntimeException;
use Throwable;
/**
* Publishers read in file paths from a variety of sources and copy
* the files out to different destinations. This class acts both as
* a base for individual publication directives as well as the mode
* of discovery for said instances. In this class a "file" is a full
* path to a verified file while a "path" is relative to its source
* or destination and may indicate either a file or directory of
* unconfirmed existence.
*
* Class failures throw the PublisherException, but some underlying
* methods may percolate different exceptions, like FileException,
* FileNotFoundException or InvalidArgumentException.
*
* Write operations will catch all errors in the file-specific
* $errors property to minimize impact of partial batch operations.
*/
class Publisher extends FileCollection
{
/**
* Array of discovered Publishers.
*
* @var array<string, self[]|null>
*/
private static $discovered = [];
/**
* Directory to use for methods that need temporary storage.
* Created on-the-fly as needed.
*
* @var string|null
*/
private $scratch;
/**
* Exceptions for specific files from the last write operation.
*
* @var array<string, Throwable>
*/
private $errors = [];
/**
* List of file published curing the last write operation.
*
* @var string[]
*/
private $published = [];
/**
* List of allowed directories and their allowed files regex.
* Restrictions are intentionally private to prevent overriding.
*
* @var array<string,string>
*/
private $restrictions;
/**
* Base path to use for the source.
*
* @var string
*/
protected $source = ROOTPATH;
/**
* Base path to use for the destination.
*
* @var string
*/
protected $destination = FCPATH;
//--------------------------------------------------------------------
// Support Methods
//--------------------------------------------------------------------
/**
* Discovers and returns all Publishers in the specified namespace directory.
*
* @return self[]
*/
final public static function discover(string $directory = 'Publishers'): array
{
if (isset(self::$discovered[$directory])) {
return self::$discovered[$directory];
}
self::$discovered[$directory] = [];
/** @var FileLocator $locator */
$locator = service('locator');
if ([] === $files = $locator->listFiles($directory)) {
return [];
}
// Loop over each file checking to see if it is a Publisher
foreach (array_unique($files) as $file) {
$className = $locator->findQualifiedNameFromPath($file);
if (is_string($className) && class_exists($className) && is_a($className, self::class, true)) {
self::$discovered[$directory][] = new $className();
}
}
sort(self::$discovered[$directory]);
return self::$discovered[$directory];
}
/**
* Removes a directory and all its files and subdirectories.
*/
private static function wipeDirectory(string $directory): void
{
if (is_dir($directory)) {
// Try a few times in case of lingering locks
$attempts = 10;
while ((bool) $attempts && ! delete_files($directory, true, false, true)) {
// @codeCoverageIgnoreStart
$attempts--;
usleep(100000); // .1s
// @codeCoverageIgnoreEnd
}
@rmdir($directory);
}
}
//--------------------------------------------------------------------
// Class Core
//--------------------------------------------------------------------
/**
* Loads the helper and verifies the source and destination directories.
*/
public function __construct(?string $source = null, ?string $destination = null)
{
helper(['filesystem']);
$this->source = self::resolveDirectory($source ?? $this->source);
$this->destination = self::resolveDirectory($destination ?? $this->destination);
// Restrictions are intentionally not injected to prevent overriding
$this->restrictions = config('Publisher')->restrictions;
// Make sure the destination is allowed
foreach (array_keys($this->restrictions) as $directory) {
if (strpos($this->destination, $directory) === 0) {
return;
}
}
throw PublisherException::forDestinationNotAllowed($this->destination);
}
/**
* Cleans up any temporary files in the scratch space.
*/
public function __destruct()
{
if (isset($this->scratch)) {
self::wipeDirectory($this->scratch);
$this->scratch = null;
}
}
/**
* Reads files from the sources and copies them out to their destinations.
* This method should be reimplemented by child classes intended for
* discovery.
*
* @throws RuntimeException
*/
public function publish(): bool
{
// Safeguard against accidental misuse
if ($this->source === ROOTPATH && $this->destination === FCPATH) {
throw new RuntimeException('Child classes of Publisher should provide their own publish method or a source and destination.');
}
return $this->addPath('/')->merge(true);
}
//--------------------------------------------------------------------
// Property Accessors
//--------------------------------------------------------------------
/**
* Returns the source directory.
*/
final public function getSource(): string
{
return $this->source;
}
/**
* Returns the destination directory.
*/
final public function getDestination(): string
{
return $this->destination;
}
/**
* Returns the temporary workspace, creating it if necessary.
*/
final public function getScratch(): string
{
if ($this->scratch === null) {
$this->scratch = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . bin2hex(random_bytes(6)) . DIRECTORY_SEPARATOR;
mkdir($this->scratch, 0700);
}
return $this->scratch;
}
/**
* Returns errors from the last write operation if any.
*
* @return array<string,Throwable>
*/
final public function getErrors(): array
{
return $this->errors;
}
/**
* Returns the files published by the last write operation.
*
* @return string[]
*/
final public function getPublished(): array
{
return $this->published;
}
//--------------------------------------------------------------------
// Additional Handlers
//--------------------------------------------------------------------
/**
* Verifies and adds paths to the list.
*
* @param string[] $paths
*
* @return $this
*/
final public function addPaths(array $paths, bool $recursive = true)
{
foreach ($paths as $path) {
$this->addPath($path, $recursive);
}
return $this;
}
/**
* Adds a single path to the file list.
*
* @return $this
*/
final public function addPath(string $path, bool $recursive = true)
{
$this->add($this->source . $path, $recursive);
return $this;
}
/**
* Downloads and stages files from an array of URIs.
*
* @param string[] $uris
*
* @return $this
*/
final public function addUris(array $uris)
{
foreach ($uris as $uri) {
$this->addUri($uri);
}
return $this;
}
/**
* Downloads a file from the URI, and adds it to the file list.
*
* @param string $uri Because HTTP\URI is stringable it will still be accepted
*
* @return $this
*/
final public function addUri(string $uri)
{
// Figure out a good filename (using URI strips queries and fragments)
$file = $this->getScratch() . basename((new URI($uri))->getPath());
// Get the content and write it to the scratch space
write_file($file, service('curlrequest')->get($uri)->getBody());
return $this->addFile($file);
}
//--------------------------------------------------------------------
// Write Methods
//--------------------------------------------------------------------
/**
* Removes the destination and all its files and folders.
*
* @return $this
*/
final public function wipe()
{
self::wipeDirectory($this->destination);
return $this;
}
/**
* Copies all files into the destination, does not create directory structure.
*
* @param bool $replace Whether to overwrite existing files.
*
* @return bool Whether all files were copied successfully
*/
final public function copy(bool $replace = true): bool
{
$this->errors = $this->published = [];
foreach ($this->get() as $file) {
$to = $this->destination . basename($file);
try {
$this->safeCopyFile($file, $to, $replace);
$this->published[] = $to;
} catch (Throwable $e) {
$this->errors[$file] = $e;
}
}
return $this->errors === [];
}
/**
* Merges all files into the destination.
* Creates a mirrored directory structure only for files from source.
*
* @param bool $replace Whether to overwrite existing files.
*
* @return bool Whether all files were copied successfully
*/
final public function merge(bool $replace = true): bool
{
$this->errors = $this->published = [];
// Get the files from source for special handling
$sourced = self::filterFiles($this->get(), $this->source);
// Handle everything else with a flat copy
$this->files = array_diff($this->files, $sourced);
$this->copy($replace);
// Copy each sourced file to its relative destination
foreach ($sourced as $file) {
// Resolve the destination path
$to = $this->destination . substr($file, strlen($this->source));
try {
$this->safeCopyFile($file, $to, $replace);
$this->published[] = $to;
} catch (Throwable $e) {
$this->errors[$file] = $e;
}
}
return $this->errors === [];
}
/**
* Copies a file with directory creation and identical file awareness.
* Intentionally allows errors.
*
* @throws PublisherException For collisions and restriction violations
*/
private function safeCopyFile(string $from, string $to, bool $replace): void
{
// Verify this is an allowed file for its destination
foreach ($this->restrictions as $directory => $pattern) {
if (strpos($to, $directory) === 0 && self::matchFiles([$to], $pattern) === []) {
throw PublisherException::forFileNotAllowed($from, $directory, $pattern);
}
}
// Check for an existing file
if (file_exists($to)) {
// If not replacing or if files are identical then consider successful
if (! $replace || same_file($from, $to)) {
return;
}
// If it is a directory then do not try to remove it
if (is_dir($to)) {
throw PublisherException::forCollision($from, $to);
}
// Try to remove anything else
unlink($to);
}
// Make sure the directory exists
if (! is_dir($directory = pathinfo($to, PATHINFO_DIRNAME))) {
mkdir($directory, 0775, true);
}
// Allow copy() to throw errors
copy($from, $to);
}
}

View File

@ -172,11 +172,10 @@ class DatabaseHandler extends BaseHandler
$insertData = [
'id' => $id,
'ip_address' => $this->ipAddress,
'timestamp' => 'now()',
'data' => $this->platform === 'postgre' ? '\x' . bin2hex($data) : $data,
];
if (! $this->db->table($this->table)->insert($insertData)) {
if (! $this->db->table($this->table)->set('timestamp', 'now()', false)->insert($insertData)) {
return $this->fail();
}
@ -192,13 +191,13 @@ class DatabaseHandler extends BaseHandler
$builder = $builder->where('ip_address', $this->ipAddress);
}
$updateData = ['timestamp' => 'now()'];
$updateData = [];
if ($this->fingerprint !== md5($data)) {
$updateData['data'] = ($this->platform === 'postgre') ? '\x' . bin2hex($data) : $data;
}
if (! $builder->update($updateData)) {
if (! $builder->set('timestamp', 'now()', false)->update($updateData)) {
return $this->fail();
}
@ -257,7 +256,7 @@ class DatabaseHandler extends BaseHandler
$separator = $this->platform === 'postgre' ? '\'' : ' ';
$interval = implode($separator, ['', "{$max_lifetime} second", '']);
return $this->db->table($this->table)->delete("timestamp < now() - INTERVAL {$interval}") ? 1 : $this->fail();
return $this->db->table($this->table)->where('timestamp <', "now() - INTERVAL {$interval}", false)->delete() ? 1 : $this->fail();
}
/**

View File

@ -14,6 +14,7 @@ namespace CodeIgniter\Test\Mock;
use Closure;
use CodeIgniter\Cache\CacheInterface;
use CodeIgniter\Cache\Handlers\BaseHandler;
use PHPUnit\Framework\Assert;
class MockCache extends BaseHandler implements CacheInterface
{
@ -31,6 +32,13 @@ class MockCache extends BaseHandler implements CacheInterface
*/
protected $expirations = [];
/**
* If true, will not cache any data.
*
* @var bool
*/
protected $bypass = false;
/**
* Takes care of any handler-specific setup that must be done.
*/
@ -49,16 +57,12 @@ class MockCache extends BaseHandler implements CacheInterface
{
$key = static::validateKey($key, $this->prefix);
return $this->cache[$key] ?? null;
return array_key_exists($key, $this->cache) ? $this->cache[$key] : null;
}
/**
* Get an item from the cache, or execute the given Closure and store the result.
*
* @param string $key Cache item name
* @param int $ttl Time to live
* @param Closure $callback Callback return value
*
* @return mixed
*/
public function remember(string $key, int $ttl, Closure $callback)
@ -89,6 +93,10 @@ class MockCache extends BaseHandler implements CacheInterface
*/
public function save(string $key, $value, int $ttl = 60, bool $raw = false)
{
if ($this->bypass) {
return false;
}
$key = static::validateKey($key, $this->prefix);
$this->cache[$key] = $value;
@ -100,8 +108,6 @@ class MockCache extends BaseHandler implements CacheInterface
/**
* Deletes a specific item from the cache store.
*
* @param string $key Cache item name
*
* @return bool
*/
public function delete(string $key)
@ -120,8 +126,6 @@ class MockCache extends BaseHandler implements CacheInterface
/**
* Deletes items from the cache store matching a given pattern.
*
* @param string $pattern Cache items glob-style pattern
*
* @return int
*/
public function deleteMatching(string $pattern)
@ -141,9 +145,6 @@ class MockCache extends BaseHandler implements CacheInterface
/**
* Performs atomic incrementation of a raw stored value.
*
* @param string $key Cache ID
* @param int $offset Step/value to increase by
*
* @return bool
*/
public function increment(string $key, int $offset = 1)
@ -163,9 +164,6 @@ class MockCache extends BaseHandler implements CacheInterface
/**
* Performs atomic decrementation of a raw stored value.
*
* @param string $key Cache ID
* @param int $offset Step/value to increase by
*
* @return bool
*/
public function decrement(string $key, int $offset = 1)
@ -212,10 +210,7 @@ class MockCache extends BaseHandler implements CacheInterface
/**
* Returns detailed information about the specific item in the cache.
*
* @param string $key Cache item name.
*
* @return array|null
* Returns null if the item does not exist, otherwise array<string, mixed>
* @return array|null Returns null if the item does not exist, otherwise array<string, mixed>
* with at least the 'expire' key for absolute epoch expiry (or null).
*/
public function getMetaData(string $key)
@ -230,16 +225,73 @@ class MockCache extends BaseHandler implements CacheInterface
return null;
}
return [
'expire' => $this->expirations[$key],
];
return ['expire' => $this->expirations[$key]];
}
/**
* Determines if the driver is supported on this system.
* Determine if the driver is supported on this system.
*/
public function isSupported(): bool
{
return true;
}
//--------------------------------------------------------------------
// Test Helpers
//--------------------------------------------------------------------
/**
* Instructs the class to ignore all
* requests to cache an item, and always "miss"
* when checked for existing data.
*
* @return $this
*/
public function bypass(bool $bypass = true)
{
$this->clean();
$this->bypass = $bypass;
return $this;
}
//--------------------------------------------------------------------
// Additional Assertions
//--------------------------------------------------------------------
/**
* Asserts that the cache has an item named $key.
* The value is not checked since storing false or null
* values is valid.
*/
public function assertHas(string $key)
{
Assert::assertNotNull($this->get($key), "The cache does not have an item named: `{$key}`");
}
/**
* Asserts that the cache has an item named $key with a value matching $value.
*
* @param mixed $value
*/
public function assertHasValue(string $key, $value = null)
{
$item = $this->get($key);
// Let assertHas handle throwing the error for consistency
// if the key is not found
if (empty($item)) {
$this->assertHas($key);
}
Assert::assertSame($value, $this->get($key), "The cached item `{$key}` does not equal match expectation. Found: " . print_r($value, true));
}
/**
* Asserts that the cache does NOT have an item named $key.
*/
public function assertMissing(string $key)
{
Assert::assertArrayNotHasKey($key, $this->cache, "The cached item named `{$key}` exists.");
}
}

View File

@ -672,21 +672,42 @@ class Validation implements ValidationInterface
*/
protected function splitRules(string $rules): array
{
$nonEscapeBracket = '((?<!\\\\)(?:\\\\\\\\)*[\[\]])';
$pipeNotInBracket = sprintf(
'/\|(?=(?:[^\[\]]*%s[^\[\]]*%s)*(?![^\[\]]*%s))/',
$nonEscapeBracket,
$nonEscapeBracket,
$nonEscapeBracket
);
if (strpos($rules, '|') === false) {
return [$rules];
}
$_rules = preg_split($pipeNotInBracket, $rules);
$string = $rules;
$rules = [];
$length = strlen($string);
$cursor = 0;
return array_unique($_rules);
while ($cursor < $length) {
$pos = strpos($string, '|', $cursor);
if ($pos === false) {
// we're in the last rule
$pos = $length;
}
$rule = substr($string, $cursor, $pos - $cursor);
while (
(substr_count($rule, '[') - substr_count($rule, '\['))
!== (substr_count($rule, ']') - substr_count($rule, '\]'))
) {
// the pipe is inside the brackets causing the closing bracket to
// not be included. so, we adjust the rule to include that portion.
$pos = strpos($string, '|', $cursor + strlen($rule) + 1) ?: $length;
$rule = substr($string, $cursor, $pos - $cursor);
}
$rules[] = $rule;
$cursor += strlen($rule) + 1; // +1 to exclude the pipe
}
return array_unique($rules);
}
// Misc
/**
* Resets the class to a blank slate. Should be called whenever
* you need to process more than one array.

View File

@ -121,4 +121,18 @@ class Registrar
return $config;
}
/**
* Demonstrates Publisher security.
*
* @see PublisherRestrictionsTest::testRegistrarsNotAllowed()
*
* @return array
*/
public static function Publisher()
{
return [
'restrictions' => [SUPPORTPATH => '*'],
];
}
}

View File

@ -70,6 +70,7 @@ class Migration_Create_test_tables extends Migration
'type_double' => ['type' => 'DOUBLE', 'null' => true],
'type_decimal' => ['type' => 'DECIMAL', 'constraint' => '18,4', 'null' => true],
'type_blob' => ['type' => 'BLOB', 'null' => true],
'type_boolean' => ['type' => 'BOOLEAN', 'null' => true],
];
if ($this->db->DBDriver === 'Postgre') {
@ -127,6 +128,29 @@ class Migration_Create_test_tables extends Migration
'ip' => ['type' => 'VARCHAR', 'constraint' => 100],
'ip2' => ['type' => 'VARCHAR', 'constraint' => 100],
])->createTable('ip_table', true);
// Database session table
if ($this->db->DBDriver === 'MySQLi') {
$this->forge->addField([
'id' => ['type' => 'VARCHAR', 'constraint' => 128, 'null' => false],
'ip_address' => ['type' => 'VARCHAR', 'constraint' => 45, 'null' => false],
'timestamp timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL',
'data' => ['type' => 'BLOB', 'null' => false],
]);
$this->forge->addKey('id', true);
$this->forge->createTable('ci_sessions', true);
}
if ($this->db->DBDriver === 'Postgre') {
$this->forge->addField([
'id' => ['type' => 'VARCHAR', 'constraint' => 128, 'null' => false],
'ip_address inet NOT NULL',
'timestamp timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL',
"data bytea DEFAULT '' NOT NULL",
]);
$this->forge->addKey('id', true);
$this->forge->createTable('ci_sessions', true);
}
}
public function down()
@ -140,5 +164,9 @@ class Migration_Create_test_tables extends Migration
$this->forge->dropTable('stringifypkey', true);
$this->forge->dropTable('without_auto_increment', true);
$this->forge->dropTable('ip_table', true);
if (in_array($this->db->DBDriver, ['MySQLi', 'Postgre'], true)) {
$this->forge->dropTable('ci_sessions', true);
}
}
}

View File

@ -106,6 +106,7 @@ class CITestSeeder extends Seeder
'type_datetime' => '2020-06-18T05:12:24.000+02:00',
'type_timestamp' => '2019-07-18T21:53:21.000+02:00',
'type_bigint' => 2342342,
'type_boolean' => 1,
],
],
];
@ -119,7 +120,8 @@ class CITestSeeder extends Seeder
}
if ($this->db->DBDriver === 'Postgre') {
$data['type_test'][0]['type_time'] = '15:22:00';
$data['type_test'][0]['type_time'] = '15:22:00';
$data['type_test'][0]['type_boolean'] = true;
unset(
$data['type_test'][0]['type_enum'],
$data['type_test'][0]['type_set'],
@ -146,6 +148,24 @@ class CITestSeeder extends Seeder
);
}
if ($this->db->DBDriver === 'MySQLi') {
$data['ci_sessions'][] = [
'id' => '1f5o06b43phsnnf8if6bo33b635e4p2o',
'ip_address' => '127.0.0.1',
'timestamp' => '2021-06-25 21:54:14',
'data' => '__ci_last_regenerate|i:1624650854;_ci_previous_url|s:40:\"http://localhost/index.php/home/index\";',
];
}
if ($this->db->DBDriver === 'Postgre') {
$data['ci_sessions'][] = [
'id' => '1f5o06b43phsnnf8if6bo33b635e4p2o',
'ip_address' => '127.0.0.1',
'timestamp' => '2021-06-25 21:54:14.991403+02',
'data' => '\x' . bin2hex('__ci_last_regenerate|i:1624650854;_ci_previous_url|s:40:\"http://localhost/index.php/home/index\";'),
];
}
foreach ($data as $table => $dummy_data) {
$this->db->table($table)->truncate();

View File

@ -0,0 +1,56 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Tests\Support\Publishers;
use CodeIgniter\Publisher\Publisher;
final class TestPublisher extends Publisher
{
/**
* Fakes an error on the given file.
*/
public static function setResult(bool $result)
{
self::$result = $result;
}
/**
* Return value for publish()
*
* @var bool
*/
private static $result = true;
/**
* Base path to use for the source.
*
* @var string
*/
protected $source = SUPPORTPATH . 'Files';
/**
* Base path to use for the destination.
*
* @var string
*/
protected $destination = WRITEPATH;
/**
* Fakes a publish event so no files are actually copied.
*/
public function publish(): bool
{
$this->addPath('');
return self::$result;
}
}

View File

@ -0,0 +1,46 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Cache;
use CodeIgniter\Cache\Handlers\BaseHandler;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\Mock\MockCache;
/**
* @internal
*/
final class CacheMockTest extends CIUnitTestCase
{
public function testMockReturnsMockCacheClass()
{
$this->assertInstanceOf(BaseHandler::class, service('cache'));
$mock = mock(CacheFactory::class);
$this->assertInstanceOf(MockCache::class, $mock);
$this->assertInstanceOf(MockCache::class, service('cache'));
}
public function testMockCaching()
{
$mock = mock(CacheFactory::class);
// Ensure it stores the value normally
$mock->save('foo', 'bar');
$mock->assertHas('foo');
$mock->assertHasValue('foo', 'bar');
// Try it again with bypass on
$mock->bypass();
$mock->save('foo', 'bar');
$mock->assertMissing('foo');
}
}

View File

@ -44,6 +44,16 @@ final class BaseHandlerTest extends CIUnitTestCase
];
}
public function testValidateKeyUsesConfig()
{
config('Cache')->reservedCharacters = 'b';
$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage('Cache key contains reserved characters b');
BaseHandler::validateKey('banana');
}
public function testValidateKeySuccess()
{
$string = 'banana';

View File

@ -156,6 +156,18 @@ final class FileHandlerTest extends CIUnitTestCase
unlink($file);
}
public function testSavePermanent()
{
$this->assertTrue($this->fileHandler->save(self::$key1, 'value', 0));
$metaData = $this->fileHandler->getMetaData(self::$key1);
$this->assertNull($metaData['expire']);
$this->assertLessThanOrEqual(1, $metaData['mtime'] - time());
$this->assertSame('value', $metaData['data']);
$this->assertTrue($this->fileHandler->delete(self::$key1));
}
public function testDelete()
{
$this->fileHandler->save(self::$key1, 'value');

View File

@ -102,6 +102,18 @@ final class MemcachedHandlerTest extends CIUnitTestCase
$this->assertTrue($this->memcachedHandler->save(self::$key1, 'value'));
}
public function testSavePermanent()
{
$this->assertTrue($this->memcachedHandler->save(self::$key1, 'value', 0));
$metaData = $this->memcachedHandler->getMetaData(self::$key1);
$this->assertNull($metaData['expire']);
$this->assertLessThanOrEqual(1, $metaData['mtime'] - time());
$this->assertSame('value', $metaData['data']);
$this->assertTrue($this->memcachedHandler->delete(self::$key1));
}
public function testDelete()
{
$this->memcachedHandler->save(self::$key1, 'value');

View File

@ -109,6 +109,18 @@ final class PredisHandlerTest extends CIUnitTestCase
$this->assertTrue($this->PredisHandler->save(self::$key1, 'value'));
}
public function testSavePermanent()
{
$this->assertTrue($this->PredisHandler->save(self::$key1, 'value', 0));
$metaData = $this->PredisHandler->getMetaData(self::$key1);
$this->assertNull($metaData['expire']);
$this->assertLessThanOrEqual(1, $metaData['mtime'] - time());
$this->assertSame('value', $metaData['data']);
$this->assertTrue($this->PredisHandler->delete(self::$key1));
}
public function testDelete()
{
$this->PredisHandler->save(self::$key1, 'value');

View File

@ -109,6 +109,18 @@ final class RedisHandlerTest extends CIUnitTestCase
$this->assertTrue($this->redisHandler->save(self::$key1, 'value'));
}
public function testSavePermanent()
{
$this->assertTrue($this->redisHandler->save(self::$key1, 'value', 0));
$metaData = $this->redisHandler->getMetaData(self::$key1);
$this->assertNull($metaData['expire']);
$this->assertLessThanOrEqual(1, $metaData['mtime'] - time());
$this->assertSame('value', $metaData['data']);
$this->assertTrue($this->redisHandler->delete(self::$key1));
}
public function testDelete()
{
$this->redisHandler->save(self::$key1, 'value');

View File

@ -132,4 +132,24 @@ final class CommandGeneratorTest extends CIUnitTestCase
$this->assertStringContainsString('File created: ', CITestStreamFilter::$buffer);
$this->assertFileExists(APPPATH . 'Controllers/TestModuleController.php');
}
/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/4857
*/
public function testGeneratorIsNotConfusedWithNamespaceLikeClassNames(): void
{
$time = time();
$notExists = true;
command('make:migration App_Lesson');
// we got 5 chances to prove that the file created went to app/Database/Migrations
foreach (range(0, 4) as $increment) {
$expectedFile = sprintf('%sDatabase/Migrations/%s_AppLesson.php', APPPATH, gmdate('Y-m-d-His', $time + $increment));
clearstatcache(true, $expectedFile);
$notExists = $notExists && ! is_file($expectedFile);
}
$this->assertFalse($notExists, 'Creating migration file for class "AppLesson" did not go to "app/Database/Migrations"');
}
}

View File

@ -0,0 +1,64 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Commands;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\Filters\CITestStreamFilter;
use Tests\Support\Publishers\TestPublisher;
/**
* @internal
*/
final class PublishCommandTest extends CIUnitTestCase
{
private $streamFilter;
protected function setUp(): void
{
parent::setUp();
CITestStreamFilter::$buffer = '';
$this->streamFilter = stream_filter_append(STDOUT, 'CITestStreamFilter');
$this->streamFilter = stream_filter_append(STDERR, 'CITestStreamFilter');
}
protected function tearDown(): void
{
parent::tearDown();
stream_filter_remove($this->streamFilter);
TestPublisher::setResult(true);
}
public function testDefault()
{
command('publish');
$this->assertStringContainsString(lang('Publisher.publishSuccess', [
TestPublisher::class,
0,
WRITEPATH,
]), CITestStreamFilter::$buffer);
}
public function testFailure()
{
TestPublisher::setResult(false);
command('publish');
$this->assertStringContainsString(lang('Publisher.publishFailure', [
TestPublisher::class,
WRITEPATH,
]), CITestStreamFilter::$buffer);
}
}

View File

@ -99,6 +99,80 @@ final class UpdateTest extends CIUnitTestCase
$this->assertSame($expectedBinds, $builder->getBinds());
}
public function testUpdateWithSetAsInt()
{
$builder = new BaseBuilder('jobs', $this->db);
$builder->testMode()->set('age', 22)->where('id', 1)->update(null, null, null);
$expectedSQL = 'UPDATE "jobs" SET "age" = 22 WHERE "id" = 1';
$expectedBinds = [
'age' => [
22,
true,
],
'id' => [
1,
true,
],
];
$this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledUpdate()));
$this->assertSame($expectedBinds, $builder->getBinds());
}
public function testUpdateWithSetAsBoolean()
{
$builder = new BaseBuilder('jobs', $this->db);
$builder->testMode()->set('manager', true)->where('id', 1)->update(null, null, null);
$expectedSQL = 'UPDATE "jobs" SET "manager" = 1 WHERE "id" = 1';
$expectedBinds = [
'manager' => [
true,
true,
],
'id' => [
1,
true,
],
];
$this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledUpdate()));
$this->assertSame($expectedBinds, $builder->getBinds());
}
public function testUpdateWithSetAsArray()
{
$builder = new BaseBuilder('jobs', $this->db);
$builder->testMode()->set(['name' => 'Programmer', 'age' => 22, 'manager' => true])->where('id', 1)->update(null, null, null);
$expectedSQL = 'UPDATE "jobs" SET "name" = \'Programmer\', "age" = 22, "manager" = 1 WHERE "id" = 1';
$expectedBinds = [
'name' => [
'Programmer',
true,
],
'age' => [
22,
true,
],
'manager' => [
true,
true,
],
'id' => [
1,
true,
],
];
$this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledUpdate()));
$this->assertSame($expectedBinds, $builder->getBinds());
}
public function testUpdateThrowsExceptionWithNoData()
{
$builder = new BaseBuilder('jobs', $this->db);

View File

@ -372,6 +372,8 @@ final class ForgeTest extends CIUnitTestCase
public function testForeignKey()
{
$this->forge->dropTable('forge_test_users', true);
$attributes = [];
if ($this->db->DBDriver === 'MySQLi') {
@ -427,6 +429,172 @@ final class ForgeTest extends CIUnitTestCase
$this->forge->dropTable('forge_test_users', true);
}
/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/4986
*/
public function testForeignKeyAddingWithStringFields()
{
if ($this->db->DBDriver !== 'MySQLi') {
$this->markTestSkipped('Testing only on MySQLi but fix expands to all DBs.');
}
$attributes = ['ENGINE' => 'InnoDB'];
$this->forge->addField([
'`id` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY',
'`name` VARCHAR(255) NOT NULL',
])->createTable('forge_test_users', true, $attributes);
$this->forge
->addField([
'`id` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY',
'`users_id` INT(11) NOT NULL',
'`name` VARCHAR(255) NOT NULL',
])
->addForeignKey('users_id', 'forge_test_users', 'id', 'CASCADE', 'CASCADE')
->createTable('forge_test_invoices', true, $attributes);
$foreignKeyData = $this->db->getForeignKeyData('forge_test_invoices')[0];
$this->assertSame($this->db->DBPrefix . 'forge_test_invoices_users_id_foreign', $foreignKeyData->constraint_name);
$this->assertSame('users_id', $foreignKeyData->column_name);
$this->assertSame('id', $foreignKeyData->foreign_column_name);
$this->assertSame($this->db->DBPrefix . 'forge_test_invoices', $foreignKeyData->table_name);
$this->assertSame($this->db->DBPrefix . 'forge_test_users', $foreignKeyData->foreign_table_name);
$this->forge->dropTable('forge_test_invoices', true);
$this->forge->dropTable('forge_test_users', true);
}
/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/4310
*/
public function testCompositeForeignKey()
{
$attributes = [];
if ($this->db->DBDriver === 'MySQLi') {
$attributes = ['ENGINE' => 'InnoDB'];
}
$this->forge->addField([
'id' => [
'type' => 'INTEGER',
'constraint' => 11,
],
'second_id' => [
'type' => 'VARCHAR',
'constraint' => 50,
],
'name' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
]);
$this->forge->addPrimaryKey(['id', 'second_id']);
$this->forge->createTable('forge_test_users', true, $attributes);
$this->forge->addField([
'id' => [
'type' => 'INTEGER',
'constraint' => 11,
],
'users_id' => [
'type' => 'INTEGER',
'constraint' => 11,
],
'users_second_id' => [
'type' => 'VARCHAR',
'constraint' => 50,
],
'name' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->addForeignKey(['users_id', 'users_second_id'], 'forge_test_users', ['id', 'second_id'], 'CASCADE', 'CASCADE');
$this->forge->createTable('forge_test_invoices', true, $attributes);
$foreignKeyData = $this->db->getForeignKeyData('forge_test_invoices');
if ($this->db->DBDriver === 'SQLite3') {
$this->assertSame('users_id to db_forge_test_users.id', $foreignKeyData[0]->constraint_name);
$this->assertSame(0, $foreignKeyData[0]->sequence);
$this->assertSame('users_second_id to db_forge_test_users.second_id', $foreignKeyData[1]->constraint_name);
$this->assertSame(1, $foreignKeyData[1]->sequence);
} else {
$haystack = ['users_id', 'users_second_id'];
$this->assertSame($this->db->DBPrefix . 'forge_test_invoices_users_id_users_second_id_foreign', $foreignKeyData[0]->constraint_name);
$this->assertContains($foreignKeyData[0]->column_name, $haystack);
$secondIdKey = $this->db->DBDriver === 'Postgre' ? 2 : 1;
$this->assertSame($this->db->DBPrefix . 'forge_test_invoices_users_id_users_second_id_foreign', $foreignKeyData[$secondIdKey]->constraint_name);
$this->assertContains($foreignKeyData[$secondIdKey]->column_name, $haystack);
}
$this->assertSame($this->db->DBPrefix . 'forge_test_invoices', $foreignKeyData[0]->table_name);
$this->assertSame($this->db->DBPrefix . 'forge_test_users', $foreignKeyData[0]->foreign_table_name);
$this->forge->dropTable('forge_test_invoices', true);
$this->forge->dropTable('forge_test_users', true);
}
/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/4310
*/
public function testCompositeForeignKeyFieldNotExistException()
{
$this->expectException(DatabaseException::class);
$this->expectExceptionMessage('Field `user_id, user_second_id` not found.');
$attributes = [];
if ($this->db->DBDriver === 'MySQLi') {
$attributes = ['ENGINE' => 'InnoDB'];
}
$this->forge->addField([
'id' => [
'type' => 'INTEGER',
'constraint' => 11,
],
'second_id' => [
'type' => 'VARCHAR',
'constraint' => 50,
],
'name' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
]);
$this->forge->addPrimaryKey(['id', 'second_id']);
$this->forge->createTable('forge_test_users', true, $attributes);
$this->forge->addField([
'id' => [
'type' => 'INTEGER',
'constraint' => 11,
],
'users_id' => [
'type' => 'INTEGER',
'constraint' => 11,
],
'users_second_id' => [
'type' => 'VARCHAR',
'constraint' => 50,
],
'name' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
]);
$this->forge->addKey('id', true);
$this->forge->addForeignKey(['user_id', 'user_second_id'], 'forge_test_users', ['id', 'second_id'], 'CASCADE', 'CASCADE');
$this->forge->createTable('forge_test_invoices', true, $attributes);
}
public function testForeignKeyFieldNotExistException()
{
$this->expectException(DatabaseException::class);
@ -473,6 +641,8 @@ final class ForgeTest extends CIUnitTestCase
public function testDropForeignKey()
{
$this->forge->dropTable('forge_test_users', true);
$attributes = [];
if ($this->db->DBDriver === 'MySQLi') {

View File

@ -52,6 +52,10 @@ final class MetadataTest extends CIUnitTestCase
$prefix . 'without_auto_increment',
$prefix . 'ip_table',
];
if (in_array($this->db->DBDriver, ['MySQLi', 'Postgre'], true)) {
$this->expectedTables[] = $prefix . 'ci_sessions';
}
}
public function testListTables()

View File

@ -50,7 +50,7 @@ final class PreparedQueryTest extends CIUnitTestCase
if ($this->db->DBDriver === 'SQLSRV') {
$database = $this->db->getDatabase();
$expected = "INSERT INTO {$ec}{$database}{$ec}.{$ec}dbo{$ec}.{$ec}{$pre}user{$ec} ({$ec}name{$ec},{$ec}email{$ec}) VALUES ({$placeholders})";
$expected = "INSERT INTO {$ec}{$database}{$ec}.{$ec}{$this->db->schema}{$ec}.{$ec}{$pre}user{$ec} ({$ec}name{$ec},{$ec}email{$ec}) VALUES ({$placeholders})";
} else {
$expected = "INSERT INTO {$ec}{$pre}user{$ec} ({$ec}name{$ec}, {$ec}email{$ec}) VALUES ({$placeholders})";
}
@ -107,6 +107,10 @@ final class PreparedQueryTest extends CIUnitTestCase
$query = $this->db->prepare(static function ($db) {
$sql = "INSERT INTO {$db->DBPrefix}user (name, email, country) VALUES (?, ?, ?)";
if ($db->DBDriver === 'SQLSRV') {
$sql = "INSERT INTO {$db->schema}.{$db->DBPrefix}user (name, email, country) VALUES (?, ?, ?)";
}
return (new Query($db))->setQuery($sql);
});

View File

@ -211,4 +211,23 @@ final class UpdateTest extends CIUnitTestCase
'description' => 'Developer',
]);
}
public function testSetWithBoolean()
{
$this->db->table('type_test')
->set('type_boolean', false)
->update();
$this->seeInDatabase('type_test', [
'type_boolean' => false,
]);
$this->db->table('type_test')
->set('type_boolean', true)
->update();
$this->seeInDatabase('type_test', [
'type_boolean' => true,
]);
}
}

View File

@ -93,7 +93,7 @@ final class MigrationRunnerTest extends CIUnitTestCase
];
if ($this->db->DBDriver === 'SQLSRV') {
$this->db->simpleQuery('SET IDENTITY_INSERT ' . $this->db->prefixTable('migrations') . ' ON');
$this->db->simpleQuery('SET IDENTITY_INSERT ' . $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->prefixTable('migrations') . ' ON');
}
$this->hasInDatabase('migrations', $expected);
@ -110,8 +110,7 @@ final class MigrationRunnerTest extends CIUnitTestCase
$this->assertSame($expected, $history);
if ($this->db->DBDriver === 'SQLSRV') {
$this->db->simpleQuery('SET IDENTITY_INSERT ' . $this->db->prefixTable('migrations') . ' OFF');
$this->db->simpleQuery('SET IDENTITY_INSERT ' . $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->prefixTable('migrations') . ' OFF');
$db = $this->getPrivateProperty($runner, 'db');
$db->table('migrations')->delete(['id' => 4]);
}

View File

@ -0,0 +1,556 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
use CodeIgniter\Files\Exceptions\FileException;
use CodeIgniter\Files\File;
use CodeIgniter\Files\FileCollection;
use CodeIgniter\Test\CIUnitTestCase;
/**
* @internal
*/
final class FileCollectionTest extends CIUnitTestCase
{
/**
* A known, valid file
*
* @var string
*/
private $file = SUPPORTPATH . 'Files/baker/banana.php';
/**
* A known, valid directory
*
* @var string
*/
private $directory = SUPPORTPATH . 'Files/able/';
/**
* Initialize the helper, since some
* tests call static methods before
* the constructor would load it.
*/
public static function setUpBeforeClass(): void
{
parent::setUpBeforeClass();
helper(['filesystem']);
}
public function testResolveDirectoryDirectory()
{
$method = $this->getPrivateMethodInvoker(FileCollection::class, 'resolveDirectory');
$this->assertSame($this->directory, $method($this->directory));
}
public function testResolveDirectoryFile()
{
$method = $this->getPrivateMethodInvoker(FileCollection::class, 'resolveDirectory');
$this->expectException(FileException::class);
$this->expectExceptionMessage(lang('Files.expectedDirectory', ['invokeArgs']));
$method($this->file);
}
public function testResolveDirectorySymlink()
{
// Create a symlink to test
$link = sys_get_temp_dir() . DIRECTORY_SEPARATOR . bin2hex(random_bytes(4));
symlink($this->directory, $link);
$method = $this->getPrivateMethodInvoker(FileCollection::class, 'resolveDirectory');
$this->assertSame($this->directory, $method($link));
unlink($link);
}
public function testResolveFileFile()
{
$method = $this->getPrivateMethodInvoker(FileCollection::class, 'resolveFile');
$this->assertSame($this->file, $method($this->file));
}
public function testResolveFileSymlink()
{
// Create a symlink to test
$link = sys_get_temp_dir() . DIRECTORY_SEPARATOR . bin2hex(random_bytes(4));
symlink($this->file, $link);
$method = $this->getPrivateMethodInvoker(FileCollection::class, 'resolveFile');
$this->assertSame($this->file, $method($link));
unlink($link);
}
public function testResolveFileDirectory()
{
$method = $this->getPrivateMethodInvoker(FileCollection::class, 'resolveFile');
$this->expectException(FileException::class);
$this->expectExceptionMessage(lang('Files.expectedFile', ['invokeArgs']));
$method($this->directory);
}
public function testConstructorAddsFiles()
{
$expected = [
$this->directory . 'apple.php',
$this->file,
];
$collection = new class ([$this->file]) extends FileCollection {
protected $files = [
SUPPORTPATH . 'Files/able/apple.php',
];
};
$this->assertSame($expected, $collection->get());
}
public function testConstructorCallsDefine()
{
$collection = new class () extends FileCollection {
protected function define(): void
{
$this->add(SUPPORTPATH . 'Files/baker/banana.php');
}
};
$this->assertSame([$this->file], $collection->get());
}
public function testAddStringFile()
{
$files = new FileCollection();
$files->add(SUPPORTPATH . 'Files/baker/banana.php');
$this->assertSame([$this->file], $files->get());
}
public function testAddStringFileRecursiveDoesNothing()
{
$files = new FileCollection();
$files->add(SUPPORTPATH . 'Files/baker/banana.php', true);
$this->assertSame([$this->file], $files->get());
}
public function testAddStringDirectory()
{
$files = new FileCollection();
$expected = [
$this->directory . 'apple.php',
$this->directory . 'fig_3.php',
$this->directory . 'prune_ripe.php',
];
$files->add(SUPPORTPATH . 'Files/able');
$this->assertSame($expected, $files->get());
}
public function testAddStringDirectoryRecursive()
{
$files = new FileCollection();
$expected = [
$this->directory . 'apple.php',
$this->directory . 'fig_3.php',
$this->directory . 'prune_ripe.php',
SUPPORTPATH . 'Files/baker/banana.php',
];
$files->add(SUPPORTPATH . 'Files');
$this->assertSame($expected, $files->get());
}
public function testAddArray()
{
$files = new FileCollection();
$expected = [
$this->directory . 'apple.php',
$this->directory . 'fig_3.php',
$this->directory . 'prune_ripe.php',
SUPPORTPATH . 'Files/baker/banana.php',
];
$files->add([
SUPPORTPATH . 'Files/able',
SUPPORTPATH . 'Files/baker/banana.php',
]);
$this->assertSame($expected, $files->get());
}
public function testAddArrayRecursive()
{
$files = new FileCollection();
$expected = [
$this->directory . 'apple.php',
$this->directory . 'fig_3.php',
$this->directory . 'prune_ripe.php',
SUPPORTPATH . 'Files/baker/banana.php',
SUPPORTPATH . 'Log/Handlers/TestHandler.php',
];
$files->add([
SUPPORTPATH . 'Files',
SUPPORTPATH . 'Log',
], true);
$this->assertSame($expected, $files->get());
}
public function testAddFile()
{
$collection = new FileCollection();
$this->assertSame([], $this->getPrivateProperty($collection, 'files'));
$collection->addFile($this->file);
$this->assertSame([$this->file], $this->getPrivateProperty($collection, 'files'));
}
public function testAddFileMissing()
{
$collection = new FileCollection();
$this->expectException(FileException::class);
$this->expectExceptionMessage(lang('Files.expectedFile', ['addFile']));
$collection->addFile('TheHillsAreAlive.bmp');
}
public function testAddFileDirectory()
{
$collection = new FileCollection();
$this->expectException(FileException::class);
$this->expectExceptionMessage(lang('Files.expectedFile', ['addFile']));
$collection->addFile($this->directory);
}
public function testAddFiles()
{
$collection = new FileCollection();
$files = [
$this->file,
$this->file,
];
$collection->addFiles($files);
$this->assertSame($files, $this->getPrivateProperty($collection, 'files'));
}
public function testGet()
{
$collection = new FileCollection();
$collection->addFile($this->file);
$this->assertSame([$this->file], $collection->get());
}
public function testGetSorts()
{
$collection = new FileCollection();
$files = [
$this->file,
$this->directory . 'apple.php',
];
$collection->addFiles($files);
$this->assertSame(array_reverse($files), $collection->get());
}
public function testGetUniques()
{
$collection = new FileCollection();
$files = [
$this->file,
$this->file,
];
$collection->addFiles($files);
$this->assertSame([$this->file], $collection->get());
}
public function testSet()
{
$collection = new FileCollection();
$collection->set([$this->file]);
$this->assertSame([$this->file], $collection->get());
}
public function testSetInvalid()
{
$collection = new FileCollection();
$this->expectException(FileException::class);
$this->expectExceptionMessage(lang('Files.expectedFile', ['addFile']));
$collection->set(['flerb']);
}
public function testRemoveFile()
{
$collection = new FileCollection();
$files = [
$this->file,
$this->directory . 'apple.php',
];
$collection->addFiles($files);
$collection->removeFile($this->file);
$this->assertSame([$this->directory . 'apple.php'], $collection->get());
}
public function testRemoveFiles()
{
$collection = new FileCollection();
$files = [
$this->file,
$this->directory . 'apple.php',
];
$collection->addFiles($files);
$collection->removeFiles($files);
$this->assertSame([], $collection->get());
}
public function testAddDirectoryInvalid()
{
$collection = new FileCollection();
$this->expectException(FileException::class);
$this->expectExceptionMessage(lang('Files.expectedDirectory', ['addDirectory']));
$collection->addDirectory($this->file);
}
public function testAddDirectory()
{
$collection = new FileCollection();
$expected = [
$this->directory . 'apple.php',
$this->directory . 'fig_3.php',
$this->directory . 'prune_ripe.php',
];
$collection->addDirectory($this->directory);
$this->assertSame($expected, $collection->get());
}
public function testAddDirectoryRecursive()
{
$collection = new FileCollection();
$expected = [
$this->directory . 'apple.php',
$this->directory . 'fig_3.php',
$this->directory . 'prune_ripe.php',
SUPPORTPATH . 'Files/baker/banana.php',
];
$collection->addDirectory(SUPPORTPATH . 'Files', true);
$this->assertSame($expected, $collection->get());
}
public function testAddDirectories()
{
$collection = new FileCollection();
$expected = [
$this->directory . 'apple.php',
$this->directory . 'fig_3.php',
$this->directory . 'prune_ripe.php',
SUPPORTPATH . 'Files/baker/banana.php',
];
$collection->addDirectories([
$this->directory,
SUPPORTPATH . 'Files/baker',
]);
$this->assertSame($expected, $collection->get());
}
public function testAddDirectoriesRecursive()
{
$collection = new FileCollection();
$expected = [
$this->directory . 'apple.php',
$this->directory . 'fig_3.php',
$this->directory . 'prune_ripe.php',
SUPPORTPATH . 'Files/baker/banana.php',
SUPPORTPATH . 'Log/Handlers/TestHandler.php',
];
$collection->addDirectories([
SUPPORTPATH . 'Files',
SUPPORTPATH . 'Log',
], true);
$this->assertSame($expected, $collection->get());
}
public function testRemovePatternEmpty()
{
$collection = new FileCollection();
$collection->addDirectory(SUPPORTPATH . 'Files', true);
$files = $collection->get();
$collection->removePattern('');
$this->assertSame($files, $collection->get());
}
public function testRemovePatternRegex()
{
$collection = new FileCollection();
$collection->addDirectory(SUPPORTPATH . 'Files', true);
$expected = [
$this->directory . 'apple.php',
SUPPORTPATH . 'Files/baker/banana.php',
];
$collection->removePattern('#[a-z]+_.*#');
$this->assertSame($expected, $collection->get());
}
public function testRemovePatternPseudo()
{
$collection = new FileCollection();
$collection->addDirectory(SUPPORTPATH . 'Files', true);
$expected = [
$this->directory . 'apple.php',
SUPPORTPATH . 'Files/baker/banana.php',
];
$collection->removePattern('*_*.php');
$this->assertSame($expected, $collection->get());
}
public function testRemovePatternScope()
{
$collection = new FileCollection();
$collection->addDirectory(SUPPORTPATH . 'Files', true);
$expected = [
SUPPORTPATH . 'Files/baker/banana.php',
];
$collection->removePattern('*.php', $this->directory);
$this->assertSame($expected, $collection->get());
}
public function testRetainPatternEmpty()
{
$collection = new FileCollection();
$collection->addDirectory(SUPPORTPATH . 'Files', true);
$files = $collection->get();
$collection->retainPattern('');
$this->assertSame($files, $collection->get());
}
public function testRetainPatternRegex()
{
$collection = new FileCollection();
$collection->addDirectory(SUPPORTPATH . 'Files', true);
$expected = [
$this->directory . 'fig_3.php',
$this->directory . 'prune_ripe.php',
];
$collection->retainPattern('#[a-z]+_.*#');
$this->assertSame($expected, $collection->get());
}
public function testRetainPatternPseudo()
{
$collection = new FileCollection();
$collection->addDirectory(SUPPORTPATH . 'Files', true);
$expected = [
$this->directory . 'fig_3.php',
];
$collection->retainPattern('*_?.php');
$this->assertSame($expected, $collection->get());
}
public function testRetainPatternScope()
{
$collection = new FileCollection();
$collection->addDirectory(SUPPORTPATH . 'Files', true);
$expected = [
$this->directory . 'fig_3.php',
SUPPORTPATH . 'Files/baker/banana.php',
];
$collection->retainPattern('*_?.php', $this->directory);
$this->assertSame($expected, $collection->get());
}
public function testCount()
{
$collection = new FileCollection();
$collection->addDirectory(SUPPORTPATH . 'Files', true);
$this->assertCount(4, $collection);
}
public function testIterable()
{
$collection = new FileCollection();
$collection->addDirectory(SUPPORTPATH . 'Files', true);
$count = 0;
foreach ($collection as $file) {
$this->assertInstanceOf(File::class, $file);
$count++;
}
$this->assertSame($count, 4);
}
}

View File

@ -247,7 +247,7 @@ final class IncomingRequestTest extends CIUnitTestCase
public function testNegotiatesCharset()
{
// $_SERVER['HTTP_ACCEPT_CHARSET'] = 'iso-8859-5, unicode-1-1;q=0.8';
// $_SERVER['HTTP_ACCEPT_CHARSET'] = 'iso-8859-5, unicode-1-1;q=0.8';
$this->request->setHeader('Accept-Charset', 'iso-8859-5, unicode-1-1;q=0.8');
$this->assertSame(strtolower($this->request->config->charset), $this->request->negotiate('charset', ['iso-8859', 'unicode-1-2']));
@ -299,6 +299,8 @@ final class IncomingRequestTest extends CIUnitTestCase
$request = new IncomingRequest($config, new URI(), $json, new UserAgent());
$this->assertSame('bar', $request->getJsonVar('foo'));
$this->assertNull($request->getJsonVar('notExists'));
$jsonVar = $request->getJsonVar('baz');
$this->assertIsObject($jsonVar);
$this->assertSame('buzz', $jsonVar->fizz);
@ -340,11 +342,7 @@ final class IncomingRequestTest extends CIUnitTestCase
public function testGetVarWorksWithJson()
{
$jsonObj = [
'foo' => 'bar',
'fizz' => 'buzz',
];
$json = json_encode($jsonObj);
$json = json_encode(['foo' => 'bar', 'fizz' => 'buzz']);
$config = new App();
$config->baseURL = 'http://example.com/';
@ -354,6 +352,7 @@ final class IncomingRequestTest extends CIUnitTestCase
$this->assertSame('bar', $request->getVar('foo'));
$this->assertSame('buzz', $request->getVar('fizz'));
$this->assertNull($request->getVar('notExists'));
$multiple = $request->getVar(['foo', 'fizz']);
$this->assertIsArray($multiple);

View File

@ -271,6 +271,23 @@ final class LanguageTest extends CIUnitTestCase
$this->assertSame('More.shootMe', $this->lang->getLine('More.shootMe'));
}
/**
* Test if after using lang() with a locale the Language class keep the locale after return the $line
*/
public function testLangKeepLocale()
{
$this->lang = Services::language('en', true);
lang('Language.languageGetLineInvalidArgumentException');
$this->assertSame('en', $this->lang->getLocale());
lang('Language.languageGetLineInvalidArgumentException', [], 'ru');
$this->assertSame('en', $this->lang->getLocale());
lang('Language.languageGetLineInvalidArgumentException');
$this->assertSame('en', $this->lang->getLocale());
}
/**
* Testing base locale vs variants, with fallback to English.
*

View File

@ -0,0 +1,156 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
use CodeIgniter\Publisher\Publisher;
use CodeIgniter\Test\CIUnitTestCase;
/**
* @internal
*/
final class PublisherInputTest extends CIUnitTestCase
{
/**
* A known, valid file
*
* @var string
*/
private $file = SUPPORTPATH . 'Files/baker/banana.php';
/**
* A known, valid directory
*
* @var string
*/
private $directory = SUPPORTPATH . 'Files/able/';
/**
* Initialize the helper, since some
* tests call static methods before
* the constructor would load it.
*/
public static function setUpBeforeClass(): void
{
parent::setUpBeforeClass();
helper(['filesystem']);
}
public function testAddPathFile()
{
$publisher = new Publisher(SUPPORTPATH . 'Files');
$publisher->addPath('baker/banana.php');
$this->assertSame([$this->file], $publisher->get());
}
public function testAddPathFileRecursiveDoesNothing()
{
$publisher = new Publisher(SUPPORTPATH . 'Files');
$publisher->addPath('baker/banana.php', true);
$this->assertSame([$this->file], $publisher->get());
}
public function testAddPathDirectory()
{
$publisher = new Publisher(SUPPORTPATH . 'Files');
$expected = [
$this->directory . 'apple.php',
$this->directory . 'fig_3.php',
$this->directory . 'prune_ripe.php',
];
$publisher->addPath('able');
$this->assertSame($expected, $publisher->get());
}
public function testAddPathDirectoryRecursive()
{
$publisher = new Publisher(SUPPORTPATH);
$expected = [
$this->directory . 'apple.php',
$this->directory . 'fig_3.php',
$this->directory . 'prune_ripe.php',
SUPPORTPATH . 'Files/baker/banana.php',
];
$publisher->addPath('Files');
$this->assertSame($expected, $publisher->get());
}
public function testAddPaths()
{
$publisher = new Publisher(SUPPORTPATH . 'Files');
$expected = [
$this->directory . 'apple.php',
$this->directory . 'fig_3.php',
$this->directory . 'prune_ripe.php',
SUPPORTPATH . 'Files/baker/banana.php',
];
$publisher->addPaths([
'able',
'baker/banana.php',
]);
$this->assertSame($expected, $publisher->get());
}
public function testAddPathsRecursive()
{
$publisher = new Publisher(SUPPORTPATH);
$expected = [
$this->directory . 'apple.php',
$this->directory . 'fig_3.php',
$this->directory . 'prune_ripe.php',
SUPPORTPATH . 'Files/baker/banana.php',
SUPPORTPATH . 'Log/Handlers/TestHandler.php',
];
$publisher->addPaths([
'Files',
'Log',
], true);
$this->assertSame($expected, $publisher->get());
}
public function testAddUri()
{
$publisher = new Publisher();
$publisher->addUri('https://raw.githubusercontent.com/codeigniter4/CodeIgniter4/develop/composer.json');
$scratch = $this->getPrivateProperty($publisher, 'scratch');
$this->assertSame([$scratch . 'composer.json'], $publisher->get());
}
public function testAddUris()
{
$publisher = new Publisher();
$publisher->addUris([
'https://raw.githubusercontent.com/codeigniter4/CodeIgniter4/develop/LICENSE',
'https://raw.githubusercontent.com/codeigniter4/CodeIgniter4/develop/composer.json',
]);
$scratch = $this->getPrivateProperty($publisher, 'scratch');
$this->assertSame([$scratch . 'LICENSE', $scratch . 'composer.json'], $publisher->get());
}
}

View File

@ -0,0 +1,231 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
use CodeIgniter\Publisher\Publisher;
use CodeIgniter\Test\CIUnitTestCase;
use org\bovigo\vfs\vfsStream;
use org\bovigo\vfs\vfsStreamDirectory;
/**
* @internal
*/
final class PublisherOutputTest extends CIUnitTestCase
{
/**
* Files to seed to VFS
*
* @var array
*/
private $structure;
/**
* Virtual destination
*
* @var vfsStreamDirectory
*/
private $root;
/**
* A known, valid file
*
* @var string
*/
private $file = SUPPORTPATH . 'Files/baker/banana.php';
/**
* A known, valid directory
*
* @var string
*/
private $directory = SUPPORTPATH . 'Files/able/';
/**
* Initialize the helper, since some
* tests call static methods before
* the constructor would load it.
*/
public static function setUpBeforeClass(): void
{
parent::setUpBeforeClass();
helper(['filesystem']);
}
protected function setUp(): void
{
parent::setUp();
$this->structure = [
'able' => [
'apple.php' => 'Once upon a midnight dreary',
'bazam' => 'While I pondered weak and weary',
],
'boo' => [
'far' => 'Upon a tome of long-forgotten lore',
'faz' => 'There came a tapping up on the door',
],
'AnEmptyFolder' => [],
'simpleFile' => 'A tap-tap-tapping upon my door',
'.hidden' => 'There is no spoon',
];
$this->root = vfsStream::setup('root', null, $this->structure);
// Add root to the list of allowed destinations
config('Publisher')->restrictions[$this->root->url()] = '*';
}
public function testCopy()
{
$publisher = new Publisher($this->directory, $this->root->url());
$publisher->addFile($this->file);
$this->assertFileDoesNotExist($this->root->url() . '/banana.php');
$result = $publisher->copy(false);
$this->assertTrue($result);
$this->assertFileExists($this->root->url() . '/banana.php');
}
public function testCopyReplace()
{
$file = $this->directory . 'apple.php';
$publisher = new Publisher($this->directory, $this->root->url() . '/able');
$publisher->addFile($file);
$this->assertFileExists($this->root->url() . '/able/apple.php');
$this->assertFalse(same_file($file, $this->root->url() . '/able/apple.php'));
$result = $publisher->copy(true);
$this->assertTrue($result);
$this->assertTrue(same_file($file, $this->root->url() . '/able/apple.php'));
}
public function testCopyIgnoresSame()
{
$publisher = new Publisher($this->directory, $this->root->url());
$publisher->addFile($this->file);
copy($this->file, $this->root->url() . '/banana.php');
$result = $publisher->copy(false);
$this->assertTrue($result);
$result = $publisher->copy(true);
$this->assertTrue($result);
$this->assertSame([$this->root->url() . '/banana.php'], $publisher->getPublished());
}
public function testCopyIgnoresCollision()
{
$publisher = new Publisher($this->directory, $this->root->url());
mkdir($this->root->url() . '/banana.php');
$result = $publisher->addFile($this->file)->copy(false);
$this->assertTrue($result);
$this->assertSame([], $publisher->getErrors());
$this->assertSame([$this->root->url() . '/banana.php'], $publisher->getPublished());
}
public function testCopyCollides()
{
$publisher = new Publisher($this->directory, $this->root->url());
$expected = lang('Publisher.collision', ['dir', $this->file, $this->root->url() . '/banana.php']);
mkdir($this->root->url() . '/banana.php');
$result = $publisher->addFile($this->file)->copy(true);
$errors = $publisher->getErrors();
$this->assertFalse($result);
$this->assertCount(1, $errors);
$this->assertSame([$this->file], array_keys($errors));
$this->assertSame([], $publisher->getPublished());
$this->assertSame($expected, $errors[$this->file]->getMessage());
}
public function testMerge()
{
$publisher = new Publisher(SUPPORTPATH . 'Files', $this->root->url());
$expected = [
$this->root->url() . '/able/apple.php',
$this->root->url() . '/able/fig_3.php',
$this->root->url() . '/able/prune_ripe.php',
$this->root->url() . '/baker/banana.php',
];
$this->assertFileDoesNotExist($this->root->url() . '/able/fig_3.php');
$this->assertDirectoryDoesNotExist($this->root->url() . '/baker');
$result = $publisher->addPath('/')->merge(false);
$this->assertTrue($result);
$this->assertFileExists($this->root->url() . '/able/fig_3.php');
$this->assertDirectoryExists($this->root->url() . '/baker');
$this->assertSame($expected, $publisher->getPublished());
}
public function testMergeReplace()
{
$this->assertFalse(same_file($this->directory . 'apple.php', $this->root->url() . '/able/apple.php'));
$publisher = new Publisher(SUPPORTPATH . 'Files', $this->root->url());
$expected = [
$this->root->url() . '/able/apple.php',
$this->root->url() . '/able/fig_3.php',
$this->root->url() . '/able/prune_ripe.php',
$this->root->url() . '/baker/banana.php',
];
$result = $publisher->addPath('/')->merge(true);
$this->assertTrue($result);
$this->assertTrue(same_file($this->directory . 'apple.php', $this->root->url() . '/able/apple.php'));
$this->assertSame($expected, $publisher->getPublished());
}
public function testMergeCollides()
{
$publisher = new Publisher(SUPPORTPATH . 'Files', $this->root->url());
$expected = lang('Publisher.collision', ['dir', $this->directory . 'fig_3.php', $this->root->url() . '/able/fig_3.php']);
$published = [
$this->root->url() . '/able/apple.php',
$this->root->url() . '/able/prune_ripe.php',
$this->root->url() . '/baker/banana.php',
];
mkdir($this->root->url() . '/able/fig_3.php');
$result = $publisher->addPath('/')->merge(true);
$errors = $publisher->getErrors();
$this->assertFalse($result);
$this->assertCount(1, $errors);
$this->assertSame([$this->directory . 'fig_3.php'], array_keys($errors));
$this->assertSame($published, $publisher->getPublished());
$this->assertSame($expected, $errors[$this->directory . 'fig_3.php']->getMessage());
}
public function testPublish()
{
$publisher = new Publisher(SUPPORTPATH . 'Files', $this->root->url());
$result = $publisher->publish();
$this->assertTrue($result);
$this->assertFileExists($this->root->url() . '/able/fig_3.php');
$this->assertDirectoryExists($this->root->url() . '/baker');
$this->assertTrue(same_file($this->directory . 'apple.php', $this->root->url() . '/able/apple.php'));
}
}

View File

@ -0,0 +1,120 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
use CodeIgniter\Publisher\Exceptions\PublisherException;
use CodeIgniter\Publisher\Publisher;
use CodeIgniter\Test\CIUnitTestCase;
/**
* Publisher Restrictions Test
*
* Tests that the restrictions defined in the configuration
* file properly prevent disallowed actions.
*
* @internal
*/
final class PublisherRestrictionsTest extends CIUnitTestCase
{
/**
* @see Tests\Support\Config\Registrars::Publisher()
*/
public function testRegistrarsNotAllowed()
{
$this->assertArrayNotHasKey(SUPPORTPATH, config('Publisher')->restrictions);
}
public function testImmutableRestrictions()
{
$publisher = new Publisher();
// Try to "hack" the Publisher by adding our desired destination to the config
config('Publisher')->restrictions[SUPPORTPATH] = '*';
$restrictions = $this->getPrivateProperty($publisher, 'restrictions');
$this->assertArrayNotHasKey(SUPPORTPATH, $restrictions);
}
/**
* @dataProvider fileProvider
*/
public function testDefaultPublicRestrictions(string $path)
{
$publisher = new Publisher(ROOTPATH, FCPATH);
$pattern = config('Publisher')->restrictions[FCPATH];
// Use the scratch space to create a file
$file = $publisher->getScratch() . $path;
file_put_contents($file, 'To infinity and beyond!');
$result = $publisher->addFile($file)->merge();
$this->assertFalse($result);
$errors = $publisher->getErrors();
$this->assertCount(1, $errors);
$this->assertSame([$file], array_keys($errors));
$expected = lang('Publisher.fileNotAllowed', [$file, FCPATH, $pattern]);
$this->assertSame($expected, $errors[$file]->getMessage());
}
public function fileProvider()
{
yield 'php' => ['index.php'];
yield 'exe' => ['cat.exe'];
yield 'flat' => ['banana'];
}
/**
* @dataProvider destinationProvider
*/
public function testDestinations(string $destination, bool $allowed)
{
config('Publisher')->restrictions = [
APPPATH => '',
FCPATH => '',
SUPPORTPATH . 'Files' => '',
SUPPORTPATH . 'Files/../' => '',
];
if (! $allowed) {
$this->expectException(PublisherException::class);
$this->expectExceptionMessage(lang('Publisher.destinationNotAllowed', [$destination]));
}
$publisher = new Publisher(null, $destination);
$this->assertInstanceOf(Publisher::class, $publisher);
}
public function destinationProvider()
{
return [
'explicit' => [
APPPATH,
true,
],
'subdirectory' => [
APPPATH . 'Config',
true,
],
'relative' => [
SUPPORTPATH . 'Files/able/../',
true,
],
'parent' => [
SUPPORTPATH,
false,
],
];
}
}

View File

@ -0,0 +1,155 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
use CodeIgniter\Publisher\Exceptions\PublisherException;
use CodeIgniter\Publisher\Publisher;
use CodeIgniter\Test\CIUnitTestCase;
use Tests\Support\Publishers\TestPublisher;
/**
* @internal
*/
final class PublisherSupportTest extends CIUnitTestCase
{
/**
* A known, valid file
*
* @var string
*/
private $file = SUPPORTPATH . 'Files/baker/banana.php';
/**
* A known, valid directory
*
* @var string
*/
private $directory = SUPPORTPATH . 'Files/able/';
/**
* Initialize the helper, since some
* tests call static methods before
* the constructor would load it.
*/
public static function setUpBeforeClass(): void
{
parent::setUpBeforeClass();
helper(['filesystem']);
}
public function testDiscoverDefault()
{
$result = Publisher::discover();
$this->assertCount(1, $result);
$this->assertInstanceOf(TestPublisher::class, $result[0]);
}
public function testDiscoverNothing()
{
$result = Publisher::discover('Nothing');
$this->assertSame([], $result);
}
public function testDiscoverStores()
{
$publisher = Publisher::discover()[0];
$publisher->set([])->addFile($this->file);
$result = Publisher::discover();
$this->assertSame($publisher, $result[0]);
$this->assertSame([$this->file], $result[0]->get());
}
public function testGetSource()
{
$publisher = new Publisher(ROOTPATH);
$this->assertSame(ROOTPATH, $publisher->getSource());
}
public function testGetDestination()
{
$publisher = new Publisher(ROOTPATH, SUPPORTPATH);
$this->assertSame(SUPPORTPATH, $publisher->getDestination());
}
public function testGetScratch()
{
$publisher = new Publisher();
$this->assertNull($this->getPrivateProperty($publisher, 'scratch'));
$scratch = $publisher->getScratch();
$this->assertIsString($scratch);
$this->assertDirectoryExists($scratch);
$this->assertDirectoryIsWritable($scratch);
$this->assertNotNull($this->getPrivateProperty($publisher, 'scratch'));
// Directory and contents should be removed on __destruct()
$file = $scratch . 'obvious_statement.txt';
file_put_contents($file, 'Bananas are a most peculiar fruit');
$publisher->__destruct();
$this->assertFileDoesNotExist($file);
$this->assertDirectoryDoesNotExist($scratch);
}
public function testGetErrors()
{
$publisher = new Publisher();
$this->assertSame([], $publisher->getErrors());
$expected = [
$this->file => PublisherException::forCollision($this->file, $this->file),
];
$this->setPrivateProperty($publisher, 'errors', $expected);
$this->assertSame($expected, $publisher->getErrors());
}
public function testWipeDirectory()
{
$directory = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . bin2hex(random_bytes(6));
mkdir($directory, 0700);
$this->assertDirectoryExists($directory);
$method = $this->getPrivateMethodInvoker(Publisher::class, 'wipeDirectory');
$method($directory);
$this->assertDirectoryDoesNotExist($directory);
}
public function testWipeIgnoresFiles()
{
$method = $this->getPrivateMethodInvoker(Publisher::class, 'wipeDirectory');
$method($this->file);
$this->assertFileExists($this->file);
}
public function testWipe()
{
$directory = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . bin2hex(random_bytes(6));
mkdir($directory, 0700);
$this->assertDirectoryExists($directory);
config('Publisher')->restrictions[$directory] = ''; // Allow the directory
$publisher = new Publisher($this->directory, $directory);
$publisher->wipe();
$this->assertDirectoryDoesNotExist($directory);
}
}

View File

@ -0,0 +1,141 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Session\Handlers;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
use CodeIgniter\Test\ReflectionHelper;
use Config\App as AppConfig;
use Config\Database as DatabaseConfig;
/**
* @internal
*/
final class DatabaseHandlerTest extends CIUnitTestCase
{
use DatabaseTestTrait;
use ReflectionHelper;
protected $refresh = true;
protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder';
protected function setUp(): void
{
parent::setUp();
if (! in_array(config(DatabaseConfig::class)->tests['DBDriver'], ['MySQLi', 'Postgre'], true)) {
$this->markTestSkipped('Database Session Handler requires database driver to be MySQLi or Postgre');
}
}
protected function getInstance($options = [])
{
$defaults = [
'sessionDriver' => 'CodeIgniter\Session\Handlers\DatabaseHandler',
'sessionCookieName' => 'ci_session',
'sessionExpiration' => 7200,
'sessionSavePath' => 'ci_sessions',
'sessionMatchIP' => false,
'sessionTimeToUpdate' => 300,
'sessionRegenerateDestroy' => false,
'cookieDomain' => '',
'cookiePrefix' => '',
'cookiePath' => '/',
'cookieSecure' => false,
'cookieSameSite' => 'Lax',
];
$config = array_merge($defaults, $options);
$appConfig = new AppConfig();
foreach ($config as $key => $c) {
$appConfig->{$key} = $c;
}
return new DatabaseHandler($appConfig, '127.0.0.1');
}
public function testOpen()
{
$handler = $this->getInstance();
$this->assertTrue($handler->open('ci_sessions', 'ci_session'));
}
public function testReadSuccess()
{
$handler = $this->getInstance();
$expected = '__ci_last_regenerate|i:1624650854;_ci_previous_url|s:40:\"http://localhost/index.php/home/index\";';
$this->assertSame($expected, $handler->read('1f5o06b43phsnnf8if6bo33b635e4p2o'));
$this->assertTrue($this->getPrivateProperty($handler, 'rowExists'));
$this->assertSame('1483201a66afd2bd671e4a67dc6ecf24', $this->getPrivateProperty($handler, 'fingerprint'));
}
public function testReadFailure()
{
$handler = $this->getInstance();
$this->assertSame('', $handler->read('123456b43phsnnf8if6bo33b635e4321'));
$this->assertFalse($this->getPrivateProperty($handler, 'rowExists'));
$this->assertSame('d41d8cd98f00b204e9800998ecf8427e', $this->getPrivateProperty($handler, 'fingerprint'));
}
public function testWriteInsert()
{
$handler = $this->getInstance();
$this->setPrivateProperty($handler, 'lock', true);
$data = '__ci_last_regenerate|i:1624650854;_ci_previous_url|s:40:\"http://localhost/index.php/home/index\";';
$this->assertTrue($handler->write('555556b43phsnnf8if6bo33b635e4444', $data));
$this->setPrivateProperty($handler, 'lock', false);
$row = $this->db->table('ci_sessions')
->getWhere(['id' => '555556b43phsnnf8if6bo33b635e4444'])
->getRow();
$this->assertGreaterThan(time() - 100, strtotime($row->timestamp));
$this->assertSame('1483201a66afd2bd671e4a67dc6ecf24', $this->getPrivateProperty($handler, 'fingerprint'));
}
public function testWriteUpdate()
{
$handler = $this->getInstance();
$this->setPrivateProperty($handler, 'sessionID', '1f5o06b43phsnnf8if6bo33b635e4p2o');
$this->setPrivateProperty($handler, 'rowExists', true);
$lockSession = $this->getPrivateMethodInvoker($handler, 'lockSession');
$lockSession('1f5o06b43phsnnf8if6bo33b635e4p2o');
$data = '__ci_last_regenerate|i:1624650854;_ci_previous_url|s:40:\"http://localhost/index.php/home/index\";';
$this->assertTrue($handler->write('1f5o06b43phsnnf8if6bo33b635e4p2o', $data));
$releaseLock = $this->getPrivateMethodInvoker($handler, 'releaseLock');
$releaseLock();
$row = $this->db->table('ci_sessions')
->getWhere(['id' => '1f5o06b43phsnnf8if6bo33b635e4p2o'])
->getRow();
$this->assertGreaterThan(time() - 100, strtotime($row->timestamp));
$this->assertSame('1483201a66afd2bd671e4a67dc6ecf24', $this->getPrivateProperty($handler, 'fingerprint'));
}
public function testGC()
{
$handler = $this->getInstance();
$this->assertSame(1, $handler->gc(3600));
}
}

View File

@ -984,4 +984,63 @@ final class ValidationTest extends CIUnitTestCase
]],
];
}
/**
* @dataProvider provideStringRulesCases
*
* @see https://github.com/codeigniter4/CodeIgniter4/issues/4929
*/
public function testSplittingOfComplexStringRules(string $input, array $expected): void
{
$splitter = $this->getPrivateMethodInvoker($this->validation, 'splitRules');
$this->assertSame($expected, $splitter($input));
}
public function provideStringRulesCases(): iterable
{
yield [
'required',
['required'],
];
yield [
'required|numeric',
['required', 'numeric'],
];
yield [
'required|max_length[500]|hex',
['required', 'max_length[500]', 'hex'],
];
yield [
'required|numeric|regex_match[/[a-zA-Z]+/]',
['required', 'numeric', 'regex_match[/[a-zA-Z]+/]'],
];
yield [
'required|max_length[500]|regex_match[/^;"\'{}\[\]^<>=/]',
['required', 'max_length[500]', 'regex_match[/^;"\'{}\[\]^<>=/]'],
];
yield [
'regex_match[/^;"\'{}\[\]^<>=/]|regex_match[/[^a-z0-9.\|_]+/]',
['regex_match[/^;"\'{}\[\]^<>=/]', 'regex_match[/[^a-z0-9.\|_]+/]'],
];
yield [
'required|regex_match[/^(01[2689]|09)[0-9]{8}$/]|numeric',
['required', 'regex_match[/^(01[2689]|09)[0-9]{8}$/]', 'numeric'],
];
yield [
'required|regex_match[/^[0-9]{4}[\-\.\[\/][0-9]{2}[\-\.\[\/][0-9]{2}/]|max_length[10]',
['required', 'regex_match[/^[0-9]{4}[\-\.\[\/][0-9]{2}[\-\.\[\/][0-9]{2}/]', 'max_length[10]'],
];
yield [
'required|regex_match[/^(01|2689|09)[0-9]{8}$/]|numeric',
['required', 'regex_match[/^(01|2689|09)[0-9]{8}$/]', 'numeric'],
];
}
}

View File

@ -12,6 +12,7 @@ See all the changes.
.. toctree::
:titlesonly:
v4.2.0
v4.1.4
v4.1.3
v4.1.2

View File

@ -0,0 +1,19 @@
Version 4.2.0
=============
Release Date: Not released
**4.2.0 release of CodeIgniter4**
Enhancements:
- Added Cache config for reserved characters
- The ``addForeignKey`` function of the ``Forge`` class can now define composite foreign keys in an array
Changes:
Deprecations:
- Deprecated ``CodeIgniter\\Cache\\Handlers\\BaseHandler::RESERVED_CHARACTERS`` in favor of the new config property
Bugs Fixed:

View File

@ -205,6 +205,27 @@ Options:
* ``--suffix``: Append the component suffix to the generated class name.
* ``--force``: Set this flag to overwrite existing files on destination.
make:validation
---------------
Creates a new validation file.
Usage:
======
::
make:validation <name> [options]
Argument:
=========
* ``name``: The name of the validation class. **[REQUIRED]**
Options:
========
* ``--namespace``: Set the root namespace. Defaults to value of ``APP_NAMESPACE``.
* ``--suffix``: Append the component suffix to the generated class name.
* ``--force``: Set this flag to overwrite existing files on destination.
.. note:: Do you need to have the generated code in a subfolder? Let's say if you want to create a controller
class to reside in the ``Admin`` subfolder of the main ``Controllers`` folder, you will just need
to prepend the subfolder to the class name, like this: ``php spark make:controller admin/login``. This

View File

@ -38,7 +38,7 @@ Getting Input from the User
Sometimes you need to ask the user for more information. They might not have provided optional command-line
arguments, or the script may have encountered an existing file and needs confirmation before overwriting. This is
handled with the ``prompt()`` method.
handled with the ``prompt()`` or ``promptByKey()`` method.
You can provide a question by passing it in as the first parameter::
@ -61,6 +61,38 @@ Validation rules can also be written in the array syntax.::
$email = CLI::prompt('What is your email?', null, ['required', 'valid_email']);
**promptByKey()**
Predefined answers (options) for prompt sometimes need to be described or are too complex to select via their value.
``promptByKey()`` allows the user to select an option by its key instead of its value::
$fruit = CLI::promptByKey('These are your choices:', ['The red apple', 'The plump orange', 'The ripe banana']);
//These are your choices:
// [0] The red apple
// [1] The plump orange
// [2] The ripe banana
//
//[0, 1, 2]:
Named keys are also possible::
$fruit = CLI::promptByKey(['These are your choices:', 'Which would you like?'], [
'apple' => 'The red apple',
'orange' => 'The plump orange',
'banana' => 'The ripe banana'
]);
//These are your choices:
// [apple] The red apple
// [orange] The plump orange
// [banana] The ripe banana
//
//Which would you like? [apple, orange, banana]:
Finally, you can pass :ref:`validation <validation>` rules to the answer input as the third parameter, the acceptable answers are automatically restricted to the passed options.
Providing Feedback
==================

View File

@ -15,6 +15,7 @@ Regular Queries
To submit a query, use the **query** function::
$db = db_connect();
$db->query('YOUR QUERY HERE');
The ``query()`` function returns a database result **object** when "read"

View File

@ -244,7 +244,10 @@ This function enables you to set **WHERE** clauses using one of four
methods:
.. note:: All values passed to this function are escaped automatically,
producing safer queries.
producing safer queries, except when using a custom string.
.. note:: ``$builder->where()`` accepts an optional third parameter. If you set it to
``false``, CodeIgniter will not try to protect your field or table names.
#. **Simple key/value method:**
@ -294,15 +297,18 @@ methods:
#. **Custom string:**
You can write your own clauses manually::
$where = "name='Joe' AND status='boss' OR status='active'";
$builder->where($where);
``$builder->where()`` accepts an optional third parameter. If you set it to
``false``, CodeIgniter will not try to protect your field or table names.
If you are using user-supplied data within the string, you MUST escape the
data manually. Failure to do so could result in SQL injections.
::
::
$name = $builder->db->escape('Joe');
$where = "name={$name} AND status='boss' OR status='active'";
$builder->where($where);
$builder->where('MATCH (field) AGAINST ("value")', null, false);
#. **Subqueries:**
You can use an anonymous function to create a subquery.
@ -1720,7 +1726,7 @@ Class Reference
.. php:method:: set($key[, $value = ''[, $escape = null]])
:param mixed $key: Field name, or an array of field/value pairs
:param string $value: Field value, if $key is a single field
:param mixed $value: Field value, if $key is a single field
:param bool $escape: Whether to escape values and identifiers
:returns: ``BaseBuilder`` instance (method chaining)
:rtype: ``BaseBuilder``

View File

@ -159,6 +159,36 @@ it returns the current row and moves the internal data pointer ahead.
echo $row->body;
}
For use with MySQLi you may set MySQLi's result mode to
``MYSQLI_USE_RESULT`` for maximum memory savings. Use of this is not
generally recommended but it can be beneficial in some circumstances
such as writing large queries to csv. If you change the result mode
be aware of the tradeoffs associated with it.
::
$db->resultMode = MYSQLI_USE_RESULT; // for unbuffered results
$query = $db->query("YOUR QUERY");
$file = new \CodeIgniter\Files\File(WRITEPATH.'data.csv');
$csv = $file->openFile('w');
while ($row = $query->getUnbufferedRow('array'))
{
$csv->fputcsv($row);
}
$db->resultMode = MYSQLI_STORE_RESULT; // return to default mode
.. note:: When using ``MYSQLI_USE_RESULT`` all subsequent calls on the same
connection will result in error until all records have been fetched or
a ``freeResult()`` call has been made. The ``getNumRows()`` method will only
return the number of rows based on the current position of the data pointer.
MyISAM tables will remain locked until all the records have been fetched
or a ``freeResult()`` call has been made.
You can optionally pass 'object' (default) or 'array' in order to specify
the returned value's type::

View File

@ -230,13 +230,19 @@ Adding Foreign Keys
Foreign Keys help to enforce relationships and actions across your tables. For tables that support Foreign Keys,
you may add them directly in forge::
$forge->addForeignKey('users_id','users','id');
// gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`) REFERENCES `users`(`id`)
$forge->addForeignKey('users_id','users','id');
// gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`) REFERENCES `users`(`id`)
$forge->addForeignKey(['users_id', 'users_name'],'users',['id', 'name']);
// gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`, `users_name`) REFERENCES `users`(`id`, `name`)
You can specify the desired action for the "on delete" and "on update" properties of the constraint::
$forge->addForeignKey('users_id','users','id','CASCADE','CASCADE');
// gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE
$forge->addForeignKey('users_id','users','id','CASCADE','CASCADE');
// gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE
$forge->addForeignKey(['users_id', 'users_name'],'users',['id', 'name'],'CASCADE','CASCADE');
// gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`, `users_name`) REFERENCES `users`(`id`, `name`) ON DELETE CASCADE ON UPDATE CASCADE
Creating a table
================

View File

@ -258,8 +258,11 @@ creates is the Pascal case version of the filename.
You can use (make:migration) with the following options:
- ``-n`` - to choose namespace, otherwise the value of ``APP_NAMESPACE`` will be used.
- ``-force`` - If a similarly named migration file is present in destination, this will be overwritten.
- ``--session`` - Generates the migration file for database sessions.
- ``--table`` - Table name to use for database sessions. Default: ``ci_sessions``.
- ``--dbgroup`` - Database group to use for database sessions. Default: ``default``.
- ``--namespace`` - Set root namespace. Default: ``APP_NAMESPACE``.
- ``--suffix`` - Append the component title to the class name.
*********************
Migration Preferences

View File

@ -125,15 +125,13 @@ Using the command line, you can easily generate seed files.
::
// This command will create a UserSeeder seed file
// located at app/Database/Seeds/ directory.
> php spark make:seeder UserSeeder
> php spark make:seeder user --suffix
// Output: UserSeeder.php file located at app/Database/Seeds directory.
You can supply the **root** namespace where the seed file will be stored by supplying the ``-n`` option::
You can supply the **root** namespace where the seed file will be stored by supplying the ``--namespace`` option::
> php spark make:seeder MySeeder -n Acme\Blog
> php spark make:seeder MySeeder --namespace Acme\Blog
If ``Acme\Blog`` is mapped to ``app/Blog`` directory, then this command will save the
seed file to ``app/Blog/Database/Seeds/``.
If ``Acme\Blog`` is mapped to ``app/Blog`` directory, then this command will generate ``MySeeder.php`` at ``app/Blog/Database/Seeds`` directory.
Supplying the ``--force`` option will overwrite existing files in destination.

View File

@ -0,0 +1,12 @@
#############################
Upgrading from 4.1.3 to 4.2.0
#############################
**Changes for set() method in BaseBuilder and Model class**
The casting for the ``$value`` parameter has been removed to fix a bug where passing parameters as array and string
to the ``set()`` method were handled differently. If you extended the ``BaseBuilder`` class or ``Model`` class yourself
and modified the ``set()`` method, then you need to change its definition from
``public function set($key, ?string $value = '', ?bool $escape = null)`` to
``public function set($key, $value = '', ?bool $escape = null)``.

View File

@ -107,3 +107,124 @@ The move() method returns a new File instance that for the relocated file, so yo
resulting location is needed::
$file = $file->move(WRITEPATH.'uploads');
****************
File Collections
****************
Working with groups of files can be cumbersome, so the framework supplies the ``FileCollection`` class to facilitate
locating and working with groups of files across the filesystem. At its most basic, ``FileCollection`` is an index
of files you set or build::
$files = new FileCollection([
FCPATH . 'index.php',
ROOTPATH . 'spark',
]);
$files->addDirectory(APPPATH . 'Filters');
After you have input the files you would like to work with you may remove files or use the filtering commands to remove
or retain files matching a certain regex or glob-style pattern::
$files->removeFile(APPPATH . 'Filters/DevelopToolbar');
$files->removePattern('#\.gitkeep#');
$files->retainPattern('*.php');
When your collection is complete, you can use ``get()`` to retrieve the final list of file paths, or take advantage of
``FileCollection`` being countable and iterable to work directly with each ``File``::
echo 'My files: ' . implode(PHP_EOL, $files->get());
echo 'I have ' . count($files) . ' files!';
foreach ($files as $file)
{
echo 'Moving ' . $file->getBasename() . ', ' . $file->getSizeByUnit('mb');
$file->move(WRITABLE . $file->getRandomName());
}
Below are the specific methods for working with a ``FileCollection``.
Starting a Collection
=====================
**__construct(string[] $files = [])**
The constructor accepts an optional array of file paths to use as the initial collection. These are passed to
**add()** so any files supplied by child classes in the **$files** will remain.
**define()**
Allows child classes to define their own initial files. This method is called by the constructor and allows
predefined collections without having to use their methods. Example::
class ConfigCollection extends \CodeIgniter\Files\FileCollection
{
protected function define(): void
{
$this->add(APPPATH . 'Config', true)->retainPattern('*.php');
}
}
Now you may use the ``ConfigCollection`` anywhere in your project to access all App Config files without
having to re-call the collection methods every time.
**set(array $files)**
Sets the list of input files to the provided string array of file paths. This will remove any existing
files from the collection, so ``$collection->set([])`` is essentially a hard reset.
Inputting Files
===============
**add(string[]|string $paths, bool $recursive = true)**
Adds all files indicated by the path or array of paths. If the path resolves to a directory then ``$recursive``
will include sub-directories.
**addFile(string $file)**
**addFiles(array $files)**
Adds the file or files to the current list of input files. Files are absolute paths to actual files.
**removeFile(string $file)**
**removeFiles(array $files)**
Removes the file or files from the current list of input files.
**addDirectory(string $directory, bool $recursive = false)**
**addDirectories(array $directories, bool $recursive = false)**
Adds all files from the directory or directories, optionally recursing into sub-directories. Directories are
absolute paths to actual directories.
Filtering Files
===============
**removePattern(string $pattern, string $scope = null)**
**retainPattern(string $pattern, string $scope = null)**
Filters the current file list through the pattern (and optional scope), removing or retaining matched
files. ``$pattern`` may be a complete regex (like ``'#[A-Za-z]+\.php#'``) or a pseudo-regex similar
to ``glob()`` (like ``*.css``).
If a ``$scope`` is provided then only files in or under that directory will be considered (i.e. files
outside of ``$scope`` are always retained). When no scope is provided then all files are subject.
Examples::
$files = new FileCollection();
$files->add(APPPATH . 'Config', true); // Adds all Config files and directories
$files->removePattern('*tion.php'); // Would remove Encryption.php, Validation.php, and boot/production.php
$files->removePattern('*tion.php', APPPATH . 'Config/boot'); // Would only remove boot/production.php
$files->retainPattern('#A.+php$#'); // Would keep only Autoload.php
$files->retainPattern('#d.+php$#', APPPATH . 'Config/boot'); // Would keep everything but boot/production.php and boot/testing.php
Retrieving Files
================
**get(): string[]**
Returns an array of all the loaded input files.
.. note:: ``FileCollection`` is an ``IteratorAggregate`` so you can work with it directly (e.g. ``foreach ($collection as $file)``).

View File

@ -14,6 +14,7 @@ Library Reference
honeypot
images
pagination
publisher
security
sessions
throttler

View File

@ -0,0 +1,410 @@
#########
Publisher
#########
The Publisher library provides a means to copy files within a project using robust detection and error checking.
.. contents::
:local:
:depth: 2
*******************
Loading the Library
*******************
Because Publisher instances are specific to their source and destination this library is not available
through ``Services`` but should be instantiated or extended directly. E.g.
$publisher = new \CodeIgniter\Publisher\Publisher();
*****************
Concept and Usage
*****************
``Publisher`` solves a handful of common problems when working within a backend framework:
* How do I maintain project assets with version dependencies?
* How do I manage uploads and other "dynamic" files that need to be web accessible?
* How can I update my project when the framework or modules change?
* How can components inject new content into existing projects?
At its most basic, publishing amounts to copying a file or files into a project. ``Publisher`` extends ``FileCollection``
to enact fluent-style command chaining to read, filter, and process input files, then copies or merges them into the target destination.
You may use ``Publisher`` on demand in your Controllers or other components, or you may stage publications by extending
the class and leveraging its discovery with ``spark publish``.
On Demand
=========
Access ``Publisher`` directly by instantiating a new instance of the class::
$publisher = new \CodeIgniter\Publisher\Publisher();
By default the source and destination will be set to ``ROOTPATH`` and ``FCPATH`` respectively, giving ``Publisher``
easy access to take any file from your project and make it web-accessible. Alternatively you may pass a new source
or source and destination into the constructor::
use CodeIgniter\Publisher\Publisher;
$vendorPublisher = new Publisher(ROOTPATH . 'vendor');
$filterPublisher = new Publisher('/path/to/module/Filters', APPPATH . 'Filters');
// Once the source and destination are set you may start adding relative input files
$frameworkPublisher = new Publisher(ROOTPATH . 'vendor/codeigniter4/codeigniter4');
// All "path" commands are relative to $source
$frameworkPublisher->addPath('app/Config/Cookie.php');
// You may also add from outside the source, but the files will not be merged into subdirectories
$frameworkPublisher->addFiles([
'/opt/mail/susan',
'/opt/mail/ubuntu',
]);
$frameworkPublisher->addDirectory(SUPPORTPATH . 'Images');
Once all the files are staged use one of the output commands (**copy()** or **merge()**) to process the staged files
to their destination(s)::
// Place all files into $destination
$frameworkPublisher->copy();
// Place all files into $destination, overwriting existing files
$frameworkPublisher->copy(true);
// Place files into their relative $destination directories, overwriting and saving the boolean result
$result = $frameworkPublisher->merge(true);
See the :ref:`reference` for a full description of available methods.
Automation and Discovery
========================
You may have regular publication tasks embedded as part of your application deployment or upkeep. ``Publisher`` leverages
the powerful ``Autoloader`` to locate any child classes primed for publication::
use CodeIgniter\CLI\CLI;
use CodeIgniter\Publisher\Publisher;
foreach (Publisher::discover() as $publisher)
{
$result = $publisher->publish();
if ($result === false)
{
CLI::error(get_class($publisher) . ' failed to publish!', 'red');
}
}
By default ``discover()`` will search for the "Publishers" directory across all namespaces, but you may specify a
different directory and it will return any child classes found::
$memePublishers = Publisher::discover('CatGIFs');
Most of the time you will not need to handle your own discovery, just use the provided "publish" command::
> php spark publish
By default on your class extension ``publish()`` will add all files from your ``$source`` and merge them
out to your destination, overwriting on collision.
Security
========
In order to prevent modules from injecting malicious code into your projects, ``Publisher`` contains a config file
that defines which directories and file patterns are allowed as destinations. By default, files may only be published
to your project (to prevent access to the rest of the filesystem), and the **public/** folder (``FCPATH``) will only
receive files with the following extensions:
* Web assets: css, scss, js, map
* Non-executable web files: htm, html, xml, json, webmanifest
* Fonts: tff, eot, woff
* Images: gif, jpg, jpeg, tiff, png, webp, bmp, ico, svg
If you need to add or adjust the security for your project then alter the ``$restrictions`` property of ``Config\Publisher``.
********
Examples
********
Here are a handful of example use cases and their implementations to help you get started publishing.
File Sync Example
=================
You want to display a "photo of the day" image on your homepage. You have a feed for daily photos but you
need to get the actual file into a browsable location in your project at **public/images/daily_photo.jpg**.
You can set up :doc:`Custom Command </cli/cli_commands>` to run daily that will handle this for you::
<?php
namespace App\Commands;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\Publisher\Publisher;
use Throwable;
class DailyPhoto extends BaseCommand
{
protected $group = 'Publication';
protected $name = 'publish:daily';
protected $description = 'Publishes the latest daily photo to the homepage.';
public function run(array $params)
{
$publisher = new Publisher('/path/to/photos/', FCPATH . 'assets/images');
try
{
$publisher->addPath('daily_photo.jpg')->copy(true); // `true` to enable overwrites
}
catch (Throwable $e)
{
$this->showError($e);
}
}
}
Now running ``spark publish:daily`` will keep your homepage's image up-to-date. What if the photo is
coming from an external API? You can use ``addUri()`` in place of ``addPath()`` to download the remote
resource and publish it out instead::
$publisher->addUri('https://example.com/feeds/daily_photo.jpg')->copy(true);
Asset Dependencies Example
==========================
You want to integrate the frontend library "Bootstrap" into your project, but the frequent updates makes it a hassle
to keep up with. You can create a publication definition in your project to sync frontend assets by extending
``Publisher`` in your project. So **app/Publishers/BootstrapPublisher.php** might look like this::
<?php
namespace App\Publishers;
use CodeIgniter\Publisher\Publisher;
class BootstrapPublisher extends Publisher
{
/**
* Tell Publisher where to get the files.
* Since we will use Composer to download
* them we point to the "vendor" directory.
*
* @var string
*/
protected $source = 'vendor/twbs/bootstrap/';
/**
* FCPATH is always the default destination,
* but we may want them to go in a sub-folder
* to keep things organized.
*
* @var string
*/
protected $destination = FCPATH . 'bootstrap';
/**
* Use the "publish" method to indicate that this
* class is ready to be discovered and automated.
*
* @return boolean
*/
public function publish(): bool
{
return $this
// Add all the files relative to $source
->addPath('dist')
// Indicate we only want the minimized versions
->retainPattern('*.min.*')
// Merge-and-replace to retain the original directory structure
->merge(true);
}
}
Now add the dependency via Composer and call ``spark publish`` to run the publication::
> composer require twbs/bootstrap
> php spark publish
... and you'll end up with something like this::
public/.htaccess
public/favicon.ico
public/index.php
public/robots.txt
public/
bootstrap/
css/
bootstrap.min.css
bootstrap-utilities.min.css.map
bootstrap-grid.min.css
bootstrap.rtl.min.css
bootstrap.min.css.map
bootstrap-reboot.min.css
bootstrap-utilities.min.css
bootstrap-reboot.rtl.min.css
bootstrap-grid.min.css.map
js/
bootstrap.esm.min.js
bootstrap.bundle.min.js.map
bootstrap.bundle.min.js
bootstrap.min.js
bootstrap.esm.min.js.map
bootstrap.min.js.map
Module Deployment Example
=========================
You want to allow developers using your popular authentication module the ability to expand on the default behavior
of your Migration, Controller, and Model. You can create your own module "publish" command to inject these components
into an application for use::
<?php
namespace Math\Auth\Commands;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\Publisher\Publisher;
use Throwable;
class AuthPublish extends BaseCommand
{
protected $group = 'Auth';
protected $name = 'auth:publish';
protected $description = 'Publish Auth components into the current application.';
public function run(array $params)
{
// Use the Autoloader to figure out the module path
$source = service('autoloader')->getNamespace('Math\\Auth');
$publisher = new Publisher($source, APPATH);
try
{
// Add only the desired components
$publisher->addPaths([
'Controllers',
'Database/Migrations',
'Models',
])->merge(false); // Be careful not to overwrite anything
}
catch (Throwable $e)
{
$this->showError($e);
return;
}
// If publication succeeded then update namespaces
foreach ($publisher->getPublished() as $file)
{
// Replace the namespace
$contents = file_get_contents($file);
$contents = str_replace('namespace Math\\Auth', 'namespace ' . APP_NAMESPACE, );
file_put_contents($file, $contents);
}
}
}
Now when your module users run ``php spark auth:publish`` they will have the following added to their project::
app/Controllers/AuthController.php
app/Database/Migrations/2017-11-20-223112_create_auth_tables.php.php
app/Models/LoginModel.php
app/Models/UserModel.php
.. _reference:
*****************
Library Reference
*****************
.. note:: ``Publisher`` is an extension of :doc:`FileCollection </libraries/files>` so has access to all those methods for reading and filtering files.
Support Methods
===============
**[static] discover(string $directory = 'Publishers'): Publisher[]**
Discovers and returns all Publishers in the specified namespace directory. For example, if both
**app/Publishers/FrameworkPublisher.php** and **myModule/src/Publishers/AssetPublisher.php** exist and are
extensions of ``Publisher`` then ``Publisher::discover()`` would return an instance of each.
**publish(): bool**
Processes the full input-process-output chain. By default this is the equivalent of calling ``addPath($source)``
and ``merge(true)`` but child classes will typically provide their own implementation. ``publish()`` is called
on all discovered Publishers when running ``spark publish``.
Returns success or failure.
**getScratch(): string**
Returns the temporary workspace, creating it if necessary. Some operations use intermediate storage to stage
files and changes, and this provides the path to a transient, writable directory that you may use as well.
**getErrors(): array<string,Throwable>**
Returns any errors from the last write operation. The array keys are the files that caused the error, and the
values are the Throwable that was caught. Use ``getMessage()`` on the Throwable to get the error message.
**addPath(string $path, bool $recursive = true)**
**addPaths(array $path, bool $recursive = true)**
Adds all files indicated by the relative paths. Paths are references to actual files or directories relative
to ``$source``. If the relative path resolves to a directory then ``$recursive`` will include sub-directories.
**addUri(string $uri)**
**addUris(array $uris)**
Downloads the contents of a URI using ``CURLRequest`` into the scratch workspace then adds the resulting
file to the list.
.. note:: The CURL request made is a simple ``GET`` and uses the response body for the file contents. Some
remote files may need a custom request to be handled properly.
Outputting Files
================
**wipe()**
Removes all files, directories, and sub-directories from ``$destination``.
.. important:: Use wisely.
**copy(bool $replace = true): bool**
Copies all files into the ``$destination``. This does not recreate the directory structure, so every file
from the current list will end up in the same destination directory. Using ``$replace`` will cause files
to overwrite when there is already an existing file. Returns success or failure, use ``getPublished()``
and ``getErrors()`` to troubleshoot failures.
Be mindful of duplicate basename collisions, for example::
$publisher = new Publisher('/home/source', '/home/destination');
$publisher->addPaths([
'pencil/lead.png',
'metal/lead.png',
]);
// This is bad! Only one file will remain at /home/destination/lead.png
$publisher->copy(true);
**merge(bool $replace = true): bool**
Copies all files into the ``$destination`` in appropriate relative sub-directories. Any files that
match ``$source`` will be placed into their equivalent directories in ``$destination``, effectively
creating a "mirror" or "rsync" operation. Using ``$replace`` will cause files
to overwrite when there is already an existing file; since directories are merged this will not
affect other files in the destination. Returns success or failure, use ``getPublished()`` and
``getErrors()`` to troubleshoot failures.
Example::
$publisher = new Publisher('/home/source', '/home/destination');
$publisher->addPaths([
'pencil/lead.png',
'metal/lead.png',
]);
// Results in "/home/destination/pencil/lead.png" and "/home/destination/metal/lead.png"
$publisher->merge();

View File

@ -610,7 +610,7 @@ setting**. The examples below work both on MySQL and PostgreSQL::
ALTER TABLE ci_sessions DROP PRIMARY KEY;
You can choose the Database group to use by adding a new line to the
**application\Config\App.php** file with the name of the group to use::
**app/Config/App.php** file with the name of the group to use::
public $sessionDBGroup = 'groupName';

View File

@ -75,10 +75,9 @@ along the lines of::
{
$throttler = Services::throttler();
// Restrict an IP address to no more
// than 1 request per second across the
// entire site.
if ($throttler->check($request->getIPAddress(), 60, MINUTE) === false) {
// Restrict an IP address to no more than 1 request
// per second across the entire site.
if ($throttler->check(md5($request->getIPAddress()), 60, MINUTE) === false) {
return Services::response()->setStatusCode(429);
}
}

View File

@ -275,13 +275,13 @@ simply map the ``full_name`` column in the database to the ``$name`` property, a
];
protected $datamap = [
'full_name' => 'name',
'name' => 'full_name',
];
}
By adding our new database name to the ``$datamap`` array, we can tell the class what class property the database column
should be accessible through. The key of the array is the name of the column in the database, where the value in the array
is class property to map it to.
should be accessible through. The key of the array is class property to map it to, where the value in the array is the
name of the column in the database.
In this example, when the model sets the ``full_name`` field on the User class, it actually assigns that value to the
class' ``$name`` property, so it can be set and retrieved through ``$user->name``. The value will still be accessible

View File

@ -2,7 +2,7 @@
Testing
#######
CodeIgniter ships with a number of tools to help you test and debug your application thoroughly.
CodeIgniter ships with a number of tools to help you test and debug your application thoroughly.
The following sections should get you quickly testing your applications.
.. toctree::
@ -16,3 +16,4 @@ The following sections should get you quickly testing your applications.
response
benchmark
debugging
Mocking <mocking>

View File

@ -0,0 +1,53 @@
######################
Mocking System Classes
######################
Several components within the framework provide mocked versions of their classes that can be used during testing. These classes
can take the place of the normal class during test execution, often providing additional assertions to test that actions
have taken place (or not taken place) during the execution of the test. This might be checking data gets cached correctly,
emails were sent correctly, etc.
.. contents::
:local:
:depth: 1
Cache
=====
You can mock the cache with the ``mock()`` method, using the ``CacheFactory`` as its only parameter.
::
$mock = mock(CodeIgniter\Cache\CacheFactory::class);
While this returns an instance of ``CodeIgniter\Test\Mock\MockCache`` that you can use directly, it also inserts the
mock into the Service class, so any calls within your code to ``service('cache')`` or ``Config\Services::cache()`` will
use the mocked class within its place.
When using this in more than one test method within a single file you should call either the ``clean()`` or ``bypass()``
methods during the test ``setUp()`` to ensure a clean slate when your tests run.
Additional Methods
------------------
You can instruct the mocked cache handler to never do any caching with the ``bypass()`` method. This will emulate
using the dummy handler and ensures that your test does not rely on cached data for your tests.
::
$mock = mock(CodeIgniter\Cache\CacheFactory::class);
// Never cache any items during this test.
$mock->bypass();
Available Assertions
--------------------
The following new assertions are available on the mocked class for using during testing:
::
$mock = mock(CodeIgniter\Cache\CacheFactory::class);
// Assert that a cached item named $key exists
$mock->assertHas($key);
// Assert that a cached item named $key exists with a value of $value
$mock->assertHasValue($key, $value);
// Assert that a cached item named $key does NOT exist
$mock->assertMissing($key);