mirror of
https://github.com/codeigniter4/CodeIgniter4.git
synced 2025-02-20 11:44:28 +08:00
commit
3ea4bac65b
75
.github/workflows/test-deptrac.yml
vendored
Normal file
75
.github/workflows/test-deptrac.yml
vendored
Normal 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
|
2
.github/workflows/test-phpcpd.yml
vendored
2
.github/workflows/test-phpcpd.yml
vendored
@ -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/
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
28
app/Config/Publisher.php
Normal 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',
|
||||
];
|
||||
}
|
@ -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
229
depfile.yaml
Normal 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
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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'],
|
||||
];
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
104
system/Commands/Utilities/Publish.php
Normal file
104
system/Commands/Utilities/Publish.php
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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.
|
||||
*
|
||||
|
42
system/Config/Publisher.php
Normal file
42
system/Config/Publisher.php
Normal 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()
|
||||
{
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
||||
|
@ -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'];
|
||||
|
@ -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());
|
||||
|
||||
|
@ -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");
|
||||
|
@ -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'));
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -198,6 +198,10 @@ class Forge extends BaseForge
|
||||
$attributes['TYPE'] = 'TEXT';
|
||||
break;
|
||||
|
||||
case 'BOOLEAN':
|
||||
$attributes['TYPE'] = 'INT';
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -41,7 +41,7 @@ class Entity implements JsonSerializable
|
||||
*
|
||||
* Example:
|
||||
* $datamap = [
|
||||
* 'db_name' => 'class_name'
|
||||
* 'class_name' => 'db_name'
|
||||
* ];
|
||||
*/
|
||||
protected $datamap = [];
|
||||
|
25
system/Exceptions/TestException.php
Normal file
25
system/Exceptions/TestException.php
Normal 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]));
|
||||
}
|
||||
}
|
@ -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]));
|
||||
}
|
||||
}
|
||||
|
367
system/Files/FileCollection.php
Normal file
367
system/Files/FileCollection.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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.',
|
||||
];
|
||||
|
22
system/Language/en/Publisher.php
Normal file
22
system/Language/en/Publisher.php
Normal 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}!',
|
||||
];
|
15
system/Language/en/Test.php
Normal file
15
system/Language/en/Test.php
Normal 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',
|
||||
];
|
@ -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];
|
||||
|
||||
|
49
system/Publisher/Exceptions/PublisherException.php
Normal file
49
system/Publisher/Exceptions/PublisherException.php
Normal 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]));
|
||||
}
|
||||
}
|
438
system/Publisher/Publisher.php
Normal file
438
system/Publisher/Publisher.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -121,4 +121,18 @@ class Registrar
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Demonstrates Publisher security.
|
||||
*
|
||||
* @see PublisherRestrictionsTest::testRegistrarsNotAllowed()
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function Publisher()
|
||||
{
|
||||
return [
|
||||
'restrictions' => [SUPPORTPATH => '*'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
|
56
tests/_support/Publishers/TestPublisher.php
Normal file
56
tests/_support/Publishers/TestPublisher.php
Normal 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;
|
||||
}
|
||||
}
|
46
tests/system/Cache/CacheMockTest.php
Normal file
46
tests/system/Cache/CacheMockTest.php
Normal 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');
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
@ -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');
|
||||
|
@ -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');
|
||||
|
@ -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');
|
||||
|
@ -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');
|
||||
|
@ -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"');
|
||||
}
|
||||
}
|
||||
|
64
tests/system/Commands/PublishCommandTest.php
Normal file
64
tests/system/Commands/PublishCommandTest.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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') {
|
||||
|
@ -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()
|
||||
|
@ -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);
|
||||
});
|
||||
|
||||
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -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]);
|
||||
}
|
||||
|
556
tests/system/Files/FileCollectionTest.php
Normal file
556
tests/system/Files/FileCollectionTest.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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.
|
||||
*
|
||||
|
156
tests/system/Publisher/PublisherInputTest.php
Normal file
156
tests/system/Publisher/PublisherInputTest.php
Normal 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());
|
||||
}
|
||||
}
|
231
tests/system/Publisher/PublisherOutputTest.php
Normal file
231
tests/system/Publisher/PublisherOutputTest.php
Normal 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'));
|
||||
}
|
||||
}
|
120
tests/system/Publisher/PublisherRestrictionsTest.php
Normal file
120
tests/system/Publisher/PublisherRestrictionsTest.php
Normal 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,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
155
tests/system/Publisher/PublisherSupportTest.php
Normal file
155
tests/system/Publisher/PublisherSupportTest.php
Normal 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);
|
||||
}
|
||||
}
|
141
tests/system/Session/Handlers/DatabaseHandlerTest.php
Normal file
141
tests/system/Session/Handlers/DatabaseHandlerTest.php
Normal 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));
|
||||
}
|
||||
}
|
@ -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'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ See all the changes.
|
||||
.. toctree::
|
||||
:titlesonly:
|
||||
|
||||
v4.2.0
|
||||
v4.1.4
|
||||
v4.1.3
|
||||
v4.1.2
|
||||
|
19
user_guide_src/source/changelogs/v4.2.0.rst
Normal file
19
user_guide_src/source/changelogs/v4.2.0.rst
Normal 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:
|
@ -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
|
||||
|
@ -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
|
||||
==================
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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``
|
||||
|
@ -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::
|
||||
|
||||
|
@ -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
|
||||
================
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
12
user_guide_src/source/installation/upgrade_420.rst
Normal file
12
user_guide_src/source/installation/upgrade_420.rst
Normal 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)``.
|
||||
|
@ -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)``).
|
||||
|
@ -14,6 +14,7 @@ Library Reference
|
||||
honeypot
|
||||
images
|
||||
pagination
|
||||
publisher
|
||||
security
|
||||
sessions
|
||||
throttler
|
||||
|
410
user_guide_src/source/libraries/publisher.rst
Normal file
410
user_guide_src/source/libraries/publisher.rst
Normal 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();
|
@ -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';
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
53
user_guide_src/source/testing/mocking.rst
Normal file
53
user_guide_src/source/testing/mocking.rst
Normal 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);
|
Loading…
x
Reference in New Issue
Block a user