From 731a5e7514897d1b061171a040e32e0c55bd2eb0 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Sat, 10 Oct 2026 00:00:57 +0800 Subject: [PATCH 01/52] chore: fix release automation failures on the first run (#10610) --- .github/workflows/verify-release.yml | 2 +- tests/system/AutoReview/UpdateUpgradeGuideTest.php | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/verify-release.yml b/.github/workflows/verify-release.yml index 0488b76a1a70..7d6d2d8da681 100644 --- a/.github/workflows/verify-release.yml +++ b/.github/workflows/verify-release.yml @@ -76,7 +76,7 @@ jobs: php-version: '8.2' tools: composer extensions: intl, gd - coverage: none + coverage: xdebug - name: Wait for Packagist to serve the new framework version run: | diff --git a/tests/system/AutoReview/UpdateUpgradeGuideTest.php b/tests/system/AutoReview/UpdateUpgradeGuideTest.php index f982ac375dfa..bc7e2fc07cd3 100644 --- a/tests/system/AutoReview/UpdateUpgradeGuideTest.php +++ b/tests/system/AutoReview/UpdateUpgradeGuideTest.php @@ -75,7 +75,7 @@ public function testErrorWhenUpgradeGuideIsMissing(): void public function testDryRunPrintsSectionsWithoutWriting(): void { - $this->assertFileExists($this->upgradePath); + $this->skipIfUpgradeGuideIsMissing(); exec(sprintf('php ./admin/update-upgrade-guide.php %s --dry-run 2>&1', $this->nextVersion), $output, $exitCode); $outputString = implode("\n", $output); @@ -88,7 +88,7 @@ public function testDryRunPrintsSectionsWithoutWriting(): void public function testUpdatesUpgradeGuide(): void { - $this->assertFileExists($this->upgradePath); + $this->skipIfUpgradeGuideIsMissing(); if (trim((string) exec("git status --porcelain -- {$this->upgradePath}")) !== '') { $this->markTestSkipped('You have uncommitted changes to the upgrade guide that will be erased by this test.'); @@ -110,4 +110,14 @@ public function testUpdatesUpgradeGuide(): void exec("git restore -- {$this->upgradePath}"); } } + + private function skipIfUpgradeGuideIsMissing(): void + { + if (! is_file($this->upgradePath)) { + $this->markTestSkipped(sprintf( + '%s does not exist yet. It is created by "php admin/create-new-changelog.php" after a release.', + $this->upgradePath, + )); + } + } } From 7822cde17731b025b798c88975cb806d1513eebc Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Sat, 10 Oct 2026 00:26:32 +0800 Subject: [PATCH 02/52] docs: add changelog and upgrade for v4.7.6 (#10611) --- system/CodeIgniter.php | 2 +- user_guide_src/source/changelogs/index.rst | 1 + user_guide_src/source/changelogs/v4.7.6.rst | 35 ++++++++++++ .../source/installation/upgrade_476.rst | 55 +++++++++++++++++++ .../source/installation/upgrading.rst | 1 + 5 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 user_guide_src/source/changelogs/v4.7.6.rst create mode 100644 user_guide_src/source/installation/upgrade_476.rst diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index c3725f2388ad..380c8917d373 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -55,7 +55,7 @@ class CodeIgniter /** * The current version of CodeIgniter Framework */ - public const CI_VERSION = '4.7.5'; + public const CI_VERSION = '4.7.6-dev'; /** * App startup time. diff --git a/user_guide_src/source/changelogs/index.rst b/user_guide_src/source/changelogs/index.rst index 9b6030c49735..747fa78e7780 100644 --- a/user_guide_src/source/changelogs/index.rst +++ b/user_guide_src/source/changelogs/index.rst @@ -12,6 +12,7 @@ See all the changes. .. toctree:: :titlesonly: + v4.7.6 v4.7.5 v4.7.4 v4.7.3 diff --git a/user_guide_src/source/changelogs/v4.7.6.rst b/user_guide_src/source/changelogs/v4.7.6.rst new file mode 100644 index 000000000000..d2641f99b7cf --- /dev/null +++ b/user_guide_src/source/changelogs/v4.7.6.rst @@ -0,0 +1,35 @@ +############# +Version 4.7.6 +############# + +Release Date: Unreleased + +**4.7.6 release of CodeIgniter4** + +.. contents:: + :local: + :depth: 3 + +******** +BREAKING +******** + +*************** +Message Changes +*************** + +******* +Changes +******* + +************ +Deprecations +************ + +********** +Bugs Fixed +********** + +See the repo's +`CHANGELOG.md `_ +for a complete list of bugs fixed. diff --git a/user_guide_src/source/installation/upgrade_476.rst b/user_guide_src/source/installation/upgrade_476.rst new file mode 100644 index 000000000000..6e6e0118aa07 --- /dev/null +++ b/user_guide_src/source/installation/upgrade_476.rst @@ -0,0 +1,55 @@ +############################# +Upgrading from 4.7.5 to 4.7.6 +############################# + +Please refer to the upgrade instructions corresponding to your installation method. + +- :ref:`Composer Installation App Starter Upgrading ` +- :ref:`Composer Installation Adding CodeIgniter4 to an Existing Project Upgrading ` +- :ref:`Manual Installation Upgrading ` + +.. contents:: + :local: + :depth: 2 + +********************** +Mandatory File Changes +********************** + +**************** +Breaking Changes +**************** + +********************* +Breaking Enhancements +********************* + +************* +Project Files +************* + +Some files in the **project space** (root, app, public, writable) received updates. Due to +these files being outside of the **system** scope they will not be changed without your intervention. + +.. note:: There are some third-party CodeIgniter modules available to assist + with merging changes to the project space: + `Explore on Packagist `_. + +Content Changes +=============== + +The following files received significant changes (including deprecations or visual adjustments) +and it is recommended that you merge the updated versions with your application: + +Config +------ + +- @TODO + +All Changes +=========== + +This is a list of all files in the **project space** that received changes; +many will be simple comments or formatting that have no effect on the runtime: + +- @TODO diff --git a/user_guide_src/source/installation/upgrading.rst b/user_guide_src/source/installation/upgrading.rst index fbe8095a8f68..923e25c51470 100644 --- a/user_guide_src/source/installation/upgrading.rst +++ b/user_guide_src/source/installation/upgrading.rst @@ -22,6 +22,7 @@ Alternatively, replace it with a new file and add your previous lines. backward_compatibility_notes + upgrade_476 upgrade_475 upgrade_474 upgrade_473 From 9c6232378f686b34029c2e28a7c7151419da4df7 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Sat, 10 Oct 2026 05:45:46 +0800 Subject: [PATCH 03/52] chore: normalize Actions cache keys (#10614) --- .github/workflows/reusable-coveralls.yml | 6 +++--- .github/workflows/reusable-phpunit-test.yml | 7 +++---- .../reusable-serviceless-phpunit-test.yml | 6 +++--- .github/workflows/test-coding-standards.yml | 6 +++--- .github/workflows/test-phpstan.yml | 10 ++++++---- .github/workflows/test-psalm.yml | 10 ++++++---- .github/workflows/test-random-execution.yml | 5 +++-- .github/workflows/test-rector.yml | 16 +++++++++------- .github/workflows/test-structarmed.yml | 16 +++++++++------- 9 files changed, 45 insertions(+), 37 deletions(-) diff --git a/.github/workflows/reusable-coveralls.yml b/.github/workflows/reusable-coveralls.yml index cf9c02b22e11..ffd482fdf6f0 100644 --- a/.github/workflows/reusable-coveralls.yml +++ b/.github/workflows/reusable-coveralls.yml @@ -46,10 +46,10 @@ jobs: uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ steps.composer-cache.outputs.COMPOSER_CACHE_FILES_DIR }} - key: ${{ github.job }}-php-${{ inputs.php-version }}-${{ hashFiles('**/composer.*') }} + key: composer-php-${{ inputs.php-version }}-${{ hashFiles('**/composer.json') }} restore-keys: | - ${{ github.job }}-php-${{ inputs.php-version }}- - ${{ github.job }}- + composer-php-${{ inputs.php-version }}- + composer- - name: Cache PHPUnit's static analysis cache uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 diff --git a/.github/workflows/reusable-phpunit-test.yml b/.github/workflows/reusable-phpunit-test.yml index b216a71493bb..e21c3a65b671 100644 --- a/.github/workflows/reusable-phpunit-test.yml +++ b/.github/workflows/reusable-phpunit-test.yml @@ -195,11 +195,10 @@ jobs: uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ steps.setup-env.outputs.COMPOSER_CACHE_FILES_DIR }} - key: ${{ inputs.job-id || github.job }}-php-${{ inputs.php-version }}-db-${{ inputs.db-platform || 'none' }}-${{ hashFiles('**/composer.*') }} + key: composer-php-${{ inputs.php-version }}-${{ hashFiles('**/composer.json') }} restore-keys: | - ${{ inputs.job-id || github.job }}-php-${{ inputs.php-version }}-db-${{ inputs.db-platform || 'none' }}- - ${{ inputs.job-id || github.job }}-php-${{ inputs.php-version }}- - ${{ inputs.job-id || github.job }}- + composer-php-${{ inputs.php-version }}- + composer- - name: Cache PHPUnit's static analysis cache if: ${{ inputs.enable-artifact-upload }} diff --git a/.github/workflows/reusable-serviceless-phpunit-test.yml b/.github/workflows/reusable-serviceless-phpunit-test.yml index 8420d2b8c978..590c064cf022 100644 --- a/.github/workflows/reusable-serviceless-phpunit-test.yml +++ b/.github/workflows/reusable-serviceless-phpunit-test.yml @@ -92,10 +92,10 @@ jobs: uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ steps.setup-env.outputs.COMPOSER_CACHE_FILES_DIR }} - key: ${{ inputs.job-id || github.job }}-php-${{ inputs.php-version }}-${{ hashFiles('**/composer.*') }} + key: composer-php-${{ inputs.php-version }}-${{ hashFiles('**/composer.json') }} restore-keys: | - ${{ inputs.job-id || github.job }}-php-${{ inputs.php-version }}- - ${{ inputs.job-id || github.job }}- + composer-php-${{ inputs.php-version }}- + composer- - name: Cache PHPUnit's static analysis cache if: ${{ inputs.enable-artifact-upload }} diff --git a/.github/workflows/test-coding-standards.yml b/.github/workflows/test-coding-standards.yml index b02059733b13..16e0d5d745f6 100644 --- a/.github/workflows/test-coding-standards.yml +++ b/.github/workflows/test-coding-standards.yml @@ -52,10 +52,10 @@ jobs: uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ steps.composer-cache.outputs.COMPOSER_CACHE_FILES_DIR }} - key: ${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }} + key: composer-php-${{ matrix.php-version }}-${{ hashFiles('**/composer.json') }} restore-keys: | - ${{ runner.os }}-${{ matrix.php-version }}- - ${{ runner.os }}- + composer-php-${{ matrix.php-version }}- + composer- - name: Install dependencies run: composer update --ansi --no-interaction ${{ matrix.composer-option }} diff --git a/.github/workflows/test-phpstan.yml b/.github/workflows/test-phpstan.yml index cc246f871808..22813782d756 100644 --- a/.github/workflows/test-phpstan.yml +++ b/.github/workflows/test-phpstan.yml @@ -69,8 +69,10 @@ jobs: uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ steps.composer-cache.outputs.COMPOSER_CACHE_FILES_DIR }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ runner.os }}-composer- + key: composer-php-8.2-${{ hashFiles('**/composer.json') }} + restore-keys: | + composer-php-8.2- + composer- - name: Create PHPStan result cache directory run: mkdir -p build/phpstan @@ -79,8 +81,8 @@ jobs: uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: build/phpstan - key: ${{ runner.os }}-phpstan-${{ github.sha }} - restore-keys: ${{ runner.os }}-phpstan- + key: phpstan-${{ hashFiles('**/*.php', '**/*.neon', 'composer.json') }} + restore-keys: phpstan- - name: Install dependencies run: composer update --ansi --no-interaction diff --git a/.github/workflows/test-psalm.yml b/.github/workflows/test-psalm.yml index 3f385ce858fb..27aa0712f0c1 100644 --- a/.github/workflows/test-psalm.yml +++ b/.github/workflows/test-psalm.yml @@ -57,8 +57,10 @@ jobs: uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ steps.composer-cache.outputs.COMPOSER_CACHE_FILES_DIR }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}-${{ hashFiles('**/composer.lock') }} - restore-keys: ${{ runner.os }}-composer- + key: composer-php-${{ matrix.php-version }}-${{ hashFiles('**/composer.json') }} + restore-keys: | + composer-php-${{ matrix.php-version }}- + composer- - name: Create Psalm cache directory run: mkdir -p build/psalm @@ -67,8 +69,8 @@ jobs: uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: build/psalm - key: ${{ runner.os }}-psalm-${{ github.sha }} - restore-keys: ${{ runner.os }}-psalm- + key: psalm-${{ hashFiles('**/*.php', 'psalm*.xml', 'composer.json') }} + restore-keys: psalm- - name: Install dependencies run: | diff --git a/.github/workflows/test-random-execution.yml b/.github/workflows/test-random-execution.yml index e90de6bb031d..932c17b683d7 100644 --- a/.github/workflows/test-random-execution.yml +++ b/.github/workflows/test-random-execution.yml @@ -189,9 +189,10 @@ jobs: uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ steps.composer-cache.outputs.COMPOSER_CACHE_FILES_DIR }} - key: PHP_${{ matrix.php-version }}-${{ hashFiles('**/composer.*') }} + key: composer-php-${{ matrix.php-version }}-${{ hashFiles('**/composer.json') }} restore-keys: | - PHP_${{ matrix.php-version }}- + composer-php-${{ matrix.php-version }}- + composer- - name: Install project dependencies run: | diff --git a/.github/workflows/test-rector.yml b/.github/workflows/test-rector.yml index 2fa9b74c0e02..215670734064 100644 --- a/.github/workflows/test-rector.yml +++ b/.github/workflows/test-rector.yml @@ -75,20 +75,22 @@ jobs: uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ steps.composer-cache.outputs.COMPOSER_CACHE_FILES_DIR }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ runner.os }}-composer- - - - name: Install dependencies - run: composer update --ansi --no-interaction ${{ matrix.composer-option }} + key: composer-php-${{ matrix.php-version }}-${{ hashFiles('**/composer.json') }} + restore-keys: | + composer-php-${{ matrix.php-version }}- + composer- - name: Rector Cache uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: /tmp/rector - key: ${{ runner.os }}-rector-${{ github.run_id }} - restore-keys: ${{ runner.os }}-rector- + key: rector-${{ hashFiles('**/*.php', 'composer.json') }} + restore-keys: rector- - run: mkdir -p /tmp/rector + - name: Install dependencies + run: composer update --ansi --no-interaction ${{ matrix.composer-option }} + - name: Run static analysis run: vendor/bin/rector process --dry-run --no-progress-bar diff --git a/.github/workflows/test-structarmed.yml b/.github/workflows/test-structarmed.yml index 62a2dbc7d1cb..0deee13a46b0 100644 --- a/.github/workflows/test-structarmed.yml +++ b/.github/workflows/test-structarmed.yml @@ -72,20 +72,22 @@ jobs: uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ steps.composer-cache.outputs.COMPOSER_CACHE_FILES_DIR }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ runner.os }}-composer- - - - name: Install dependencies - run: composer update --ansi --no-interaction + key: composer-php-${{ matrix.php-version }}-${{ hashFiles('**/composer.json') }} + restore-keys: | + composer-php-${{ matrix.php-version }}- + composer- - name: Structarmed Cache uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: /tmp/structarmed - key: ${{ runner.os }}-structarmed-${{ github.run_id }} - restore-keys: ${{ runner.os }}-structarmed- + key: structarmed-${{ hashFiles('**/*.php', 'composer.json') }} + restore-keys: structarmed- - run: mkdir -p /tmp/structarmed + - name: Install dependencies + run: composer update --ansi --no-interaction + - name: Run static analysis run: vendor/bin/structarmed analyze From 8fd3fdf73ff9e0711017bfa1382b629d5025a10b Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Mon, 12 Oct 2026 10:24:01 +0200 Subject: [PATCH 04/52] docs: document CSRF in JSON request body limitations (#10619) * fix: preserve JSON body formatting when removing CSRF token * docs: document CSRF in JSON request body limitations --- user_guide_src/source/libraries/security.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/user_guide_src/source/libraries/security.rst b/user_guide_src/source/libraries/security.rst index 8fc49d646b1a..2e747c988ec1 100644 --- a/user_guide_src/source/libraries/security.rst +++ b/user_guide_src/source/libraries/security.rst @@ -224,6 +224,13 @@ The order of checking the availability of the CSRF token is as follows: .. note:: ``php://input`` (raw body) is checked since v4.4.2. +.. note:: + + If you include the CSRF token in a JSON object, removing it decodes and + re-encodes the body. This changes its formatting and may affect number + precision. To keep the JSON body unchanged, send the token in the + ``X-CSRF-TOKEN`` header and leave the CSRF field out of the body. + ********************* Other Helpful Methods ********************* From 0361d921117da2c4d268d9169d8830768cc1eaac Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Mon, 12 Oct 2026 10:24:33 +0200 Subject: [PATCH 05/52] fix: preserve JSON request body when CSRF token is not in it (#10620) * fix: preserve JSON request body when CSRF token is not in it * refactor: optimize CSRF token removal in request body by restructuring JSON and form-encoded checks --- system/Security/Security.php | 23 +++++++------- tests/system/Security/SecurityTest.php | 35 +++++++++++++++++++++ user_guide_src/source/changelogs/v4.7.6.rst | 4 +++ 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/system/Security/Security.php b/system/Security/Security.php index 4ac0de3f8ff8..583a26593976 100644 --- a/system/Security/Security.php +++ b/system/Security/Security.php @@ -295,23 +295,24 @@ private function removeTokenInRequest(IncomingRequest $request): void try { $json = json_decode($body, flags: JSON_THROW_ON_ERROR); } catch (JsonException) { - $json = null; - } + // The body is not valid JSON - check for form-encoded data. + parse_str($body, $result); - if (is_object($json)) { - if (property_exists($json, $tokenName)) { - unset($json->{$tokenName}); - $request->setBody(json_encode($json)); + if (! array_key_exists($tokenName, $result)) { + return; } + unset($result[$tokenName]); + $request->setBody(http_build_query($result)); + return; } - // If the token is found in form-encoded data, we can safely remove it. - parse_str($body, $result); - - unset($result[$tokenName]); - $request->setBody(http_build_query($result)); + // Valid JSON never falls through to form-encoded processing. + if (is_object($json) && property_exists($json, $tokenName)) { + unset($json->{$tokenName}); + $request->setBody(json_encode($json)); + } } private function getPostedToken(IncomingRequest $request): ?string diff --git a/tests/system/Security/SecurityTest.php b/tests/system/Security/SecurityTest.php index db80d91cbd92..0b8906ee8d34 100644 --- a/tests/system/Security/SecurityTest.php +++ b/tests/system/Security/SecurityTest.php @@ -237,6 +237,41 @@ public function testCsrfVerifyHeaderWithJsonBodyStripsTokenFromBody(): void $this->assertSame('{"foo":"bar"}', $request->getBody()); } + #[DataProvider('provideCsrfVerifyPreservesNonObjectJsonBody')] + public function testCsrfVerifyPreservesNonObjectJsonBody(string $body): void + { + service('superglobals') + ->setServer('REQUEST_METHOD', 'POST') + ->setCookie('csrf_cookie_name', self::CORRECT_CSRF_HASH); + + $security = $this->createMockSecurity(); + $request = $this->createIncomingRequest(); + + $request->setHeader('X-CSRF-TOKEN', self::CORRECT_CSRF_HASH); + $request->setBody($body); + + $this->assertInstanceOf(Security::class, $security->verify($request)); + $this->assertSame($body, $request->getBody()); + } + + /** + * @return iterable + */ + public static function provideCsrfVerifyPreservesNonObjectJsonBody(): iterable + { + yield 'JSON array' => ['[{"foo":"bar"}]']; + + yield 'JSON array of scalars' => ['[1, 2, 3]']; + + yield 'JSON number' => ['123']; + + yield 'JSON boolean' => ['true']; + + yield 'JSON null' => ['null']; + + yield 'JSON string' => ['"foo"']; + } + public function testCsrfVerifyPutBodyThrowsExceptionOnNoMatch(): void { service('superglobals') diff --git a/user_guide_src/source/changelogs/v4.7.6.rst b/user_guide_src/source/changelogs/v4.7.6.rst index d2641f99b7cf..04eb1be1d52b 100644 --- a/user_guide_src/source/changelogs/v4.7.6.rst +++ b/user_guide_src/source/changelogs/v4.7.6.rst @@ -30,6 +30,10 @@ Deprecations Bugs Fixed ********** +- **Security:** Fixed a bug where a valid JSON request body that is not an object + (for example a JSON array) was corrupted after CSRF verification when the + token was sent via the ``X-CSRF-TOKEN`` header. + See the repo's `CHANGELOG.md `_ for a complete list of bugs fixed. From fceba62e643ee49ce82977382b44a198014d0c75 Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Mon, 12 Oct 2026 13:48:15 +0200 Subject: [PATCH 06/52] docs: clarify hostname route matching includes the port (#10568) --- user_guide_src/source/incoming/routing.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/user_guide_src/source/incoming/routing.rst b/user_guide_src/source/incoming/routing.rst index 7dda8b687131..07e1cd0df1b8 100644 --- a/user_guide_src/source/incoming/routing.rst +++ b/user_guide_src/source/incoming/routing.rst @@ -476,6 +476,15 @@ by passing the "hostname" option along with the desired domain to allow it on as This example would only allow the specified hosts to work if the domain exactly matched **accounts.example.com**. It would not work under the main site at **example.com**. +.. note:: + + The ``hostname`` option is compared against the full ``HTTP_HOST`` value of the + request, including the port when one is present. When your application is served + on a non-standard port (for example ``localhost:8080``, behind Docker, or when + using ``php spark serve``), you must include the port in the ``hostname`` option for + the routes to match. For example, ``['hostname' => 'localhost:8080']`` matches only + requests to ``localhost:8080``. + Restrict by Multiple Hostnames ------------------------------ From b7e115beb9ffdb49d31ae0b1e41ae5315fbe0f0d Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Tue, 13 Oct 2026 14:57:05 +0800 Subject: [PATCH 07/52] fix: sort generator imports in templates with CRLF line endings (#10622) --- system/CLI/GeneratorTrait.php | 7 ++++--- tests/_support/Commands/LanguageCommand.php | 5 ++++- .../Commands/ConfigurableSortImportsTest.php | 19 +++++++++++++++++++ user_guide_src/source/changelogs/v4.7.6.rst | 1 + 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/system/CLI/GeneratorTrait.php b/system/CLI/GeneratorTrait.php index 45e72c7bda71..c4b439cdc563 100644 --- a/system/CLI/GeneratorTrait.php +++ b/system/CLI/GeneratorTrait.php @@ -399,15 +399,16 @@ protected function buildContent(string $class): string if ( $this->sortImports && preg_match( - '/(?P(?:^use [^;]+;$\n?)+)/m', + '/(?P(?:^use [^;]+;\r?$\n?)+)/m', $template, $match, ) ) { - $imports = explode("\n", trim($match['imports'])); + $eol = str_contains($match['imports'], "\r\n") ? "\r\n" : "\n"; + $imports = explode("\n", str_replace("\r\n", "\n", trim($match['imports']))); sort($imports); - return str_replace(trim($match['imports']), implode("\n", $imports), $template); + return str_replace(trim($match['imports']), implode($eol, $imports), $template); } return $template; diff --git a/tests/_support/Commands/LanguageCommand.php b/tests/_support/Commands/LanguageCommand.php index 17fba1881c94..c8a48325ae41 100644 --- a/tests/_support/Commands/LanguageCommand.php +++ b/tests/_support/Commands/LanguageCommand.php @@ -27,6 +27,7 @@ class LanguageCommand extends BaseCommand protected $options = [ '--lang' => 'The language folder to save the file.', '--sort' => 'Turn on/off the sortImports flag.', + '--crlf' => 'Use CRLF line endings in the template.', ]; public function run(array $params): void @@ -46,6 +47,8 @@ public function run(array $params): void protected function prepare(string $class): string { - return file_get_contents(__DIR__ . '/Foobar.php') ?: ''; + $template = file_get_contents(__DIR__ . '/Foobar.php') ?: ''; + + return $this->getOption('crlf') === true ? str_replace("\n", "\r\n", $template) : $template; } } diff --git a/tests/system/Commands/ConfigurableSortImportsTest.php b/tests/system/Commands/ConfigurableSortImportsTest.php index 4f54390d7ffe..a86cd66a420a 100644 --- a/tests/system/Commands/ConfigurableSortImportsTest.php +++ b/tests/system/Commands/ConfigurableSortImportsTest.php @@ -55,6 +55,25 @@ public function testEnabledSortImportsWillDisruptLanguageFilePublish(): void } } + public function testSortImportsKeepsCrlfLineEndings(): void + { + command('publish:language --lang fr --crlf'); + + $file = APPPATH . 'Language/fr/Foobar.php'; + $this->assertFileExists($file); + $this->assertStringContainsString( + "\r\n\r\nuse CodeIgniter\\CLI\\CLI;\r\nuse Config\\App;\r\n\r\n", + (string) file_get_contents($file), + ); + if (is_file($file)) { + unlink($file); + } + $dir = dirname($file); + if (is_dir($dir)) { + rmdir($dir); + } + } + public function testDisabledSortImportsWillNotAffectLanguageFilesPublish(): void { command('publish:language --lang ar --sort off'); diff --git a/user_guide_src/source/changelogs/v4.7.6.rst b/user_guide_src/source/changelogs/v4.7.6.rst index 04eb1be1d52b..360221806c4f 100644 --- a/user_guide_src/source/changelogs/v4.7.6.rst +++ b/user_guide_src/source/changelogs/v4.7.6.rst @@ -30,6 +30,7 @@ Deprecations Bugs Fixed ********** +- **Commands:** Fixed a bug where generator commands did not sort the imports of a template with CRLF line endings. - **Security:** Fixed a bug where a valid JSON request body that is not an object (for example a JSON array) was corrupted after CSRF verification when the token was sent via the ``X-CSRF-TOKEN`` header. From e0e262a7e840c8857e6d6cfa53a3c0d308e4c8fb Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Tue, 13 Oct 2026 15:00:23 +0800 Subject: [PATCH 08/52] refactor: remove `@see` test annotations in source (#10624) --- system/Autoloader/Autoloader.php | 2 -- system/Autoloader/FileLocator.php | 2 -- system/Autoloader/FileLocatorCached.php | 2 -- system/CLI/CLI.php | 2 -- system/CLI/Console.php | 2 -- system/Cache/CacheFactory.php | 2 -- system/Cache/Handlers/ApcuHandler.php | 2 -- system/Cache/Handlers/BaseHandler.php | 2 -- system/Cache/Handlers/DummyHandler.php | 2 -- system/Cache/Handlers/FileHandler.php | 2 -- system/Cache/Handlers/MemcachedHandler.php | 2 -- system/Cache/Handlers/PredisHandler.php | 2 -- system/Cache/Handlers/RedisHandler.php | 2 -- system/Cache/ResponseCache.php | 2 -- system/CodeIgniter.php | 2 -- system/Commands/Database/MigrateStatus.php | 2 -- system/Commands/Database/ShowTableInfo.php | 2 -- system/Commands/Translation/LocalizationFinder.php | 3 --- system/Commands/Translation/LocalizationSync.php | 3 --- system/Commands/Utilities/ConfigCheck.php | 2 -- system/Commands/Utilities/Namespaces.php | 2 -- system/Commands/Utilities/Routes/AutoRouteCollector.php | 2 -- .../Routes/AutoRouterImproved/AutoRouteCollector.php | 2 -- .../Routes/AutoRouterImproved/ControllerMethodReader.php | 2 -- system/Commands/Utilities/Routes/ControllerFinder.php | 2 -- system/Commands/Utilities/Routes/ControllerMethodReader.php | 2 -- system/Commands/Utilities/Routes/FilterCollector.php | 2 -- system/Commands/Utilities/Routes/FilterFinder.php | 2 -- system/Commands/Utilities/Routes/SampleURIGenerator.php | 2 -- system/Config/BaseConfig.php | 1 - system/Config/DotEnv.php | 2 -- system/Config/Factories.php | 1 - system/Config/Services.php | 1 - system/Controller.php | 3 --- system/Cookie/Cookie.php | 1 - system/Cookie/CookieStore.php | 1 - system/DataConverter/DataConverter.php | 2 -- system/Database/BaseConnection.php | 1 - system/Database/Config.php | 3 --- system/Database/RawSql.php | 3 --- system/Database/TableName.php | 2 -- system/Debug/ExceptionHandler.php | 3 --- system/Debug/Exceptions.php | 2 -- system/Debug/Timer.php | 2 -- system/Debug/Toolbar/Collectors/Database.php | 2 -- system/Debug/Toolbar/Collectors/History.php | 2 -- system/Email/Email.php | 2 -- system/Encryption/Encryption.php | 2 -- system/Encryption/Handlers/OpenSSLHandler.php | 2 -- system/Encryption/Handlers/SodiumHandler.php | 1 - system/Entity/Entity.php | 2 -- system/Events/Events.php | 2 -- system/Files/File.php | 2 -- system/Files/FileCollection.php | 1 - system/Filters/CSRF.php | 1 - system/Filters/Cors.php | 3 --- system/Filters/DebugToolbar.php | 2 -- system/Filters/Filters.php | 2 -- system/Filters/Honeypot.php | 2 -- system/Filters/InvalidChars.php | 2 -- system/Filters/SecureHeaders.php | 2 -- system/Format/Format.php | 2 -- system/Format/JSONFormatter.php | 2 -- system/Format/XMLFormatter.php | 2 -- system/HTTP/CLIRequest.php | 2 -- system/HTTP/CURLRequest.php | 2 -- system/HTTP/ContentSecurityPolicy.php | 1 - system/HTTP/Cors.php | 2 -- system/HTTP/DownloadResponse.php | 2 -- system/HTTP/Files/FileCollection.php | 2 -- system/HTTP/Header.php | 2 -- system/HTTP/IncomingRequest.php | 2 -- system/HTTP/Message.php | 2 -- system/HTTP/Negotiate.php | 1 - system/HTTP/OutgoingRequest.php | 2 -- system/HTTP/RedirectResponse.php | 2 -- system/HTTP/Request.php | 2 -- system/HTTP/Response.php | 2 -- system/HTTP/SiteURI.php | 2 -- system/HTTP/SiteURIFactory.php | 2 -- system/HTTP/URI.php | 2 -- system/HTTP/UserAgent.php | 2 -- system/Helpers/Array/ArrayHelper.php | 4 ---- system/Honeypot/Honeypot.php | 2 -- system/HotReloader/DirectoryHasher.php | 1 - system/I18n/Time.php | 2 -- system/I18n/TimeDifference.php | 2 -- system/I18n/TimeLegacy.php | 1 - system/Images/Image.php | 2 -- system/Language/Language.php | 2 -- system/Log/Handlers/ChromeLoggerHandler.php | 1 - system/Log/Handlers/ErrorlogHandler.php | 2 -- system/Log/Logger.php | 2 -- system/Pager/Pager.php | 2 -- system/Pager/PagerRenderer.php | 2 -- system/Publisher/ContentReplacer.php | 2 -- system/RESTful/ResourceController.php | 2 -- system/RESTful/ResourcePresenter.php | 2 -- system/Router/AutoRouterImproved.php | 2 -- system/Router/DefinedRouteCollector.php | 2 -- system/Router/RouteCollection.php | 1 - system/Router/Router.php | 2 -- system/Security/CheckPhpIni.php | 1 - system/Security/Security.php | 2 -- system/Session/Handlers/Database/MySQLiHandler.php | 2 -- system/Session/Handlers/Database/PostgreHandler.php | 2 -- system/Session/Session.php | 2 -- system/Superglobals.php | 1 - system/Test/DOMParser.php | 2 -- system/Test/Fabricator.php | 2 -- system/Test/TestLogger.php | 3 --- system/Test/TestResponse.php | 2 -- system/Typography/Typography.php | 2 -- system/Validation/CreditCardRules.php | 1 - system/Validation/DotArrayFilter.php | 3 --- system/Validation/FileRules.php | 2 -- system/Validation/FormatRules.php | 2 -- system/Validation/Rules.php | 2 -- system/Validation/StrictRules/CreditCardRules.php | 1 - system/Validation/StrictRules/FileRules.php | 2 -- system/Validation/StrictRules/FormatRules.php | 2 -- system/Validation/StrictRules/Rules.php | 2 -- system/Validation/Validation.php | 2 -- system/View/Cell.php | 2 -- system/View/Parser.php | 2 -- system/View/Table.php | 2 -- system/View/View.php | 2 -- 127 files changed, 246 deletions(-) diff --git a/system/Autoloader/Autoloader.php b/system/Autoloader/Autoloader.php index 92ff2026bb19..3d7068aa2a82 100644 --- a/system/Autoloader/Autoloader.php +++ b/system/Autoloader/Autoloader.php @@ -60,8 +60,6 @@ * // register the autoloader * $loader->register(); * ``` - * - * @see \CodeIgniter\Autoloader\AutoloaderTest */ class Autoloader { diff --git a/system/Autoloader/FileLocator.php b/system/Autoloader/FileLocator.php index 50015b5b7341..845f4353f20a 100644 --- a/system/Autoloader/FileLocator.php +++ b/system/Autoloader/FileLocator.php @@ -16,8 +16,6 @@ /** * Allows loading non-class files in a namespaced manner. * Works with Helpers, Views, etc. - * - * @see \CodeIgniter\Autoloader\FileLocatorTest */ class FileLocator implements FileLocatorInterface { diff --git a/system/Autoloader/FileLocatorCached.php b/system/Autoloader/FileLocatorCached.php index ee1bb1577433..0690a6af7b66 100644 --- a/system/Autoloader/FileLocatorCached.php +++ b/system/Autoloader/FileLocatorCached.php @@ -18,8 +18,6 @@ /** * FileLocator with Cache - * - * @see \CodeIgniter\Autoloader\FileLocatorCachedTest */ final class FileLocatorCached implements FileLocatorInterface { diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index 314b6a934ee1..3fca34c3cdd2 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -27,8 +27,6 @@ * Some of the code in this class is Windows-specific, and not * possible to test using travis-ci. It has been phpunit-annotated * to prevent messing up code coverage. - * - * @see \CodeIgniter\CLI\CLITest */ class CLI { diff --git a/system/CLI/Console.php b/system/CLI/Console.php index 82c31d60f7c5..001b2bc3cf87 100644 --- a/system/CLI/Console.php +++ b/system/CLI/Console.php @@ -20,8 +20,6 @@ /** * Console - * - * @see \CodeIgniter\CLI\ConsoleTest */ class Console { diff --git a/system/Cache/CacheFactory.php b/system/Cache/CacheFactory.php index b0347ffa2a93..d161c23f3987 100644 --- a/system/Cache/CacheFactory.php +++ b/system/Cache/CacheFactory.php @@ -20,8 +20,6 @@ /** * A factory for loading the desired - * - * @see \CodeIgniter\Cache\CacheFactoryTest */ class CacheFactory { diff --git a/system/Cache/Handlers/ApcuHandler.php b/system/Cache/Handlers/ApcuHandler.php index ef0f51c50dc7..ef82d68afe03 100644 --- a/system/Cache/Handlers/ApcuHandler.php +++ b/system/Cache/Handlers/ApcuHandler.php @@ -20,8 +20,6 @@ /** * APCu cache handler - * - * @see \CodeIgniter\Cache\Handlers\ApcuHandlerTest */ class ApcuHandler extends BaseHandler { diff --git a/system/Cache/Handlers/BaseHandler.php b/system/Cache/Handlers/BaseHandler.php index 7cf048c6a92b..9f8c22a1964d 100644 --- a/system/Cache/Handlers/BaseHandler.php +++ b/system/Cache/Handlers/BaseHandler.php @@ -20,8 +20,6 @@ /** * Base class for cache handling - * - * @see \CodeIgniter\Cache\Handlers\BaseHandlerTest */ abstract class BaseHandler implements CacheInterface { diff --git a/system/Cache/Handlers/DummyHandler.php b/system/Cache/Handlers/DummyHandler.php index a30475965d6f..0a4d1d3aad57 100644 --- a/system/Cache/Handlers/DummyHandler.php +++ b/system/Cache/Handlers/DummyHandler.php @@ -17,8 +17,6 @@ /** * Dummy cache handler - * - * @see \CodeIgniter\Cache\Handlers\DummyHandlerTest */ class DummyHandler extends BaseHandler { diff --git a/system/Cache/Handlers/FileHandler.php b/system/Cache/Handlers/FileHandler.php index adc31c8f447c..18ff5909e6f1 100644 --- a/system/Cache/Handlers/FileHandler.php +++ b/system/Cache/Handlers/FileHandler.php @@ -20,8 +20,6 @@ /** * File system cache handler - * - * @see \CodeIgniter\Cache\Handlers\FileHandlerTest */ class FileHandler extends BaseHandler { diff --git a/system/Cache/Handlers/MemcachedHandler.php b/system/Cache/Handlers/MemcachedHandler.php index 3633dc8e40da..7a4316ba5468 100644 --- a/system/Cache/Handlers/MemcachedHandler.php +++ b/system/Cache/Handlers/MemcachedHandler.php @@ -23,8 +23,6 @@ /** * Mamcached cache handler - * - * @see \CodeIgniter\Cache\Handlers\MemcachedHandlerTest */ class MemcachedHandler extends BaseHandler { diff --git a/system/Cache/Handlers/PredisHandler.php b/system/Cache/Handlers/PredisHandler.php index c868f34550e9..79e7ae98f916 100644 --- a/system/Cache/Handlers/PredisHandler.php +++ b/system/Cache/Handlers/PredisHandler.php @@ -23,8 +23,6 @@ /** * Predis cache handler - * - * @see \CodeIgniter\Cache\Handlers\PredisHandlerTest */ class PredisHandler extends BaseHandler { diff --git a/system/Cache/Handlers/RedisHandler.php b/system/Cache/Handlers/RedisHandler.php index 05cae32da440..ccbb29bcd032 100644 --- a/system/Cache/Handlers/RedisHandler.php +++ b/system/Cache/Handlers/RedisHandler.php @@ -21,8 +21,6 @@ /** * Redis cache handler - * - * @see \CodeIgniter\Cache\Handlers\RedisHandlerTest */ class RedisHandler extends BaseHandler { diff --git a/system/Cache/ResponseCache.php b/system/Cache/ResponseCache.php index 7f0ea9e1da94..c056c3a2e454 100644 --- a/system/Cache/ResponseCache.php +++ b/system/Cache/ResponseCache.php @@ -22,8 +22,6 @@ /** * Web Page Caching - * - * @see \CodeIgniter\Cache\ResponseCacheTest */ final class ResponseCache { diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 380c8917d373..18847fcb67a8 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -47,8 +47,6 @@ * This class is the core of the framework, and will analyse the * request, route it to a controller, and send back the response. * Of course, there are variations to that flow, but this is the brains. - * - * @see \CodeIgniter\CodeIgniterTest */ class CodeIgniter { diff --git a/system/Commands/Database/MigrateStatus.php b/system/Commands/Database/MigrateStatus.php index 7307305307c1..e7f841808fe3 100644 --- a/system/Commands/Database/MigrateStatus.php +++ b/system/Commands/Database/MigrateStatus.php @@ -18,8 +18,6 @@ /** * Displays a list of all migrations and whether they've been run or not. - * - * @see \CodeIgniter\Commands\Database\MigrateStatusTest */ class MigrateStatus extends BaseCommand { diff --git a/system/Commands/Database/ShowTableInfo.php b/system/Commands/Database/ShowTableInfo.php index d05159b1b10a..f071076fcc58 100644 --- a/system/Commands/Database/ShowTableInfo.php +++ b/system/Commands/Database/ShowTableInfo.php @@ -22,8 +22,6 @@ /** * Get table data if it exists in the database. - * - * @see \CodeIgniter\Commands\Database\ShowTableInfoTest */ class ShowTableInfo extends BaseCommand { diff --git a/system/Commands/Translation/LocalizationFinder.php b/system/Commands/Translation/LocalizationFinder.php index 8fed993a6ecc..6bd44d9eaa3b 100644 --- a/system/Commands/Translation/LocalizationFinder.php +++ b/system/Commands/Translation/LocalizationFinder.php @@ -22,9 +22,6 @@ use RecursiveIteratorIterator; use SplFileInfo; -/** - * @see \CodeIgniter\Commands\Translation\LocalizationFinderTest - */ class LocalizationFinder extends BaseCommand { protected $group = 'Translation'; diff --git a/system/Commands/Translation/LocalizationSync.php b/system/Commands/Translation/LocalizationSync.php index f54df45c364c..9e505db01619 100644 --- a/system/Commands/Translation/LocalizationSync.php +++ b/system/Commands/Translation/LocalizationSync.php @@ -24,9 +24,6 @@ use RecursiveIteratorIterator; use SplFileInfo; -/** - * @see \CodeIgniter\Commands\Translation\LocalizationSyncTest - */ class LocalizationSync extends BaseCommand { protected $group = 'Translation'; diff --git a/system/Commands/Utilities/ConfigCheck.php b/system/Commands/Utilities/ConfigCheck.php index 4583c67dae87..a99daa19c2b4 100644 --- a/system/Commands/Utilities/ConfigCheck.php +++ b/system/Commands/Utilities/ConfigCheck.php @@ -22,8 +22,6 @@ /** * Check the Config values. - * - * @see \CodeIgniter\Commands\Utilities\ConfigCheckTest */ final class ConfigCheck extends BaseCommand { diff --git a/system/Commands/Utilities/Namespaces.php b/system/Commands/Utilities/Namespaces.php index 51bc045759ff..08e68d4f9891 100644 --- a/system/Commands/Utilities/Namespaces.php +++ b/system/Commands/Utilities/Namespaces.php @@ -21,8 +21,6 @@ * Lists namespaces set in Config\Autoload with their * full server path. Helps you to verify that you have * the namespaces setup correctly. - * - * @see \CodeIgniter\Commands\Utilities\NamespacesTest */ class Namespaces extends BaseCommand { diff --git a/system/Commands/Utilities/Routes/AutoRouteCollector.php b/system/Commands/Utilities/Routes/AutoRouteCollector.php index 3c8eb44279c7..8c812108aa3d 100644 --- a/system/Commands/Utilities/Routes/AutoRouteCollector.php +++ b/system/Commands/Utilities/Routes/AutoRouteCollector.php @@ -15,8 +15,6 @@ /** * Collects data for auto route listing. - * - * @see \CodeIgniter\Commands\Utilities\Routes\AutoRouteCollectorTest */ final readonly class AutoRouteCollector { diff --git a/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php b/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php index a19bd5f24188..3e28f7a4ebec 100644 --- a/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php +++ b/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php @@ -18,8 +18,6 @@ /** * Collects data for Auto Routing Improved. - * - * @see \CodeIgniter\Commands\Utilities\Routes\AutoRouterImproved\AutoRouteCollectorTest */ final readonly class AutoRouteCollector { diff --git a/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php b/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php index 0863388b8af5..d81ba0e8aeb0 100644 --- a/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php +++ b/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php @@ -19,8 +19,6 @@ /** * Reads a controller and returns a list of auto route listing. - * - * @see \CodeIgniter\Commands\Utilities\Routes\AutoRouterImproved\ControllerMethodReaderTest */ final readonly class ControllerMethodReader { diff --git a/system/Commands/Utilities/Routes/ControllerFinder.php b/system/Commands/Utilities/Routes/ControllerFinder.php index 103a7846ab52..1d91cd3458bb 100644 --- a/system/Commands/Utilities/Routes/ControllerFinder.php +++ b/system/Commands/Utilities/Routes/ControllerFinder.php @@ -17,8 +17,6 @@ /** * Finds all controllers in a namespace for auto route listing. - * - * @see \CodeIgniter\Commands\Utilities\Routes\ControllerFinderTest */ final readonly class ControllerFinder { diff --git a/system/Commands/Utilities/Routes/ControllerMethodReader.php b/system/Commands/Utilities/Routes/ControllerMethodReader.php index aa5cddaf48d1..79f293a41679 100644 --- a/system/Commands/Utilities/Routes/ControllerMethodReader.php +++ b/system/Commands/Utilities/Routes/ControllerMethodReader.php @@ -18,8 +18,6 @@ /** * Reads a controller and returns a list of auto route listing. - * - * @see \CodeIgniter\Commands\Utilities\Routes\ControllerMethodReaderTest */ final readonly class ControllerMethodReader { diff --git a/system/Commands/Utilities/Routes/FilterCollector.php b/system/Commands/Utilities/Routes/FilterCollector.php index ec1219debe1a..ea2ed2d7ddde 100644 --- a/system/Commands/Utilities/Routes/FilterCollector.php +++ b/system/Commands/Utilities/Routes/FilterCollector.php @@ -21,8 +21,6 @@ /** * Collects filters for a route. - * - * @see \CodeIgniter\Commands\Utilities\Routes\FilterCollectorTest */ final readonly class FilterCollector { diff --git a/system/Commands/Utilities/Routes/FilterFinder.php b/system/Commands/Utilities/Routes/FilterFinder.php index 6477e1d13300..89aad6584745 100644 --- a/system/Commands/Utilities/Routes/FilterFinder.php +++ b/system/Commands/Utilities/Routes/FilterFinder.php @@ -22,8 +22,6 @@ /** * Finds filters. - * - * @see \CodeIgniter\Commands\Utilities\Routes\FilterFinderTest */ final readonly class FilterFinder { diff --git a/system/Commands/Utilities/Routes/SampleURIGenerator.php b/system/Commands/Utilities/Routes/SampleURIGenerator.php index a0e5f09bc2cb..1d6eba31f416 100644 --- a/system/Commands/Utilities/Routes/SampleURIGenerator.php +++ b/system/Commands/Utilities/Routes/SampleURIGenerator.php @@ -18,8 +18,6 @@ /** * Generate a sample URI path from route key regex. - * - * @see \CodeIgniter\Commands\Utilities\Routes\SampleURIGeneratorTest */ final class SampleURIGenerator { diff --git a/system/Config/BaseConfig.php b/system/Config/BaseConfig.php index 208797ce6d1c..7b7d43c27fdf 100644 --- a/system/Config/BaseConfig.php +++ b/system/Config/BaseConfig.php @@ -29,7 +29,6 @@ * These can be set within the .env file. * * @phpstan-consistent-constructor - * @see \CodeIgniter\Config\BaseConfigTest */ class BaseConfig { diff --git a/system/Config/DotEnv.php b/system/Config/DotEnv.php index a2f15e7c4850..ef02b555f9d0 100644 --- a/system/Config/DotEnv.php +++ b/system/Config/DotEnv.php @@ -17,8 +17,6 @@ /** * Environment-specific configuration - * - * @see \CodeIgniter\Config\DotEnvTest */ class DotEnv { diff --git a/system/Config/Factories.php b/system/Config/Factories.php index 1adbafc48e7b..5831862be8f6 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -29,7 +29,6 @@ * @method static object|null cells(string $alias, array $options = []) * @method static BaseConfig|null config(...$arguments) * @method static Model|null models(string $alias, array $options = [], ?ConnectionInterface &$conn = null) - * @see \CodeIgniter\Config\FactoriesTest */ final class Factories { diff --git a/system/Config/Services.php b/system/Config/Services.php index f5a11ac19a80..9c30ee4677c4 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -108,7 +108,6 @@ * * @see http://blog.ircmaxell.com/2015/11/simple-easy-risk-and-change.html * @see http://www.infoq.com/presentations/Simple-Made-Easy - * @see \CodeIgniter\Config\ServicesTest */ class Services extends BaseService { diff --git a/system/Controller.php b/system/Controller.php index 2fdb5d5a59a6..6a2f645ab951 100644 --- a/system/Controller.php +++ b/system/Controller.php @@ -24,9 +24,6 @@ use Config\Validation; use Psr\Log\LoggerInterface; -/** - * @see \CodeIgniter\ControllerTest - */ class Controller { /** diff --git a/system/Cookie/Cookie.php b/system/Cookie/Cookie.php index da95f18805d4..9c253530311f 100644 --- a/system/Cookie/Cookie.php +++ b/system/Cookie/Cookie.php @@ -40,7 +40,6 @@ * ``` * * @template-implements ArrayAccess - * @see \CodeIgniter\Cookie\CookieTest */ class Cookie implements ArrayAccess, CloneableCookieInterface { diff --git a/system/Cookie/CookieStore.php b/system/Cookie/CookieStore.php index ece407e98d80..aaea3d1f0301 100644 --- a/system/Cookie/CookieStore.php +++ b/system/Cookie/CookieStore.php @@ -23,7 +23,6 @@ * The CookieStore object represents an immutable collection of `Cookie` value objects. * * @implements IteratorAggregate - * @see \CodeIgniter\Cookie\CookieStoreTest */ class CookieStore implements Countable, IteratorAggregate { diff --git a/system/DataConverter/DataConverter.php b/system/DataConverter/DataConverter.php index a1b5db4f0ef9..123e1eefe72b 100644 --- a/system/DataConverter/DataConverter.php +++ b/system/DataConverter/DataConverter.php @@ -21,8 +21,6 @@ * PHP data <==> DataSource data converter * * @template TEntity of object - * - * @see \CodeIgniter\DataConverter\DataConverterTest */ final readonly class DataConverter { diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index 1db7618e2971..002cf661226d 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -59,7 +59,6 @@ * @template TResult * * @implements ConnectionInterface - * @see \CodeIgniter\Database\BaseConnectionTest */ abstract class BaseConnection implements ConnectionInterface { diff --git a/system/Database/Config.php b/system/Database/Config.php index 250af22acd62..79b8bcaa6447 100644 --- a/system/Database/Config.php +++ b/system/Database/Config.php @@ -17,9 +17,6 @@ use CodeIgniter\Exceptions\InvalidArgumentException; use Config\Database as DbConfig; -/** - * @see \CodeIgniter\Database\ConfigTest - */ class Config extends BaseConfig { /** diff --git a/system/Database/RawSql.php b/system/Database/RawSql.php index cb16131fe4c3..ef4acf39c3ea 100644 --- a/system/Database/RawSql.php +++ b/system/Database/RawSql.php @@ -15,9 +15,6 @@ use Stringable; -/** - * @see \CodeIgniter\Database\RawSqlTest - */ class RawSql implements Stringable { /** diff --git a/system/Database/TableName.php b/system/Database/TableName.php index a34ac57aa1d2..230dd845a67c 100644 --- a/system/Database/TableName.php +++ b/system/Database/TableName.php @@ -17,8 +17,6 @@ * Represents a table name in SQL. * * @interal - * - * @see \CodeIgniter\Database\TableNameTest */ class TableName { diff --git a/system/Debug/ExceptionHandler.php b/system/Debug/ExceptionHandler.php index 805b5691c079..e884052f5a87 100644 --- a/system/Debug/ExceptionHandler.php +++ b/system/Debug/ExceptionHandler.php @@ -24,9 +24,6 @@ use Config\Paths; use Throwable; -/** - * @see \CodeIgniter\Debug\ExceptionHandlerTest - */ final class ExceptionHandler extends BaseExceptionHandler implements ExceptionHandlerInterface { use ResponseTrait; diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php index 74092b627963..4fc5feddd035 100644 --- a/system/Debug/Exceptions.php +++ b/system/Debug/Exceptions.php @@ -28,8 +28,6 @@ /** * Exceptions manager - * - * @see \CodeIgniter\Debug\ExceptionsTest */ class Exceptions { diff --git a/system/Debug/Timer.php b/system/Debug/Timer.php index 4c383a743413..53f050c2091d 100644 --- a/system/Debug/Timer.php +++ b/system/Debug/Timer.php @@ -20,8 +20,6 @@ * * Provides a simple way to measure the amount of time * that elapses between two points. - * - * @see \CodeIgniter\Debug\TimerTest */ class Timer { diff --git a/system/Debug/Toolbar/Collectors/Database.php b/system/Debug/Toolbar/Collectors/Database.php index 6ab1094c2918..9f7af83957e0 100644 --- a/system/Debug/Toolbar/Collectors/Database.php +++ b/system/Debug/Toolbar/Collectors/Database.php @@ -20,8 +20,6 @@ /** * Collector for the Database tab of the Debug Toolbar. - * - * @see \CodeIgniter\Debug\Toolbar\Collectors\DatabaseTest */ class Database extends BaseCollector { diff --git a/system/Debug/Toolbar/Collectors/History.php b/system/Debug/Toolbar/Collectors/History.php index a0575feba9e8..d306810f202c 100644 --- a/system/Debug/Toolbar/Collectors/History.php +++ b/system/Debug/Toolbar/Collectors/History.php @@ -17,8 +17,6 @@ /** * History collector - * - * @see \CodeIgniter\Debug\Toolbar\Collectors\HistoryTest */ class History extends BaseCollector { diff --git a/system/Email/Email.php b/system/Email/Email.php index 9304734fd179..3236514a0cf6 100644 --- a/system/Email/Email.php +++ b/system/Email/Email.php @@ -22,8 +22,6 @@ * CodeIgniter Email Class * * Permits email to be sent using Mail, Sendmail, or SMTP. - * - * @see \CodeIgniter\Email\EmailTest */ class Email { diff --git a/system/Encryption/Encryption.php b/system/Encryption/Encryption.php index d24a6754efab..32518574239f 100644 --- a/system/Encryption/Encryption.php +++ b/system/Encryption/Encryption.php @@ -27,8 +27,6 @@ * @property-read string $driver * @property-read list $drivers * @property-read string $key - * - * @see \CodeIgniter\Encryption\EncryptionTest */ class Encryption { diff --git a/system/Encryption/Handlers/OpenSSLHandler.php b/system/Encryption/Handlers/OpenSSLHandler.php index d649a66bf060..0700b8071312 100644 --- a/system/Encryption/Handlers/OpenSSLHandler.php +++ b/system/Encryption/Handlers/OpenSSLHandler.php @@ -21,8 +21,6 @@ * * @property-read string $cipher * @property-read string $key - * - * @see \CodeIgniter\Encryption\Handlers\OpenSSLHandlerTest */ class OpenSSLHandler extends BaseHandler { diff --git a/system/Encryption/Handlers/SodiumHandler.php b/system/Encryption/Handlers/SodiumHandler.php index 0fa1bd235247..b518a322c152 100644 --- a/system/Encryption/Handlers/SodiumHandler.php +++ b/system/Encryption/Handlers/SodiumHandler.php @@ -24,7 +24,6 @@ * @property-read string|null $key * * @see https://github.com/jedisct1/libsodium/issues/392 - * @see \CodeIgniter\Encryption\Handlers\SodiumHandlerTest */ class SodiumHandler extends BaseHandler { diff --git a/system/Entity/Entity.php b/system/Entity/Entity.php index a67c44b6fd1c..30c9e37fa1a8 100644 --- a/system/Entity/Entity.php +++ b/system/Entity/Entity.php @@ -38,8 +38,6 @@ /** * Entity encapsulation, for use with CodeIgniter\Model - * - * @see \CodeIgniter\Entity\EntityTest */ class Entity implements JsonSerializable { diff --git a/system/Events/Events.php b/system/Events/Events.php index 3cad30995f94..f50d13eb288c 100644 --- a/system/Events/Events.php +++ b/system/Events/Events.php @@ -17,8 +17,6 @@ /** * Events - * - * @see \CodeIgniter\Events\EventsTest */ class Events { diff --git a/system/Files/File.php b/system/Files/File.php index 2cf394355185..dda73875a997 100644 --- a/system/Files/File.php +++ b/system/Files/File.php @@ -22,8 +22,6 @@ /** * Wrapper for PHP's built-in SplFileInfo, with goodies. - * - * @see \CodeIgniter\Files\FileTest */ class File extends SplFileInfo { diff --git a/system/Files/FileCollection.php b/system/Files/FileCollection.php index b69cfb1d266b..ea6632d9be1f 100644 --- a/system/Files/FileCollection.php +++ b/system/Files/FileCollection.php @@ -27,7 +27,6 @@ * filtering, and ordering them. * * @template-implements IteratorAggregate - * @see \CodeIgniter\Files\FileCollectionTest */ class FileCollection implements Countable, IteratorAggregate { diff --git a/system/Filters/CSRF.php b/system/Filters/CSRF.php index ea9a9939f2de..994e6eb738c6 100644 --- a/system/Filters/CSRF.php +++ b/system/Filters/CSRF.php @@ -26,7 +26,6 @@ * This filter is not intended to be used from the command line. * * @codeCoverageIgnore - * @see \CodeIgniter\Filters\CSRFTest */ class CSRF implements FilterInterface { diff --git a/system/Filters/Cors.php b/system/Filters/Cors.php index c070d72925e5..34e40c482daa 100644 --- a/system/Filters/Cors.php +++ b/system/Filters/Cors.php @@ -18,9 +18,6 @@ use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; -/** - * @see \CodeIgniter\Filters\CorsTest - */ class Cors implements FilterInterface { private ?CorsService $cors = null; diff --git a/system/Filters/DebugToolbar.php b/system/Filters/DebugToolbar.php index 7199b85f9d62..5f5ba05ab14e 100644 --- a/system/Filters/DebugToolbar.php +++ b/system/Filters/DebugToolbar.php @@ -18,8 +18,6 @@ /** * Debug toolbar filter - * - * @see \CodeIgniter\Filters\DebugToolbarTest */ class DebugToolbar implements FilterInterface { diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index 36725b8198d3..2a02ea7c08e2 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -23,8 +23,6 @@ /** * Filters - * - * @see \CodeIgniter\Filters\FiltersTest */ class Filters { diff --git a/system/Filters/Honeypot.php b/system/Filters/Honeypot.php index 4b24691ef905..fe6491d7f6ab 100644 --- a/system/Filters/Honeypot.php +++ b/system/Filters/Honeypot.php @@ -20,8 +20,6 @@ /** * Honeypot filter - * - * @see \CodeIgniter\Filters\HoneypotTest */ class Honeypot implements FilterInterface { diff --git a/system/Filters/InvalidChars.php b/system/Filters/InvalidChars.php index 5d714b0fdf11..6d8a3a2a5cee 100644 --- a/system/Filters/InvalidChars.php +++ b/system/Filters/InvalidChars.php @@ -25,8 +25,6 @@ * invalid characters: * - invalid UTF-8 characters * - control characters except line break and tab code - * - * @see \CodeIgniter\Filters\InvalidCharsTest */ class InvalidChars implements FilterInterface { diff --git a/system/Filters/SecureHeaders.php b/system/Filters/SecureHeaders.php index 42e5842384db..15a69313bb53 100644 --- a/system/Filters/SecureHeaders.php +++ b/system/Filters/SecureHeaders.php @@ -18,8 +18,6 @@ /** * Add Common Security Headers - * - * @see \CodeIgniter\Filters\SecureHeadersTest */ class SecureHeaders implements FilterInterface { diff --git a/system/Format/Format.php b/system/Format/Format.php index 6100b1a586a8..ec7d104bce8c 100644 --- a/system/Format/Format.php +++ b/system/Format/Format.php @@ -18,8 +18,6 @@ /** * The Format class is a convenient place to create Formatters. - * - * @see \CodeIgniter\Format\FormatTest */ class Format { diff --git a/system/Format/JSONFormatter.php b/system/Format/JSONFormatter.php index fb882a0e4eac..9f21e0fb8f1d 100644 --- a/system/Format/JSONFormatter.php +++ b/system/Format/JSONFormatter.php @@ -18,8 +18,6 @@ /** * JSON data formatter - * - * @see \CodeIgniter\Format\JSONFormatterTest */ class JSONFormatter implements FormatterInterface { diff --git a/system/Format/XMLFormatter.php b/system/Format/XMLFormatter.php index 0f058e8a7ff7..5ef2908858b7 100644 --- a/system/Format/XMLFormatter.php +++ b/system/Format/XMLFormatter.php @@ -19,8 +19,6 @@ /** * XML data formatter - * - * @see \CodeIgniter\Format\XMLFormatterTest */ class XMLFormatter implements FormatterInterface { diff --git a/system/HTTP/CLIRequest.php b/system/HTTP/CLIRequest.php index 55c8b797b094..e6f00c90c2a9 100644 --- a/system/HTTP/CLIRequest.php +++ b/system/HTTP/CLIRequest.php @@ -27,8 +27,6 @@ * originally made available under. * * http://fuelphp.com - * - * @see \CodeIgniter\HTTP\CLIRequestTest */ class CLIRequest extends Request { diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php index 9c2184ac84c1..5ef3352f3da6 100644 --- a/system/HTTP/CURLRequest.php +++ b/system/HTTP/CURLRequest.php @@ -22,8 +22,6 @@ /** * A lightweight HTTP client for sending synchronous HTTP requests via cURL. - * - * @see \CodeIgniter\HTTP\CURLRequestTest */ class CURLRequest extends OutgoingRequest { diff --git a/system/HTTP/ContentSecurityPolicy.php b/system/HTTP/ContentSecurityPolicy.php index acea6b2b5ad6..dfea52a62772 100644 --- a/system/HTTP/ContentSecurityPolicy.php +++ b/system/HTTP/ContentSecurityPolicy.php @@ -25,7 +25,6 @@ * @see http://www.html5rocks.com/en/tutorials/security/content-security-policy/ * @see http://content-security-policy.com/ * @see https://www.owasp.org/index.php/Content_Security_Policy - * @see \CodeIgniter\HTTP\ContentSecurityPolicyTest */ class ContentSecurityPolicy { diff --git a/system/HTTP/Cors.php b/system/HTTP/Cors.php index 270b344abb8b..e1da450b8270 100644 --- a/system/HTTP/Cors.php +++ b/system/HTTP/Cors.php @@ -18,8 +18,6 @@ /** * Cross-Origin Resource Sharing (CORS) - * - * @see \CodeIgniter\HTTP\CorsTest */ class Cors { diff --git a/system/HTTP/DownloadResponse.php b/system/HTTP/DownloadResponse.php index b7ce79b8309a..fa7dc3dddf20 100644 --- a/system/HTTP/DownloadResponse.php +++ b/system/HTTP/DownloadResponse.php @@ -20,8 +20,6 @@ /** * HTTP response when a download is requested. - * - * @see \CodeIgniter\HTTP\DownloadResponseTest */ class DownloadResponse extends Response { diff --git a/system/HTTP/Files/FileCollection.php b/system/HTTP/Files/FileCollection.php index c1c0582f6ff5..934cf9ca237e 100644 --- a/system/HTTP/Files/FileCollection.php +++ b/system/HTTP/Files/FileCollection.php @@ -20,8 +20,6 @@ * Class FileCollection * * Provides easy access to uploaded files for a request. - * - * @see \CodeIgniter\HTTP\Files\FileCollectionTest */ class FileCollection { diff --git a/system/HTTP/Header.php b/system/HTTP/Header.php index 54f162eb7caa..845307c84bca 100644 --- a/system/HTTP/Header.php +++ b/system/HTTP/Header.php @@ -20,8 +20,6 @@ * Class Header * * Represents a single HTTP header. - * - * @see \CodeIgniter\HTTP\HeaderTest */ class Header implements Stringable { diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index cc66e35f4ddc..10b4678ffa6a 100644 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -44,8 +44,6 @@ * - Query string arguments (generally via $_GET, or as parsed via parse_str()) * - Upload files, if any (as represented by $_FILES) * - Deserialized body binds (generally from $_POST) - * - * @see \CodeIgniter\HTTP\IncomingRequestTest */ class IncomingRequest extends Request { diff --git a/system/HTTP/Message.php b/system/HTTP/Message.php index 7c5114f71f63..22e0ec67a8e5 100644 --- a/system/HTTP/Message.php +++ b/system/HTTP/Message.php @@ -17,8 +17,6 @@ /** * An HTTP message - * - * @see \CodeIgniter\HTTP\MessageTest */ class Message implements MessageInterface { diff --git a/system/HTTP/Negotiate.php b/system/HTTP/Negotiate.php index d482d086e4c9..82123558a273 100644 --- a/system/HTTP/Negotiate.php +++ b/system/HTTP/Negotiate.php @@ -24,7 +24,6 @@ * server wants. * * @see http://tools.ietf.org/html/rfc7231#section-5.3 - * @see \CodeIgniter\HTTP\NegotiateTest */ class Negotiate { diff --git a/system/HTTP/OutgoingRequest.php b/system/HTTP/OutgoingRequest.php index 1534bdcb8308..74d089a03cda 100644 --- a/system/HTTP/OutgoingRequest.php +++ b/system/HTTP/OutgoingRequest.php @@ -15,8 +15,6 @@ /** * Representation of an outgoing, client-side request. - * - * @see \CodeIgniter\HTTP\OutgoingRequestTest */ class OutgoingRequest extends Message implements OutgoingRequestInterface { diff --git a/system/HTTP/RedirectResponse.php b/system/HTTP/RedirectResponse.php index ed464244a63d..61c760ee1cc9 100644 --- a/system/HTTP/RedirectResponse.php +++ b/system/HTTP/RedirectResponse.php @@ -18,8 +18,6 @@ /** * Handle a redirect response - * - * @see \CodeIgniter\HTTP\RedirectResponseTest */ class RedirectResponse extends Response { diff --git a/system/HTTP/Request.php b/system/HTTP/Request.php index 63b5f0a8dbfd..73859dd843ae 100644 --- a/system/HTTP/Request.php +++ b/system/HTTP/Request.php @@ -17,8 +17,6 @@ /** * Representation of an incoming, server-side HTTP request. - * - * @see \CodeIgniter\HTTP\RequestTest */ class Request extends OutgoingRequest implements RequestInterface { diff --git a/system/HTTP/Response.php b/system/HTTP/Response.php index 2ac2ecb7e9d2..c8107a5507ed 100644 --- a/system/HTTP/Response.php +++ b/system/HTTP/Response.php @@ -29,8 +29,6 @@ * - Status code and reason phrase * - Headers * - Message body - * - * @see \CodeIgniter\HTTP\ResponseTest */ class Response extends Message implements ResponseInterface { diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index 403269008e56..7deb85fd5613 100644 --- a/system/HTTP/SiteURI.php +++ b/system/HTTP/SiteURI.php @@ -20,8 +20,6 @@ /** * URI for the application site - * - * @see \CodeIgniter\HTTP\SiteURITest */ class SiteURI extends URI { diff --git a/system/HTTP/SiteURIFactory.php b/system/HTTP/SiteURIFactory.php index 11dccce6c540..b1def3293a17 100644 --- a/system/HTTP/SiteURIFactory.php +++ b/system/HTTP/SiteURIFactory.php @@ -21,8 +21,6 @@ * Creates SiteURI using superglobals. * * This class also updates superglobal $_SERVER and $_GET. - * - * @see \CodeIgniter\HTTP\SiteURIFactoryTest */ final readonly class SiteURIFactory { diff --git a/system/HTTP/URI.php b/system/HTTP/URI.php index 4b8b324dc80e..e7d6cc369210 100644 --- a/system/HTTP/URI.php +++ b/system/HTTP/URI.php @@ -22,8 +22,6 @@ /** * Abstraction for a uniform resource identifier (URI). - * - * @see \CodeIgniter\HTTP\URITest */ class URI implements Stringable { diff --git a/system/HTTP/UserAgent.php b/system/HTTP/UserAgent.php index fe7679e59384..b89fb63b70f0 100644 --- a/system/HTTP/UserAgent.php +++ b/system/HTTP/UserAgent.php @@ -18,8 +18,6 @@ /** * Abstraction for an HTTP user agent - * - * @see \CodeIgniter\HTTP\UserAgentTest */ class UserAgent implements Stringable { diff --git a/system/Helpers/Array/ArrayHelper.php b/system/Helpers/Array/ArrayHelper.php index d370e1f1112f..b48efbfc4802 100644 --- a/system/Helpers/Array/ArrayHelper.php +++ b/system/Helpers/Array/ArrayHelper.php @@ -20,10 +20,6 @@ * * If there are any methods that should be provided, make them * public APIs via helper functions. - * - * @see \CodeIgniter\Helpers\Array\ArrayHelperDotKeyExistsTest - * @see \CodeIgniter\Helpers\Array\ArrayHelperRecursiveDiffTest - * @see \CodeIgniter\Helpers\Array\ArrayHelperSortValuesByNaturalTest */ final class ArrayHelper { diff --git a/system/Honeypot/Honeypot.php b/system/Honeypot/Honeypot.php index 4a40a8ec232a..a26131d69b73 100644 --- a/system/Honeypot/Honeypot.php +++ b/system/Honeypot/Honeypot.php @@ -21,8 +21,6 @@ /** * class Honeypot - * - * @see \CodeIgniter\Honeypot\HoneypotTest */ class Honeypot { diff --git a/system/HotReloader/DirectoryHasher.php b/system/HotReloader/DirectoryHasher.php index ab9ab7a017a4..6bc1e6b50390 100644 --- a/system/HotReloader/DirectoryHasher.php +++ b/system/HotReloader/DirectoryHasher.php @@ -21,7 +21,6 @@ /** * @internal - * @see \CodeIgniter\HotReloader\DirectoryHasherTest */ final class DirectoryHasher { diff --git a/system/I18n/Time.php b/system/I18n/Time.php index cc254dba03ea..c5f9ffc3e557 100644 --- a/system/I18n/Time.php +++ b/system/I18n/Time.php @@ -41,8 +41,6 @@ * @property-read string $year * * @phpstan-consistent-constructor - * - * @see \CodeIgniter\I18n\TimeTest */ class Time extends DateTimeImmutable implements Stringable { diff --git a/system/I18n/TimeDifference.php b/system/I18n/TimeDifference.php index b51baf252472..10f31d436c2b 100644 --- a/system/I18n/TimeDifference.php +++ b/system/I18n/TimeDifference.php @@ -24,8 +24,6 @@ * @property-read int $seconds * @property-read float|int $weeks * @property-read float|int $years - * - * @see \CodeIgniter\I18n\TimeDifferenceTest */ class TimeDifference { diff --git a/system/I18n/TimeLegacy.php b/system/I18n/TimeLegacy.php index 119fc3f5d050..3bf92b1d624a 100644 --- a/system/I18n/TimeLegacy.php +++ b/system/I18n/TimeLegacy.php @@ -44,7 +44,6 @@ * @phpstan-consistent-constructor * * @deprecated Use Time instead. - * @see \CodeIgniter\I18n\TimeLegacyTest */ class TimeLegacy extends DateTime { diff --git a/system/Images/Image.php b/system/Images/Image.php index e634b914016a..77eecf8e7b8e 100644 --- a/system/Images/Image.php +++ b/system/Images/Image.php @@ -18,8 +18,6 @@ /** * Encapsulation of an Image file - * - * @see \CodeIgniter\Images\ImageTest */ class Image extends File { diff --git a/system/Language/Language.php b/system/Language/Language.php index 1964c91aebd9..49cfd85b9966 100644 --- a/system/Language/Language.php +++ b/system/Language/Language.php @@ -22,8 +22,6 @@ * Locale-based, built on top of PHP internationalization. * * @phpstan-type LoadedStrings array|string>|string|list> - * - * @see \CodeIgniter\Language\LanguageTest */ class Language { diff --git a/system/Log/Handlers/ChromeLoggerHandler.php b/system/Log/Handlers/ChromeLoggerHandler.php index ce8e97a12750..df3666111c9d 100644 --- a/system/Log/Handlers/ChromeLoggerHandler.php +++ b/system/Log/Handlers/ChromeLoggerHandler.php @@ -20,7 +20,6 @@ * Requires the ChromeLogger extension installed in your browser. * * @see https://craig.is/writing/chrome-logger - * @see \CodeIgniter\Log\Handlers\ChromeLoggerHandlerTest */ class ChromeLoggerHandler extends BaseHandler { diff --git a/system/Log/Handlers/ErrorlogHandler.php b/system/Log/Handlers/ErrorlogHandler.php index a7e820419fee..bf05a8e95596 100644 --- a/system/Log/Handlers/ErrorlogHandler.php +++ b/system/Log/Handlers/ErrorlogHandler.php @@ -17,8 +17,6 @@ /** * Log handler that writes to PHP's `error_log()` - * - * @see \CodeIgniter\Log\Handlers\ErrorlogHandlerTest */ class ErrorlogHandler extends BaseHandler { diff --git a/system/Log/Logger.php b/system/Log/Logger.php index 6b61837fa22b..02ecf1e97951 100644 --- a/system/Log/Logger.php +++ b/system/Log/Logger.php @@ -32,8 +32,6 @@ * The context array can contain arbitrary data, the only assumption that * can be made by implementors is that if an Exception instance is given * to produce a stack trace, it MUST be in a key named "exception". - * - * @see \CodeIgniter\Log\LoggerTest */ class Logger implements LoggerInterface { diff --git a/system/Pager/Pager.php b/system/Pager/Pager.php index 4537f1d43db3..6a17a07aeee6 100644 --- a/system/Pager/Pager.php +++ b/system/Pager/Pager.php @@ -26,8 +26,6 @@ * pagination links and reading the current url's query variable, "page" * to determine the current page. This class can support multiple * paginations on a single page. - * - * @see \CodeIgniter\Pager\PagerTest */ class Pager implements PagerInterface { diff --git a/system/Pager/PagerRenderer.php b/system/Pager/PagerRenderer.php index 70959c820ea4..e69b56deebf7 100644 --- a/system/Pager/PagerRenderer.php +++ b/system/Pager/PagerRenderer.php @@ -21,8 +21,6 @@ * This class is passed to the view that describes the pagination, * and is used to get the link information and provide utility * methods needed to work with pagination. - * - * @see \CodeIgniter\Pager\PagerRendererTest */ class PagerRenderer { diff --git a/system/Publisher/ContentReplacer.php b/system/Publisher/ContentReplacer.php index b263b1a18d7f..d61aac34bbbc 100644 --- a/system/Publisher/ContentReplacer.php +++ b/system/Publisher/ContentReplacer.php @@ -17,8 +17,6 @@ /** * Replace Text Content - * - * @see \CodeIgniter\Publisher\ContentReplacerTest */ class ContentReplacer { diff --git a/system/RESTful/ResourceController.php b/system/RESTful/ResourceController.php index ed72df212b07..549ec10ea940 100644 --- a/system/RESTful/ResourceController.php +++ b/system/RESTful/ResourceController.php @@ -18,8 +18,6 @@ /** * An extendable controller to provide a RESTful API for a resource. - * - * @see \CodeIgniter\RESTful\ResourceControllerTest */ class ResourceController extends BaseResource { diff --git a/system/RESTful/ResourcePresenter.php b/system/RESTful/ResourcePresenter.php index 00bd54ed035a..890f087d8d1b 100644 --- a/system/RESTful/ResourcePresenter.php +++ b/system/RESTful/ResourcePresenter.php @@ -17,8 +17,6 @@ /** * An extendable controller to help provide a UI for a resource. - * - * @see \CodeIgniter\RESTful\ResourcePresenterTest */ class ResourcePresenter extends BaseResource { diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index c7dbf04efec5..15b6cbb07719 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -21,8 +21,6 @@ /** * New Secure Router for Auto-Routing - * - * @see \CodeIgniter\Router\AutoRouterImprovedTest */ final class AutoRouterImproved implements AutoRouterInterface { diff --git a/system/Router/DefinedRouteCollector.php b/system/Router/DefinedRouteCollector.php index c66e25c03349..7efd5f1ddf0d 100644 --- a/system/Router/DefinedRouteCollector.php +++ b/system/Router/DefinedRouteCollector.php @@ -18,8 +18,6 @@ /** * Collect all defined routes for display. - * - * @see \CodeIgniter\Router\DefinedRouteCollectorTest */ final readonly class DefinedRouteCollector { diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index c08c01530b79..9e15b68cec1a 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -25,7 +25,6 @@ /** * @todo Implement nested resource routing (See CakePHP) - * @see \CodeIgniter\Router\RouteCollectionTest */ class RouteCollection implements RouteCollectionInterface { diff --git a/system/Router/Router.php b/system/Router/Router.php index 6e5a4630832b..f0f9555ad1ac 100644 --- a/system/Router/Router.php +++ b/system/Router/Router.php @@ -32,8 +32,6 @@ /** * Request router. - * - * @see \CodeIgniter\Router\RouterTest */ class Router implements RouterInterface { diff --git a/system/Security/CheckPhpIni.php b/system/Security/CheckPhpIni.php index d1ab3dfccda0..04bcb3e900d0 100644 --- a/system/Security/CheckPhpIni.php +++ b/system/Security/CheckPhpIni.php @@ -20,7 +20,6 @@ * Checks php.ini settings in production environment. * * @used-by \CodeIgniter\Commands\Utilities\PhpIniCheck - * @see \CodeIgniter\Security\CheckPhpIniTest */ class CheckPhpIni { diff --git a/system/Security/Security.php b/system/Security/Security.php index 583a26593976..44e96be29016 100644 --- a/system/Security/Security.php +++ b/system/Security/Security.php @@ -33,8 +33,6 @@ * * Provides methods that help protect your site against * Cross-Site Request Forgery attacks. - * - * @see \CodeIgniter\Security\SecurityTest */ class Security implements SecurityInterface { diff --git a/system/Session/Handlers/Database/MySQLiHandler.php b/system/Session/Handlers/Database/MySQLiHandler.php index a15f17bc904f..453ebb1f3cc9 100644 --- a/system/Session/Handlers/Database/MySQLiHandler.php +++ b/system/Session/Handlers/Database/MySQLiHandler.php @@ -17,8 +17,6 @@ /** * Session handler for MySQLi - * - * @see \CodeIgniter\Session\Handlers\Database\MySQLiHandlerTest */ class MySQLiHandler extends DatabaseHandler { diff --git a/system/Session/Handlers/Database/PostgreHandler.php b/system/Session/Handlers/Database/PostgreHandler.php index 1471d44d1860..d8c6d3c8b794 100644 --- a/system/Session/Handlers/Database/PostgreHandler.php +++ b/system/Session/Handlers/Database/PostgreHandler.php @@ -18,8 +18,6 @@ /** * Session handler for Postgre - * - * @see \CodeIgniter\Session\Handlers\Database\PostgreHandlerTest */ class PostgreHandler extends DatabaseHandler { diff --git a/system/Session/Session.php b/system/Session/Session.php index 65b1e87558d8..1cb47c2ee260 100644 --- a/system/Session/Session.php +++ b/system/Session/Session.php @@ -27,8 +27,6 @@ * variables in `Сonfig\Session`. * * @property string $session_id - * - * @see \CodeIgniter\Session\SessionTest */ class Session implements SessionInterface { diff --git a/system/Superglobals.php b/system/Superglobals.php index ac0ea289bd77..461a41199104 100644 --- a/system/Superglobals.php +++ b/system/Superglobals.php @@ -38,7 +38,6 @@ * @phpstan-type request_items array|string * * @internal - * @see \CodeIgniter\SuperglobalsTest */ final class Superglobals { diff --git a/system/Test/DOMParser.php b/system/Test/DOMParser.php index 0afddfa4425e..ad7d483ab22d 100644 --- a/system/Test/DOMParser.php +++ b/system/Test/DOMParser.php @@ -21,8 +21,6 @@ /** * Load a response into a DOMDocument for testing assertions based on that - * - * @see \CodeIgniter\Test\DOMParserTest */ class DOMParser { diff --git a/system/Test/Fabricator.php b/system/Test/Fabricator.php index c0b925d3cf66..93e37f12c9fc 100644 --- a/system/Test/Fabricator.php +++ b/system/Test/Fabricator.php @@ -29,8 +29,6 @@ * * Bridge class for using Faker to create example data based on * model specifications. - * - * @see \CodeIgniter\Test\FabricatorTest */ class Fabricator { diff --git a/system/Test/TestLogger.php b/system/Test/TestLogger.php index bbd0839e93b2..a13657748e53 100644 --- a/system/Test/TestLogger.php +++ b/system/Test/TestLogger.php @@ -16,9 +16,6 @@ use CodeIgniter\Log\Logger; use Stringable; -/** - * @see \CodeIgniter\Test\TestLoggerTest - */ class TestLogger extends Logger { /** diff --git a/system/Test/TestResponse.php b/system/Test/TestResponse.php index 580f938d77af..6a3a38ac88d5 100644 --- a/system/Test/TestResponse.php +++ b/system/Test/TestResponse.php @@ -25,8 +25,6 @@ * for test results. * * @mixin DOMParser - * - * @see \CodeIgniter\Test\TestResponseTest */ class TestResponse { diff --git a/system/Typography/Typography.php b/system/Typography/Typography.php index 66a13a0b3f06..0d30edc66aa7 100644 --- a/system/Typography/Typography.php +++ b/system/Typography/Typography.php @@ -17,8 +17,6 @@ /** * Typography Class - * - * @see \CodeIgniter\Typography\TypographyTest */ class Typography { diff --git a/system/Validation/CreditCardRules.php b/system/Validation/CreditCardRules.php index 0bc20f082123..e270a6ba9f78 100644 --- a/system/Validation/CreditCardRules.php +++ b/system/Validation/CreditCardRules.php @@ -19,7 +19,6 @@ * Provides validation methods for common credit-card inputs. * * @see http://en.wikipedia.org/wiki/Credit_card_number - * @see \CodeIgniter\Validation\CreditCardRulesTest */ class CreditCardRules { diff --git a/system/Validation/DotArrayFilter.php b/system/Validation/DotArrayFilter.php index 8e059c3661ae..bc2eecc497a6 100644 --- a/system/Validation/DotArrayFilter.php +++ b/system/Validation/DotArrayFilter.php @@ -13,9 +13,6 @@ namespace CodeIgniter\Validation; -/** - * @see \CodeIgniter\Validation\DotArrayFilterTest - */ final class DotArrayFilter { /** diff --git a/system/Validation/FileRules.php b/system/Validation/FileRules.php index b65b4da63085..fe8de6a1d5bf 100644 --- a/system/Validation/FileRules.php +++ b/system/Validation/FileRules.php @@ -17,8 +17,6 @@ /** * File validation rules - * - * @see \CodeIgniter\Validation\FileRulesTest */ class FileRules extends StrictFileRules { diff --git a/system/Validation/FormatRules.php b/system/Validation/FormatRules.php index e28ef97706f3..11f11909f0bf 100644 --- a/system/Validation/FormatRules.php +++ b/system/Validation/FormatRules.php @@ -17,8 +17,6 @@ /** * Format validation Rules. - * - * @see \CodeIgniter\Validation\FormatRulesTest */ class FormatRules { diff --git a/system/Validation/Rules.php b/system/Validation/Rules.php index c48465b327e0..ef9832f7ccd0 100644 --- a/system/Validation/Rules.php +++ b/system/Validation/Rules.php @@ -20,8 +20,6 @@ /** * Validation Rules. - * - * @see \CodeIgniter\Validation\RulesTest */ class Rules { diff --git a/system/Validation/StrictRules/CreditCardRules.php b/system/Validation/StrictRules/CreditCardRules.php index fd28e4454095..feded5cbe447 100644 --- a/system/Validation/StrictRules/CreditCardRules.php +++ b/system/Validation/StrictRules/CreditCardRules.php @@ -21,7 +21,6 @@ * Provides validation methods for common credit-card inputs. * * @see http://en.wikipedia.org/wiki/Credit_card_number - * @see \CodeIgniter\Validation\StrictRules\CreditCardRulesTest */ class CreditCardRules { diff --git a/system/Validation/StrictRules/FileRules.php b/system/Validation/StrictRules/FileRules.php index f84275432d54..2911042b15bf 100644 --- a/system/Validation/StrictRules/FileRules.php +++ b/system/Validation/StrictRules/FileRules.php @@ -22,8 +22,6 @@ /** * File validation rules - * - * @see \CodeIgniter\Validation\StrictRules\FileRulesTest */ class FileRules { diff --git a/system/Validation/StrictRules/FormatRules.php b/system/Validation/StrictRules/FormatRules.php index 8557a6de070d..44ada03899a7 100644 --- a/system/Validation/StrictRules/FormatRules.php +++ b/system/Validation/StrictRules/FormatRules.php @@ -17,8 +17,6 @@ /** * Format validation Rules. - * - * @see \CodeIgniter\Validation\StrictRules\FormatRulesTest */ class FormatRules { diff --git a/system/Validation/StrictRules/Rules.php b/system/Validation/StrictRules/Rules.php index cca4fdf494be..f5c31ca16c3e 100644 --- a/system/Validation/StrictRules/Rules.php +++ b/system/Validation/StrictRules/Rules.php @@ -18,8 +18,6 @@ /** * Validation Rules. - * - * @see \CodeIgniter\Validation\StrictRules\RulesTest */ class Rules { diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php index b7c702aee6bc..18c7f913f2c7 100644 --- a/system/Validation/Validation.php +++ b/system/Validation/Validation.php @@ -25,8 +25,6 @@ /** * Validator - * - * @see \CodeIgniter\Validation\ValidationTest */ class Validation implements ValidationInterface { diff --git a/system/View/Cell.php b/system/View/Cell.php index 7b0a15853b2f..855ce7dd5c58 100644 --- a/system/View/Cell.php +++ b/system/View/Cell.php @@ -44,8 +44,6 @@ * class Class { * function method(array $params=null) * } - * - * @see \CodeIgniter\View\CellTest */ class Cell { diff --git a/system/View/Parser.php b/system/View/Parser.php index 8aaa1c723493..21b3fe47b6e3 100644 --- a/system/View/Parser.php +++ b/system/View/Parser.php @@ -20,8 +20,6 @@ /** * Class for parsing pseudo-vars - * - * @see \CodeIgniter\View\ParserTest */ class Parser extends View { diff --git a/system/View/Table.php b/system/View/Table.php index 5d4958088b2d..12de27a831cd 100644 --- a/system/View/Table.php +++ b/system/View/Table.php @@ -19,8 +19,6 @@ * HTML Table Generating Class * * Lets you create tables manually or from database result objects, or arrays. - * - * @see \CodeIgniter\View\TableTest */ class Table { diff --git a/system/View/View.php b/system/View/View.php index ea12ec45bb76..6cdef5689c4a 100644 --- a/system/View/View.php +++ b/system/View/View.php @@ -24,8 +24,6 @@ /** * Class View - * - * @see \CodeIgniter\View\ViewTest */ class View implements RendererInterface { From 8e2db880c621012a7627a1331b20e728da22b5cd Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Tue, 13 Oct 2026 19:32:15 +0700 Subject: [PATCH 09/52] chore: Bump StructArmed version to ^0.19 (#10630) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 9aa9641b8e6d..3fbec754b543 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "psr/log": "^3.0" }, "require-dev": { - "boundwize/structarmed": "^0.18", + "boundwize/structarmed": "^0.19", "codeigniter/phpstan-codeigniter": "^2.1", "fakerphp/faker": "^1.24", "kint-php/kint": "^6.1", From df733747b0b34458f534601d17c37ecbea47a273 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Wed, 14 Oct 2026 18:53:30 +0800 Subject: [PATCH 10/52] chore: clarify the compatibility and static-analysis rules for agent reviewers (#10629) --- .github/copilot-instructions.md | 11 ++++++-- .github/instructions/tests.instructions.md | 2 ++ AGENTS.md | 29 +++++++++++++++++++--- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 4ad38ee3eadf..cd4aff6cff0a 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -23,8 +23,15 @@ Prioritize actionable findings involving: ## Review behavior - Determine the PR base branch and apply the compatibility policy from - `AGENTS.md`. If the base branch cannot be determined, assume `develop` - and state that assumption. + `AGENTS.md`. When it is not visible, read it from the title prefix and + the labels as `AGENTS.md` describes, and otherwise assume `develop` and + state that assumption. +- Before reporting a compatibility break, confirm the symbol exists in the + latest tagged release. A symbol introduced on the base branch since that + release is unreleased and may change freely, as `AGENTS.md` describes. +- Do not report a PHPStan suppression or baseline entry covered by the + exceptions in `AGENTS.md`, or a missing reset of state that + `CIUnitTestCase::tearDown()` already restores. - Do not report formatting-only issues already enforced by PHP-CS-Fixer. - Every finding should identify a concrete failure scenario and the affected code. diff --git a/.github/instructions/tests.instructions.md b/.github/instructions/tests.instructions.md index af2d9a96ba96..63722474b723 100644 --- a/.github/instructions/tests.instructions.md +++ b/.github/instructions/tests.instructions.md @@ -14,6 +14,8 @@ applyTo: "tests/**/*.php" Avoid testing private implementation details. - Restore services, factories, environment variables, superglobals, handlers, locale, time-related state, and other global state modified by a test. + Check `CIUnitTestCase::tearDown()` first: a test need not repeat the + resets it already performs. - Tests must not depend on execution order or state left by another test. - Do not replace a meaningful assertion with a weaker assertion to make a test pass. diff --git a/AGENTS.md b/AGENTS.md index 208568025619..675c9f83e33e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,8 +19,13 @@ existing implementation before introducing a new abstraction or convention. ## Target branch and compatibility policy Compatibility is judged against the pull request's base branch, not the -contributor's source or feature branch. When the base branch is unknown, -apply the stricter `develop` policy. +contributor's source or feature branch. When the base branch is not stated, +take the cues from the title prefix and the labels. A `feat:` prefix and the +`enhancement`, `new feature`, `breaking change`, or `4.x` labels point to the +next minor branch. A `fix:`, `refactor:`, or `docs:` prefix and the `bug` or +`refactor` labels can go either way, since the next minor branch also takes +fixes to its own unreleased code, so look at what the change touches. When no +cue settles it, apply the stricter `develop` policy. ### Base branch `develop` @@ -50,6 +55,16 @@ complete the documented deprecation lifecycle. - Every accepted break needs tests for the new behavior, a minor-version changelog entry, and migration instructions in the upgrading guide. +### Unreleased API + +A symbol first added on the base branch after its latest tagged release is +unreleased. Nothing outside the repository can depend on it yet, so changing, +renaming, or removing it is not a compatibility break: it needs no +deprecation, no upgrading-guide entry, and no changelog line beyond the one +describing the final shape. Check the version tags or the changelog of the +unreleased version before reporting a break on such a symbol. The +compatibility policy applies to what the latest tagged release ships. + ### All base branches - Production code must run on PHP 8.2. Do not require a newer PHP version, @@ -83,7 +98,15 @@ complete the documented deprecation lifecycle. - Add PHPDoc only when it contributes information that native types cannot express. Do not duplicate a parent or interface docblock. - Never suppress a static-analysis error or update a baseline to make a - check pass. + check pass. The accepted exceptions are: + - a false positive, when the usage is judged correct; + - a suppression needed to assert a runtime behavior in a test; + - a call to a function mocked in `system/Test/Mock/MockCommon.php`, such + as `is_cli(false)`, which PHPStan reports as `arguments.count`. + + Regenerate the baseline with `composer phpstan:baseline`, never by editing + the `.neon` files, and add a test file that calls `is_cli()` with an + argument to the `RemoveExtraParametersRector` skip list in `rector.php`. ## FrankenPHP Worker Mode From 899c52c9dea84f91f5be446fbf013c67dfd79498 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Oct 2026 23:48:18 +0800 Subject: [PATCH 11/52] chore(deps): bump NexusPHP/carson in / (#10639) Bumps [NexusPHP/carson](https://github.com/nexusphp/carson) in `/` from 1.7.0 to 1.8.0. Updates `NexusPHP/carson` from 1.7.0 to 1.8.0 - [Release notes](https://github.com/nexusphp/carson/releases) - [Changelog](https://github.com/NexusPHP/carson/blob/1.x/CHANGELOG.md) - [Commits](https://github.com/nexusphp/carson/compare/cb35604ab330c2e44dba673fcef712c78e75f858...53c45cf25e2303bc503209f8b5dd9107f27b2e8f) --- updated-dependencies: - dependency-name: NexusPHP/carson dependency-version: 1.8.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github_actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/run-carson.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-carson.yml b/.github/workflows/run-carson.yml index a0a160b0930d..a9f88eb7f530 100644 --- a/.github/workflows/run-carson.yml +++ b/.github/workflows/run-carson.yml @@ -32,7 +32,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Run Carson - uses: NexusPHP/carson@cb35604ab330c2e44dba673fcef712c78e75f858 # v1.7.0 + uses: NexusPHP/carson@53c45cf25e2303bc503209f8b5dd9107f27b2e8f # v1.8.0 with: app_id: ${{ secrets.CARSON_APP_ID }} private_key: ${{ secrets.CARSON_PRIVATE_KEY }} From afe293c447f2e7ca5af0ee37e1ec967d8ef5a2c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 18 Oct 2026 10:56:12 +0200 Subject: [PATCH 12/52] chore(deps): bump shivammathur/setup-php in / (#10631) Bumps [shivammathur/setup-php](https://github.com/shivammathur/setup-php) in `/` from 2.37.2 to 2.40.0. Updates `shivammathur/setup-php` from 2.37.2 to 2.40.0 - [Release notes](https://github.com/shivammathur/setup-php/releases) - [Commits](https://github.com/shivammathur/setup-php/compare/f3e473d116dcccaddc5834248c87452386958240...eb7c497e18156a6bbabfef1d3a82760b9eda3962) --- updated-dependencies: - dependency-name: shivammathur/setup-php dependency-version: 2.40.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github_actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/deploy-apidocs.yml | 2 +- .github/workflows/deploy-userguide-latest.yml | 2 +- .github/workflows/reusable-coveralls.yml | 2 +- .github/workflows/reusable-phpunit-test.yml | 2 +- .github/workflows/reusable-serviceless-phpunit-test.yml | 2 +- .github/workflows/test-autoreview.yml | 2 +- .github/workflows/test-coding-standards.yml | 2 +- .github/workflows/test-phpstan.yml | 2 +- .github/workflows/test-psalm.yml | 2 +- .github/workflows/test-random-execution.yml | 2 +- .github/workflows/test-rector.yml | 2 +- .github/workflows/test-structarmed.yml | 2 +- .github/workflows/verify-release.yml | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/deploy-apidocs.yml b/.github/workflows/deploy-apidocs.yml index 8eb59179a946..d34fce914406 100644 --- a/.github/workflows/deploy-apidocs.yml +++ b/.github/workflows/deploy-apidocs.yml @@ -44,7 +44,7 @@ jobs: persist-credentials: false - name: Setup PHP - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 with: php-version: '8.2' tools: phive diff --git a/.github/workflows/deploy-userguide-latest.yml b/.github/workflows/deploy-userguide-latest.yml index c943254325d6..797012b9faab 100644 --- a/.github/workflows/deploy-userguide-latest.yml +++ b/.github/workflows/deploy-userguide-latest.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Setup PHP - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 with: php-version: '8.2' coverage: none diff --git a/.github/workflows/reusable-coveralls.yml b/.github/workflows/reusable-coveralls.yml index ffd482fdf6f0..555625db5066 100644 --- a/.github/workflows/reusable-coveralls.yml +++ b/.github/workflows/reusable-coveralls.yml @@ -22,7 +22,7 @@ jobs: persist-credentials: false - name: Setup PHP - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 with: php-version: ${{ inputs.php-version }} tools: composer diff --git a/.github/workflows/reusable-phpunit-test.yml b/.github/workflows/reusable-phpunit-test.yml index e21c3a65b671..ead2af2a4a97 100644 --- a/.github/workflows/reusable-phpunit-test.yml +++ b/.github/workflows/reusable-phpunit-test.yml @@ -175,7 +175,7 @@ jobs: persist-credentials: false - name: Setup PHP - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 with: php-version: ${{ inputs.php-version }} tools: composer diff --git a/.github/workflows/reusable-serviceless-phpunit-test.yml b/.github/workflows/reusable-serviceless-phpunit-test.yml index 590c064cf022..4876f2b576e8 100644 --- a/.github/workflows/reusable-serviceless-phpunit-test.yml +++ b/.github/workflows/reusable-serviceless-phpunit-test.yml @@ -72,7 +72,7 @@ jobs: fetch-depth: 0 - name: Setup PHP - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 with: php-version: ${{ inputs.php-version }} tools: composer diff --git a/.github/workflows/test-autoreview.yml b/.github/workflows/test-autoreview.yml index f06e02619966..7793a3cc091b 100644 --- a/.github/workflows/test-autoreview.yml +++ b/.github/workflows/test-autoreview.yml @@ -39,7 +39,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup PHP - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 with: php-version: '8.2' diff --git a/.github/workflows/test-coding-standards.yml b/.github/workflows/test-coding-standards.yml index 16e0d5d745f6..274cc5311e2a 100644 --- a/.github/workflows/test-coding-standards.yml +++ b/.github/workflows/test-coding-standards.yml @@ -38,7 +38,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup PHP - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 with: php-version: ${{ matrix.php-version }} extensions: tokenizer diff --git a/.github/workflows/test-phpstan.yml b/.github/workflows/test-phpstan.yml index 22813782d756..d16ab6d98a27 100644 --- a/.github/workflows/test-phpstan.yml +++ b/.github/workflows/test-phpstan.yml @@ -49,7 +49,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup PHP - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 with: php-version: '8.2' extensions: intl diff --git a/.github/workflows/test-psalm.yml b/.github/workflows/test-psalm.yml index 27aa0712f0c1..23f2ea365eab 100644 --- a/.github/workflows/test-psalm.yml +++ b/.github/workflows/test-psalm.yml @@ -41,7 +41,7 @@ jobs: persist-credentials: false - name: Setup PHP - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 with: php-version: ${{ matrix.php-version }} extensions: intl, json, mbstring, xml, mysqli, oci8, pgsql, sqlsrv, sqlite3 diff --git a/.github/workflows/test-random-execution.yml b/.github/workflows/test-random-execution.yml index 932c17b683d7..14e7b9448430 100644 --- a/.github/workflows/test-random-execution.yml +++ b/.github/workflows/test-random-execution.yml @@ -174,7 +174,7 @@ jobs: fetch-depth: 0 - name: Setup PHP ${{ matrix.php-version }} - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 with: php-version: ${{ matrix.php-version }} extensions: gd, curl, iconv, json, mbstring, openssl, sodium diff --git a/.github/workflows/test-rector.yml b/.github/workflows/test-rector.yml index 215670734064..4e603df49e44 100644 --- a/.github/workflows/test-rector.yml +++ b/.github/workflows/test-rector.yml @@ -56,7 +56,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup PHP - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 with: php-version: ${{ matrix.php-version }} extensions: intl diff --git a/.github/workflows/test-structarmed.yml b/.github/workflows/test-structarmed.yml index 0deee13a46b0..b064b8213d07 100644 --- a/.github/workflows/test-structarmed.yml +++ b/.github/workflows/test-structarmed.yml @@ -54,7 +54,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup PHP - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 with: php-version: ${{ matrix.php-version }} extensions: intl diff --git a/.github/workflows/verify-release.yml b/.github/workflows/verify-release.yml index 7d6d2d8da681..842c2407a4c5 100644 --- a/.github/workflows/verify-release.yml +++ b/.github/workflows/verify-release.yml @@ -71,7 +71,7 @@ jobs: steps: - name: Setup PHP - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 with: php-version: '8.2' tools: composer From 38c73d265577c5800303a89a78b4743751a9729a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 18 Oct 2026 10:56:24 +0200 Subject: [PATCH 13/52] chore(deps): bump actions/upload-artifact in / (#10632) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) in `/` from 7.0.1 to 7.0.2. Updates `actions/upload-artifact` from 7.0.1 to 7.0.2 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/043fb46d1a93c77aae656e7c1c64a875d1fc6a0a...cf430e030ddbb5b0abf93d22962f4752f3646cd9) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: 7.0.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github_actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/deploy-userguide-latest.yml | 2 +- .github/workflows/reusable-phpunit-test.yml | 2 +- .github/workflows/reusable-serviceless-phpunit-test.yml | 2 +- .github/workflows/test-random-execution.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy-userguide-latest.yml b/.github/workflows/deploy-userguide-latest.yml index 797012b9faab..6151ad411a14 100644 --- a/.github/workflows/deploy-userguide-latest.yml +++ b/.github/workflows/deploy-userguide-latest.yml @@ -59,7 +59,7 @@ jobs: # Create an artifact of the html output - name: Upload artifact - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: HTML Documentation path: user_guide_src/build/html/ diff --git a/.github/workflows/reusable-phpunit-test.yml b/.github/workflows/reusable-phpunit-test.yml index ead2af2a4a97..6c5497b67294 100644 --- a/.github/workflows/reusable-phpunit-test.yml +++ b/.github/workflows/reusable-phpunit-test.yml @@ -232,7 +232,7 @@ jobs: - name: Upload coverage results as artifact if: ${{ inputs.enable-artifact-upload }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: ${{ steps.setup-env.outputs.ARTIFACT_NAME }} path: build/cov/coverage-${{ steps.setup-env.outputs.ARTIFACT_NAME }}.cov diff --git a/.github/workflows/reusable-serviceless-phpunit-test.yml b/.github/workflows/reusable-serviceless-phpunit-test.yml index 4876f2b576e8..60186d6eaa84 100644 --- a/.github/workflows/reusable-serviceless-phpunit-test.yml +++ b/.github/workflows/reusable-serviceless-phpunit-test.yml @@ -127,7 +127,7 @@ jobs: - name: Upload coverage results as artifact if: ${{ inputs.enable-artifact-upload }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: ${{ steps.setup-env.outputs.ARTIFACT_NAME }} path: build/cov/coverage-${{ steps.setup-env.outputs.ARTIFACT_NAME }}.cov diff --git a/.github/workflows/test-random-execution.yml b/.github/workflows/test-random-execution.yml index 14e7b9448430..a98d50cec3ad 100644 --- a/.github/workflows/test-random-execution.yml +++ b/.github/workflows/test-random-execution.yml @@ -231,7 +231,7 @@ jobs: - name: Upload random-test artifacts on failure if: ${{ always() && failure() }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: random-tests-${{ matrix.db-platform }}-php${{ matrix.php-version }} path: build/random-tests/ From 9bd153cb3433a07e85b3264c08a7f78813345a62 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 18 Oct 2026 10:56:38 +0200 Subject: [PATCH 14/52] chore(deps): bump actions/download-artifact in / (#10633) Bumps [actions/download-artifact](https://github.com/actions/download-artifact) in `/` from 8.0.1 to 8.0.2. Updates `actions/download-artifact` from 8.0.1 to 8.0.2 - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c...9000827ccba6bdab643e8b6fd33ac0654aef8333) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: 8.0.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github_actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/reusable-coveralls.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/reusable-coveralls.yml b/.github/workflows/reusable-coveralls.yml index 555625db5066..687b2a919071 100644 --- a/.github/workflows/reusable-coveralls.yml +++ b/.github/workflows/reusable-coveralls.yml @@ -29,7 +29,7 @@ jobs: coverage: xdebug - name: Download coverage files - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: path: build/cov From a245d0045a0ad44f4a90524138e2e873d501c25e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 18 Oct 2026 10:56:49 +0200 Subject: [PATCH 15/52] chore(deps): bump actions/setup-node in / (#10634) Bumps [actions/setup-node](https://github.com/actions/setup-node) in `/` from 7.0.0 to 7.1.0. Updates `actions/setup-node` from 7.0.0 to 7.1.0 - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/820762786026740c76f36085b0efc47a31fe5020...949feb2413d6458794dcd2491c4babbbce0c15c1) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: 7.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github_actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test-scss.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-scss.yml b/.github/workflows/test-scss.yml index 0000e700a422..57157785d600 100644 --- a/.github/workflows/test-scss.yml +++ b/.github/workflows/test-scss.yml @@ -36,7 +36,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Node - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: '24' From 1c0920da446fd0b15593b3874f4ed978a9a60e65 Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Sun, 18 Oct 2026 16:57:29 +0200 Subject: [PATCH 16/52] fix: handle invalid values and empty tag names in `XMLFormatter` (#10626) --- system/Format/Exceptions/FormatException.php | 11 +++++ system/Format/XMLFormatter.php | 11 ++++- system/Language/en/Format.php | 1 + tests/system/Format/XMLFormatterTest.php | 48 ++++++++++++++++++++ user_guide_src/source/changelogs/v4.7.6.rst | 7 +++ 5 files changed, 76 insertions(+), 2 deletions(-) diff --git a/system/Format/Exceptions/FormatException.php b/system/Format/Exceptions/FormatException.php index cde333b1292c..7c7e2168c69a 100644 --- a/system/Format/Exceptions/FormatException.php +++ b/system/Format/Exceptions/FormatException.php @@ -57,6 +57,17 @@ public static function forInvalidMime(string $mime) return new static(lang('Format.invalidMime', [$mime])); } + /** + * Thrown in XMLFormatter when a value cannot be converted + * to a string. + * + * @return static + */ + public static function forInvalidXMLValue(string $type) + { + return new static(lang('Format.invalidXMLValue', [$type])); + } + /** * Thrown on XMLFormatter when the `simplexml` extension * is not installed. diff --git a/system/Format/XMLFormatter.php b/system/Format/XMLFormatter.php index 5ef2908858b7..3a2e8a6f20a9 100644 --- a/system/Format/XMLFormatter.php +++ b/system/Format/XMLFormatter.php @@ -16,6 +16,7 @@ use CodeIgniter\Format\Exceptions\FormatException; use Config\Format; use SimpleXMLElement; +use Stringable; /** * XML data formatter @@ -68,7 +69,11 @@ protected function arrayToXML(array $data, &$output) $subnode = $output->addChild("{$key}"); $this->arrayToXML($value, $subnode); } else { - $output->addChild("{$key}", htmlspecialchars("{$value}")); + if (is_object($value) && ! $value instanceof Stringable) { + throw FormatException::forInvalidXMLValue($value::class); + } + + $output->addChild("{$key}", htmlspecialchars((string) $value)); } } } @@ -98,6 +103,8 @@ protected function normalizeXMLTag($key) $key = preg_replace("/[^{$validName}-]+/u", '', $key); $key = preg_replace("/^[^{$startChar}]+/u", 'item$0', $key); - return preg_replace('/^(xml).*/iu', 'item$0', $key); // XML is a reserved starting word + $key = preg_replace('/^(xml).*/iu', 'item$0', $key); // XML is a reserved starting word + + return $key === '' ? 'item' : $key; } } diff --git a/system/Language/en/Format.php b/system/Language/en/Format.php index 7d792e2b6128..1f217798afe3 100644 --- a/system/Language/en/Format.php +++ b/system/Language/en/Format.php @@ -16,5 +16,6 @@ 'invalidFormatter' => '"{0}" is not a valid Formatter class.', 'invalidJSON' => 'Failed to parse JSON string. Error: {0}', 'invalidMime' => 'No Formatter defined for mime type: "{0}".', + 'invalidXMLValue' => 'Cannot convert the value of type "{0}" to a string for XML output.', 'missingExtension' => 'The SimpleXML extension is required to format XML.', ]; diff --git a/tests/system/Format/XMLFormatterTest.php b/tests/system/Format/XMLFormatterTest.php index 107418a5d68c..f86aa95d1feb 100644 --- a/tests/system/Format/XMLFormatterTest.php +++ b/tests/system/Format/XMLFormatterTest.php @@ -13,10 +13,13 @@ namespace CodeIgniter\Format; +use CodeIgniter\Format\Exceptions\FormatException; use CodeIgniter\Test\CIUnitTestCase; use DOMDocument; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; +use stdClass; +use Stringable; /** * @internal @@ -152,6 +155,51 @@ public static function provideValidatingInvalidTags(): iterable ]; } + public function testEmptyTagNameAfterSanitization(): void + { + $data = [ + '!@#' => 'value', + ]; + + $expected = <<<'EOH' + + value + + EOH; + + $this->assertSame($expected, $this->xmlFormatter->format($data)); + } + + public function testStringableObjectValue(): void + { + $value = new class () implements Stringable { + public function __toString(): string + { + return 'bar'; + } + }; + + $data = [ + 'foo' => $value, + ]; + + $expected = <<<'EOH' + + bar + + EOH; + + $this->assertSame($expected, $this->xmlFormatter->format($data)); + } + + public function testNonStringableObjectValueThrows(): void + { + $this->expectException(FormatException::class); + $this->expectExceptionMessage('Cannot convert the value of type "stdClass" to a string for XML output.'); + + $this->xmlFormatter->format(['foo' => new stdClass()]); + } + public function testDeepNestedArrayToXml(): void { $data = [ diff --git a/user_guide_src/source/changelogs/v4.7.6.rst b/user_guide_src/source/changelogs/v4.7.6.rst index 360221806c4f..029fa79286e5 100644 --- a/user_guide_src/source/changelogs/v4.7.6.rst +++ b/user_guide_src/source/changelogs/v4.7.6.rst @@ -18,6 +18,8 @@ BREAKING Message Changes *************** +- Added the ``Format.invalidXMLValue`` language string. + ******* Changes ******* @@ -31,6 +33,11 @@ Bugs Fixed ********** - **Commands:** Fixed a bug where generator commands did not sort the imports of a template with CRLF line endings. +- **Format:** Fixed ``XMLFormatter`` so that an array key that becomes empty after + sanitization no longer triggers a ``ValueError``; the fallback tag name ``item`` + is used instead. Values that cannot be converted to a string, such as objects + not implementing ``Stringable``, now throw a ``FormatException`` instead of a + fatal ``Error``. - **Security:** Fixed a bug where a valid JSON request body that is not an object (for example a JSON array) was corrupted after CSRF verification when the token was sent via the ``X-CSRF-TOKEN`` header. From da4e508403a9af9d5a6f997015a5e94b19a6d1cd Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Sun, 18 Oct 2026 16:58:14 +0200 Subject: [PATCH 17/52] fix: detect `X-Forwarded-Proto` case-insensitively in `isSecure()` (#10627) --- system/HTTP/IncomingRequest.php | 14 +++++++-- tests/system/HTTP/IncomingRequestTest.php | 32 +++++++++++++++++++++ user_guide_src/source/changelogs/v4.7.6.rst | 4 +++ 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index 10b4678ffa6a..f3063bcb6093 100644 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -274,8 +274,18 @@ public function isSecure(): bool return false; } - if ($this->hasHeader('X-Forwarded-Proto') && $this->header('X-Forwarded-Proto')->getValue() === 'https') { - return true; + if ($this->hasHeader('X-Forwarded-Proto')) { + $proto = $this->header('X-Forwarded-Proto')->getValue(); + + // A single value may be a string or a one-element array. Multiple + // values are ambiguous and cannot be trusted, so they are ignored. + if (is_array($proto)) { + $proto = count($proto) === 1 ? reset($proto) : null; + } + + if (is_string($proto) && strtolower(trim($proto)) === 'https') { + return true; + } } if (! $this->hasHeader('Front-End-Https')) { diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index 464b46e2ce2c..89646081e53c 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -828,6 +828,12 @@ public static function provideIsSecureWithForwardedHeaders(): iterable 'X-Forwarded-Proto trusted proxy IP' => [ 'X-Forwarded-Proto', 'https', '10.0.1.200', ['10.0.1.200' => 'X-Forwarded-For'], true, ], + 'X-Forwarded-Proto trusted proxy IP uppercase' => [ + 'X-Forwarded-Proto', 'HTTPS', '10.0.1.200', ['10.0.1.200' => 'X-Forwarded-For'], true, + ], + 'X-Forwarded-Proto trusted proxy IP with surrounding whitespace' => [ + 'X-Forwarded-Proto', ' https ', '10.0.1.200', ['10.0.1.200' => 'X-Forwarded-For'], true, + ], 'X-Forwarded-Proto no trusted proxies' => [ 'X-Forwarded-Proto', 'https', '10.0.1.200', [], false, ], @@ -879,6 +885,32 @@ public static function provideIsSecureWithForwardedHeaders(): iterable ]; } + public function testIsSecureWithForwardedProtoSingleElementArray(): void + { + service('superglobals')->setServer('REMOTE_ADDR', '10.0.1.200'); + + $config = new App(); + $config->proxyIPs = ['10.0.1.200' => 'X-Forwarded-For']; + + $request = $this->createRequest($config); + $request->setHeader('X-Forwarded-Proto', ['https']); + + $this->assertTrue($request->isSecure()); + } + + public function testIsSecureWithForwardedProtoMultipleValuesIsNotSecure(): void + { + service('superglobals')->setServer('REMOTE_ADDR', '10.0.1.200'); + + $config = new App(); + $config->proxyIPs = ['10.0.1.200' => 'X-Forwarded-For']; + + $request = $this->createRequest($config); + $request->setHeader('X-Forwarded-Proto', ['https', 'http']); + + $this->assertFalse($request->isSecure()); + } + public function testUserAgent(): void { service('superglobals')->setServer('HTTP_USER_AGENT', 'Mozilla'); diff --git a/user_guide_src/source/changelogs/v4.7.6.rst b/user_guide_src/source/changelogs/v4.7.6.rst index 029fa79286e5..8739e9546d4c 100644 --- a/user_guide_src/source/changelogs/v4.7.6.rst +++ b/user_guide_src/source/changelogs/v4.7.6.rst @@ -41,6 +41,10 @@ Bugs Fixed - **Security:** Fixed a bug where a valid JSON request body that is not an object (for example a JSON array) was corrupted after CSRF verification when the token was sent via the ``X-CSRF-TOKEN`` header. +- **HTTP:** Fixed a bug where ``IncomingRequest::isSecure()`` did not + recognize ``https`` in the ``X-Forwarded-Proto`` header from a trusted + proxy when the value contained uppercase letters or surrounding + whitespace, or was represented as a single-element array. See the repo's `CHANGELOG.md `_ From b74fff9de0da5a5b6c7b81b3f788031c99c46750 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Mon, 19 Oct 2026 13:48:09 +0800 Subject: [PATCH 18/52] chore: fix the composer normalize check and remove PhpMetrics (#10640) --- .gitattributes | 1 - .github/workflows/test-autoreview.yml | 76 ------------------- .github/workflows/test-composer-normalize.yml | 54 +++++++++++++ composer.json | 2 - phpmetrics.json | 7 -- utils/composer.json | 1 - 6 files changed, 54 insertions(+), 87 deletions(-) create mode 100644 .github/workflows/test-composer-normalize.yml delete mode 100644 phpmetrics.json diff --git a/.gitattributes b/.gitattributes index 02a23de3322d..0ec474bf866d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -24,7 +24,6 @@ utils/ export-ignore .php-cs-fixer.user-guide.php export-ignore structarmed-baseline.php export-ignore structarmed.php export-ignore -phpmetrics.json export-ignore phpstan-baseline.php export-ignore phpstan-bootstrap.php export-ignore phpstan.dist.neon export-ignore diff --git a/.github/workflows/test-autoreview.yml b/.github/workflows/test-autoreview.yml index 7793a3cc091b..26b50e049208 100644 --- a/.github/workflows/test-autoreview.yml +++ b/.github/workflows/test-autoreview.yml @@ -30,79 +30,3 @@ jobs: php-version: '8.2' job-id: auto-review-tests group-name: AutoReview - - composer-normalize-tests: - name: Check normalized composer.json - runs-on: ubuntu-24.04 - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Setup PHP - uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 - with: - php-version: '8.2' - - - name: Install dependencies - run: composer update - - - name: Run on root composer.json - id: normalize-root-composer - if: ${{ always() }} - run: | - echo "ROOT_COMPOSER_RESULT<> $GITHUB_OUTPUT - echo "$(composer normalize)" >> $GITHUB_OUTPUT - echo "RCR" >> $GITHUB_OUTPUT - - - name: Run on framework composer.json - id: normalize-framework-composer - if: ${{ always() }} - working-directory: admin/framework - run: | - echo "FRAMEWORK_COMPOSER_RESULT<> $GITHUB_OUTPUT - echo "$(composer normalize)" >> $GITHUB_OUTPUT - echo "FCR" >> $GITHUB_OUTPUT - - - name: Run on starter composer.json - id: normalize-starter-composer - if: ${{ always() }} - working-directory: admin/starter - run: | - echo "STARTER_COMPOSER_RESULT<> $GITHUB_OUTPUT - echo "$(composer normalize)" >> $GITHUB_OUTPUT - echo "SCR" >> $GITHUB_OUTPUT - - - name: Analyse normalization results - run: | - if [[ '${{ steps.normalize-root-composer.conclusion }}' == 'failure' ]]; then - echo 'Normalization of root composer.json encountered a problem.'; - echo 'Please run it locally: `composer normalize`'; - exit 1; - fi - - if [[ ${{ contains(steps.normalize-root-composer.outputs.ROOT_COMPOSER_RESULT, 'Successfully normalized') }} == true ]]; then - echo 'Root composer.json is not yet normalized.'; - exit 1; - fi - - if [[ '${{ steps.normalize-framework-composer.conclusion }}' == 'failure' ]]; then - echo 'Normalization of framework composer.json encountered a problem.'; - echo 'Please run it locally: `composer normalize -d admin/framework`'; - exit 1; - fi - - if [[ ${{ contains(steps.normalize-framework-composer.outputs.FRAMEWORK_COMPOSER_RESULT, 'Successfully normalized') }} == true ]]; then - echo 'Framework composer.json is not yet normalized.'; - exit 1; - fi - - if [[ '${{ steps.normalize-starter-composer.conclusion }}' == 'failure' ]]; then - echo 'Normalization of starter composer.json encountered a problem.'; - echo 'Please run it locally: `composer normalize -d admin/starter`'; - exit 1; - fi - - if [[ ${{ contains(steps.normalize-starter-composer.outputs.STARTER_COMPOSER_RESULT, 'Successfully normalized') }} == true ]]; then - echo 'Starter composer.json is not yet normalized.'; - exit 1; - fi diff --git a/.github/workflows/test-composer-normalize.yml b/.github/workflows/test-composer-normalize.yml new file mode 100644 index 000000000000..164c2424976c --- /dev/null +++ b/.github/workflows/test-composer-normalize.yml @@ -0,0 +1,54 @@ +name: Composer Normalize + +on: + pull_request: + paths: + - '**/composer.json' + - '.github/workflows/test-composer-normalize.yml' + push: + paths: + - '**/composer.json' + - '.github/workflows/test-composer-normalize.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + composer-normalize: + name: Check normalized composer.json + runs-on: ubuntu-24.04 + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup PHP + uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 + with: + php-version: '8.2' + coverage: none + + - name: Install dependencies + run: composer update --ansi --no-interaction --working-dir=utils + + - name: Run on root composer.json + if: ${{ !cancelled() }} + run: composer normalize --ansi --dry-run --working-dir=utils "$GITHUB_WORKSPACE/composer.json" + + - name: Run on framework composer.json + if: ${{ !cancelled() }} + run: composer normalize --ansi --dry-run --working-dir=utils "$GITHUB_WORKSPACE/admin/framework/composer.json" + + - name: Run on starter composer.json + if: ${{ !cancelled() }} + run: composer normalize --ansi --dry-run --working-dir=utils "$GITHUB_WORKSPACE/admin/starter/composer.json" + + - name: Run on utils composer.json + if: ${{ !cancelled() }} + run: composer normalize --ansi --dry-run --working-dir=utils diff --git a/composer.json b/composer.json index 3fbec754b543..1ab6a1686f01 100644 --- a/composer.json +++ b/composer.json @@ -114,7 +114,6 @@ "utils/vendor/bin/php-cs-fixer fix --ansi --verbose --diff --config=.php-cs-fixer.tests.php", "utils/vendor/bin/php-cs-fixer fix --ansi --verbose --diff" ], - "metrics": "utils/vendor/bin/phpmetrics --config=phpmetrics.json", "phpstan:baseline": [ "phpstan analyse --ansi --generate-baseline=utils/phpstan-baseline/loader.neon", "split-phpstan-baseline utils/phpstan-baseline/loader.neon" @@ -128,7 +127,6 @@ "analyze": "Run static analysis", "cs": "Check the coding style", "cs-fix": "Fix the coding style", - "metrics": "Run PhpMetrics", "phpstan:baseline": "Run PHPStan then dump all errors to baseline", "phpstan:check": "Run PHPStan with support for identifiers", "test": "Run unit tests" diff --git a/phpmetrics.json b/phpmetrics.json deleted file mode 100644 index 3ff72a669193..000000000000 --- a/phpmetrics.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "includes": ["system"], - "excludes": ["ThirdParty"], - "report": { - "html": "build/phpmetrics/" - } -} diff --git a/utils/composer.json b/utils/composer.json index b5cd6aa43a55..9b5250df0352 100644 --- a/utils/composer.json +++ b/utils/composer.json @@ -5,7 +5,6 @@ "ergebnis/composer-normalize": "^2.28", "friendsofphp/php-cs-fixer": "^3.76", "nexusphp/cs-config": "^3.26", - "phpmetrics/phpmetrics": "^2.8 || ^3.0rc6", "vimeo/psalm": "^6.14" }, "autoload": { From df8b96bee0a52472d323b589bb4cb505de579d5a Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Wed, 21 Oct 2026 23:19:19 +0800 Subject: [PATCH 19/52] test: accept MariaDB version strings in `GetVersionTest` (#10642) --- tests/system/Database/Live/GetVersionTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/system/Database/Live/GetVersionTest.php b/tests/system/Database/Live/GetVersionTest.php index cb4e5c1f5386..723d6b2d0c9c 100644 --- a/tests/system/Database/Live/GetVersionTest.php +++ b/tests/system/Database/Live/GetVersionTest.php @@ -39,6 +39,6 @@ public function testGetVersion(): void $version = $this->db->getVersion(); - $this->assertMatchesRegularExpression('/\A\d+(\.\d+)*\z/', $version); + $this->assertMatchesRegularExpression('/\A\d+(\.\d+)*(-\S+)?\z/', $version); } } From 5b08e887acc90d6e74e5ec54d638f327d15d69d5 Mon Sep 17 00:00:00 2001 From: Rahul Date: Fri, 23 Oct 2026 11:12:30 +0530 Subject: [PATCH 20/52] fix: refresh `$_REQUEST` after URI GET normalization (#10606) --- system/HTTP/SiteURIFactory.php | 48 +++++++-- system/Superglobals.php | 45 ++++++++ .../SiteURIFactoryDetectRoutePathTest.php | 93 ++++++++++++++++ tests/system/SuperglobalsTest.php | 100 ++++++++++++++++++ user_guide_src/source/changelogs/v4.7.6.rst | 7 +- 5 files changed, 284 insertions(+), 9 deletions(-) diff --git a/system/HTTP/SiteURIFactory.php b/system/HTTP/SiteURIFactory.php index b1def3293a17..3533b8383c6a 100644 --- a/system/HTTP/SiteURIFactory.php +++ b/system/HTTP/SiteURIFactory.php @@ -20,7 +20,7 @@ /** * Creates SiteURI using superglobals. * - * This class also updates superglobal $_SERVER and $_GET. + * This class also updates superglobal $_REQUEST, $_SERVER, and $_GET. */ final readonly class SiteURIFactory { @@ -31,7 +31,7 @@ public function __construct(private App $appConfig, private Superglobals $superg /** * Create the current URI object from superglobals. * - * This method updates superglobal $_SERVER and $_GET. + * This method updates superglobals $_SERVER, $_GET, and $_REQUEST. */ public function createFromGlobals(): SiteURI { @@ -103,7 +103,7 @@ public function detectRoutePath(string $protocol = ''): string * Will parse the REQUEST_URI and automatically detect the URI from it, * fixing the query string if necessary. * - * This method updates superglobal $_SERVER and $_GET. + * This method updates superglobals $_SERVER, $_GET, and $_REQUEST. * * @return string The route path (before normalization). */ @@ -167,9 +167,11 @@ private function parseRequestURI(): string $this->superglobals->setServer('QUERY_STRING', $query); } - // Update our global GET for values likely to have been changed + // Refresh only the request values derived from superglobals after GET changes. + $previousRequestData = $this->superglobals->getRequestData(); parse_str($this->superglobals->server('QUERY_STRING'), $get); $this->superglobals->setGetArray($get); + $this->refreshRequestAfterGetChange($previousRequestData); return URI::removeDotSegments($path); } @@ -177,7 +179,7 @@ private function parseRequestURI(): string /** * Will parse QUERY_STRING and automatically detect the URI from it. * - * This method updates superglobal $_SERVER and $_GET. + * This method updates superglobals $_SERVER, $_GET, and $_REQUEST. * * @return string The route path (before normalization). */ @@ -199,9 +201,11 @@ private function parseQueryString(): string $path = $query; } - // Update our global GET for values likely to have been changed + // Refresh only the request values derived from superglobals after GET changes. + $previousRequestData = $this->superglobals->getRequestData(); parse_str($this->superglobals->server('QUERY_STRING'), $get); $this->superglobals->setGetArray($get); + $this->refreshRequestAfterGetChange($previousRequestData); return URI::removeDotSegments($path); } @@ -220,6 +224,38 @@ private function createURIFromRoutePath(string $routePath): SiteURI return new SiteURI($this->appConfig, $relativePath, $this->getHost()); } + /** + * Update REQUEST once during URI parsing, leaving application-defined values alone. + * + * @param array $previousRequestData + */ + private function refreshRequestAfterGetChange(array $previousRequestData): void + { + $request = $this->superglobals->getRequestArray(); + $currentData = $this->superglobals->getRequestData(); + + foreach ($previousRequestData as $key => $value) { + // A different value was supplied directly in REQUEST; preserve it. + if (! array_key_exists($key, $request) || $request[$key] !== $value) { + continue; + } + + if (array_key_exists($key, $currentData)) { + $request[$key] = $currentData[$key]; + } else { + unset($request[$key]); + } + } + + foreach ($currentData as $key => $value) { + if (! array_key_exists($key, $previousRequestData) && ! array_key_exists($key, $request)) { + $request[$key] = $value; + } + } + + $this->superglobals->setRequestArray($request); + } + /** * @return string|null The current hostname. Returns null if no valid host. */ diff --git a/system/Superglobals.php b/system/Superglobals.php index 461a41199104..7049d035785f 100644 --- a/system/Superglobals.php +++ b/system/Superglobals.php @@ -386,6 +386,51 @@ public function setRequestArray(array $array): self return $this; } + /** + * Returns the merged $_GET, $_POST, and $_COOKIE data according to the + * `request_order` (or `variables_order`) ini setting, without mutating + * $_REQUEST. + * + * PHP populates $_REQUEST only once at the start of the request. When + * $_GET is modified later (e.g. by SiteURIFactory), $_REQUEST becomes + * stale. This method returns the current merged values so callers can + * read up-to-date request data without relying on the stale $_REQUEST. + * + * @param string|null $requestOrder Overrides the `request_order` ini + * setting. Useful for testing, since the + * ini setting cannot be changed at runtime. + * + * @return array + */ + public function getRequestData(?string $requestOrder = null): array + { + $requestOrder ??= (string) ini_get('request_order'); + + if ($requestOrder === '') { + $requestOrder = (string) ini_get('variables_order'); + } + + if ($requestOrder === '') { + $requestOrder = 'GP'; + } + + $request = []; + + foreach (array_unique(str_split(strtoupper($requestOrder))) as $type) { + match ($type) { + // array_replace_recursive() matches PHP's own $_REQUEST merge + // (php_autoglobal_merge): numeric keys are preserved and + // array values are merged recursively. + 'G' => $request = array_replace_recursive($request, $this->get), + 'P' => $request = array_replace_recursive($request, $this->post), + 'C' => $request = array_replace_recursive($request, $this->cookie), + default => null, + }; + } + + return $request; + } + /** * Get all $_FILES values. * diff --git a/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php b/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php index 4bc29fbecc4d..a61f19a2fefe 100644 --- a/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php +++ b/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php @@ -258,6 +258,99 @@ public function testQueryStringWithQueryString(): void $this->assertSame(['code' => 'good'], $_GET); } + public function testQueryStringRefreshesRequestWithoutOverwritingCustomValues(): void + { + if (! $this->effectiveRequestOrderIncludesGet()) { + $this->markTestSkipped('The effective request order does not include GET.'); + } + + $superglobals = new Superglobals( + [ + 'REQUEST_URI' => '/index.php?/ci/woot?code=good', + 'QUERY_STRING' => '/ci/woot?code=good', + 'SCRIPT_NAME' => '/index.php', + ], + ['/ci/woot?code' => 'good', 'override' => 'from get'], + ['posted' => 'post'], + [], + [], + [ + '/ci/woot?code' => 'good', + 'override' => 'custom', + 'posted' => 'post', + 'unrelated' => 'keep', + ], + ); + Services::injectMock('superglobals', $superglobals); + + $factory = new SiteURIFactory(new App(), $superglobals); + + $this->assertSame('ci/woot', $factory->detectRoutePath('QUERY_STRING')); + $this->assertSame([ + 'override' => 'custom', + 'posted' => 'post', + 'unrelated' => 'keep', + 'code' => 'good', + ], $superglobals->getRequestArray()); + $this->assertSame($superglobals->getRequestArray(), $_REQUEST); + + $superglobals->setGet('code', 'later'); + + $this->assertSame('good', $superglobals->request('code')); + } + + public function testRequestURIRefreshesRequestForValidation(): void + { + if (! $this->effectiveRequestOrderIncludesGet()) { + $this->markTestSkipped('The effective request order does not include GET.'); + } + + $superglobals = new Superglobals( + [ + 'REQUEST_URI' => '/index.php/woot?code=good', + 'SCRIPT_NAME' => '/index.php', + ], + ['code' => 'stale'], + [], + [], + [], + ['code' => 'stale', 'unrelated' => 'keep'], + ); + Services::injectMock('superglobals', $superglobals); + + $config = new App(); + $factory = new SiteURIFactory($config, $superglobals); + + $this->assertSame('woot', $factory->detectRoutePath('REQUEST_URI')); + $this->assertSame(['code' => 'good', 'unrelated' => 'keep'], $superglobals->getRequestArray()); + + $request = new IncomingRequest($config, new SiteURI($config), null, new UserAgent()); + + $this->assertSame('good', $request->getVar('code')); + $this->assertTrue(service('validation')->withRequest($request)->setRules(['code' => 'in_list[good]'])->run()); + } + + /** + * Whether GET is part of the effective request order on this host. + * + * Mirrors Superglobals::getRequestData(): uses `request_order`, falls + * back to `variables_order`, then to PHP's default GP. + */ + private function effectiveRequestOrderIncludesGet(): bool + { + $order = (string) ini_get('request_order'); + + if ($order === '') { + $order = (string) ini_get('variables_order'); + } + + if ($order === '') { + $order = 'GP'; + } + + return str_contains(strtoupper($order), 'G'); + } + public function testQueryStringEmpty(): void { // /index.php? diff --git a/tests/system/SuperglobalsTest.php b/tests/system/SuperglobalsTest.php index e4b8b23b2b28..2aa24f5b64c5 100644 --- a/tests/system/SuperglobalsTest.php +++ b/tests/system/SuperglobalsTest.php @@ -302,6 +302,106 @@ public function testRequestSetArray(): void $this->assertSame($data, $_REQUEST); } + public function testGetRequestDataMergesGetAndPost(): void + { + $this->superglobals->setGetArray(['get_key' => 'get_value']); + $this->superglobals->setPostArray(['post_key' => 'post_value']); + + $data = $this->superglobals->getRequestData('GP'); + + $this->assertSame('get_value', $data['get_key']); + $this->assertSame('post_value', $data['post_key']); + } + + public function testGetRequestDataReflectsGetChanges(): void + { + $this->superglobals->setGetArray(['key' => 'old']); + + $this->assertSame('old', $this->superglobals->getRequestData('G')['key']); + + // Simulate SiteURIFactory updating $_GET after the request started. + $this->superglobals->setGetArray(['key' => 'new']); + + $this->assertSame('new', $this->superglobals->getRequestData('G')['key']); + } + + public function testGetRequestDataMergesCookie(): void + { + $this->superglobals->setGetArray(['get_key' => 'get_value']); + $this->superglobals->setPostArray(['post_key' => 'post_value']); + $this->superglobals->setCookieArray(['cookie_key' => 'cookie_value']); + + $data = $this->superglobals->getRequestData('GPC'); + + $this->assertSame('get_value', $data['get_key']); + $this->assertSame('post_value', $data['post_key']); + $this->assertSame('cookie_value', $data['cookie_key']); + } + + public function testGetRequestDataRespectsOrder(): void + { + $this->superglobals->setGetArray(['shared' => 'get']); + $this->superglobals->setPostArray(['shared' => 'post']); + $this->superglobals->setCookieArray(['shared' => 'cookie']); + + // Later sources overwrite earlier ones, matching PHP's request_order. + $this->assertSame('post', $this->superglobals->getRequestData('GP')['shared']); + $this->assertSame('cookie', $this->superglobals->getRequestData('GPC')['shared']); + $this->assertSame('get', $this->superglobals->getRequestData('PG')['shared']); + } + + public function testGetRequestDataEmptyOrderFallsBackToVariablesOrder(): void + { + $this->superglobals->setGetArray(['get' => 'value']); + $this->superglobals->setPostArray(['post' => 'value']); + $this->superglobals->setCookieArray(['cookie' => 'value']); + + $this->assertSame( + $this->superglobals->getRequestData((string) ini_get('variables_order')), + $this->superglobals->getRequestData(''), + ); + } + + public function testGetRequestDataNormalizesOrderAndIgnoresDuplicates(): void + { + $this->superglobals->setGetArray(['shared' => 'get']); + $this->superglobals->setPostArray(['shared' => 'post']); + $this->superglobals->setCookieArray(['shared' => 'cookie']); + + $this->assertSame('post', $this->superglobals->getRequestData('gp')['shared']); + $this->assertSame('post', $this->superglobals->getRequestData('GPG')['shared']); + $this->assertSame('cookie', $this->superglobals->getRequestData('gPcGpC')['shared']); + } + + public function testGetRequestDataIgnoresUnknownOrderTypes(): void + { + $this->superglobals->setGetArray(['get_key' => 'get_value']); + + $data = $this->superglobals->getRequestData('GX'); + + $this->assertSame(['get_key' => 'get_value'], $data); + } + + public function testGetRequestDataPreservesNumericKeys(): void + { + parse_str('100=foo', $get); + $this->superglobals->setGetArray($get); + + $data = $this->superglobals->getRequestData('G'); + + $this->assertSame([100 => 'foo'], $data); + } + + public function testGetRequestDataMergesRecursively(): void + { + $this->superglobals->setGetArray(['a' => ['x' => 'get']]); + $this->superglobals->setPostArray(['a' => ['y' => 'post']]); + + $data = $this->superglobals->getRequestData('GP'); + + $this->assertSame(['a' => ['x' => 'get', 'y' => 'post']], $data); + } + // $_FILES tests public function testFilesGetArray(): void { diff --git a/user_guide_src/source/changelogs/v4.7.6.rst b/user_guide_src/source/changelogs/v4.7.6.rst index 8739e9546d4c..dc63b43dc354 100644 --- a/user_guide_src/source/changelogs/v4.7.6.rst +++ b/user_guide_src/source/changelogs/v4.7.6.rst @@ -38,13 +38,14 @@ Bugs Fixed is used instead. Values that cannot be converted to a string, such as objects not implementing ``Stringable``, now throw a ``FormatException`` instead of a fatal ``Error``. -- **Security:** Fixed a bug where a valid JSON request body that is not an object - (for example a JSON array) was corrupted after CSRF verification when the - token was sent via the ``X-CSRF-TOKEN`` header. +- **HTTP:** Fixed stale ``$_REQUEST`` values after ``SiteURIFactory`` normalizes GET parameters, so ``Validation::withRequest()`` reads the normalized values while unrelated REQUEST entries are preserved. - **HTTP:** Fixed a bug where ``IncomingRequest::isSecure()`` did not recognize ``https`` in the ``X-Forwarded-Proto`` header from a trusted proxy when the value contained uppercase letters or surrounding whitespace, or was represented as a single-element array. +- **Security:** Fixed a bug where a valid JSON request body that is not an object + (for example a JSON array) was corrupted after CSRF verification when the + token was sent via the ``X-CSRF-TOKEN`` header. See the repo's `CHANGELOG.md `_ From 6ddf104caec59f4e71164c9fe389a2423d750b33 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Fri, 23 Oct 2026 17:06:19 +0800 Subject: [PATCH 21/52] fix: widen accepted values in `set_value()`'s `$default` (#10641) * fix: widen accepted types in `set_value()`'s `$default` * docs: reflow changelog --- system/Helpers/form_helper.php | 19 +++++------ tests/system/Helpers/FormHelperTest.php | 34 +++++++++++++++++++ user_guide_src/source/changelogs/v4.7.6.rst | 16 +++------ user_guide_src/source/helpers/form_helper.rst | 4 +-- 4 files changed, 49 insertions(+), 24 deletions(-) diff --git a/system/Helpers/form_helper.php b/system/Helpers/form_helper.php index dee1f001365b..b030b2f06009 100644 --- a/system/Helpers/form_helper.php +++ b/system/Helpers/form_helper.php @@ -11,6 +11,7 @@ * the LICENSE file that was distributed with this source code. */ +use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\Validation\Exceptions\ValidationException; use Config\App; use Config\Validation; @@ -547,27 +548,25 @@ function form_close(string $extra = ''): string if (! function_exists('set_value')) { /** - * Form Value - * * Grabs a value from the POST array for the specified field so you can * re-populate an input field or textarea * - * @param string $field Field name - * @param list|string $default Default value - * @param bool $htmlEscape Whether to escape HTML special characters or not + * @param string $field Field name + * @param list|scalar|Stringable|null $default Default value + * @param bool $htmlEscape Whether to escape HTML special characters or not * * @return list|string */ function set_value(string $field, $default = '', bool $htmlEscape = true): array|string { + /** @var IncomingRequest $request */ $request = service('request'); - // Try any old input data we may have first - $value = $request->getOldInput($field); - - $value ??= $request->getPost($field) ?? $default; + $value = $request->getOldInput($field) + ?? $request->getPost($field) + ?? (is_array($default) ? $default : (string) $default); - return ($htmlEscape) ? esc($value) : $value; + return $htmlEscape ? esc($value) : $value; } } diff --git a/tests/system/Helpers/FormHelperTest.php b/tests/system/Helpers/FormHelperTest.php index c24d34b04a3e..7ed8c8668123 100644 --- a/tests/system/Helpers/FormHelperTest.php +++ b/tests/system/Helpers/FormHelperTest.php @@ -22,10 +22,12 @@ use Config\Filters; use Config\Services; use PHPUnit\Framework\Attributes\BackupGlobals; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\PreserveGlobalState; use PHPUnit\Framework\Attributes\RunInSeparateProcess; use PHPUnit\Framework\Attributes\WithoutErrorHandler; +use Stringable; /** * @internal @@ -867,6 +869,38 @@ public function testSetValue(): void $this->assertSame('baz', set_value('foo', 'baz')); } + #[DataProvider('provideSetValueCastsNonArrayDefaultToString')] + public function testSetValueCastsNonArrayDefaultToString(mixed $default, string $expected): void + { + $this->assertSame($expected, set_value('foo', $default)); + } + + /** + * @return iterable + */ + public static function provideSetValueCastsNonArrayDefaultToString(): iterable + { + $stringable = self::createStub(Stringable::class); + $stringable->method('__toString')->willReturn(''); + + yield 'int' => [1, '1']; + + yield 'float' => [1.5, '1.5']; + + yield 'true' => [true, '1']; + + yield 'false' => [false, '']; + + yield 'null' => [null, '']; + + yield 'stringable' => [$stringable, '<bar>']; + } + + public function testSetValueKeepsArrayDefault(): void + { + $this->assertSame(['a', 'b'], set_value('foo', ['a', 'b'])); + } + public function testSetSelect(): void { $_SESSION['_ci_old_input']['post']['foo'] = 'bar'; diff --git a/user_guide_src/source/changelogs/v4.7.6.rst b/user_guide_src/source/changelogs/v4.7.6.rst index dc63b43dc354..75e6f890047c 100644 --- a/user_guide_src/source/changelogs/v4.7.6.rst +++ b/user_guide_src/source/changelogs/v4.7.6.rst @@ -33,19 +33,11 @@ Bugs Fixed ********** - **Commands:** Fixed a bug where generator commands did not sort the imports of a template with CRLF line endings. -- **Format:** Fixed ``XMLFormatter`` so that an array key that becomes empty after - sanitization no longer triggers a ``ValueError``; the fallback tag name ``item`` - is used instead. Values that cannot be converted to a string, such as objects - not implementing ``Stringable``, now throw a ``FormatException`` instead of a - fatal ``Error``. +- **Format:** Fixed ``XMLFormatter`` so that an array key that becomes empty after sanitization no longer triggers a ``ValueError``; the fallback tag name ``item`` is used instead. Values that cannot be converted to a string, such as objects not implementing ``Stringable``, now throw a ``FormatException`` instead of a fatal ``Error``. +- **Helpers:** Fixed a regression in v4.7.5 where ``set_value()`` threw a ``TypeError`` when the default value was not a string or an array, such as an integer, a float, ``null``, or a ``Stringable`` object. Such values are now converted to a string. - **HTTP:** Fixed stale ``$_REQUEST`` values after ``SiteURIFactory`` normalizes GET parameters, so ``Validation::withRequest()`` reads the normalized values while unrelated REQUEST entries are preserved. -- **HTTP:** Fixed a bug where ``IncomingRequest::isSecure()`` did not - recognize ``https`` in the ``X-Forwarded-Proto`` header from a trusted - proxy when the value contained uppercase letters or surrounding - whitespace, or was represented as a single-element array. -- **Security:** Fixed a bug where a valid JSON request body that is not an object - (for example a JSON array) was corrupted after CSRF verification when the - token was sent via the ``X-CSRF-TOKEN`` header. +- **HTTP:** Fixed a bug where ``IncomingRequest::isSecure()`` did not recognize ``https`` in the ``X-Forwarded-Proto`` header from a trusted proxy when the value contained uppercase letters or surrounding whitespace, or was represented as a single-element array. +- **Security:** Fixed a bug where a valid JSON request body that is not an object (for example a JSON array) was corrupted after CSRF verification when the token was sent via the ``X-CSRF-TOKEN`` header. See the repo's `CHANGELOG.md `_ diff --git a/user_guide_src/source/helpers/form_helper.rst b/user_guide_src/source/helpers/form_helper.rst index 9a2d9f026abb..36d19374a774 100644 --- a/user_guide_src/source/helpers/form_helper.rst +++ b/user_guide_src/source/helpers/form_helper.rst @@ -452,10 +452,10 @@ The following functions are available: .. php:function:: set_value($field[, $default = ''[, $html_escape = true]]) :param string $field: Field name - :param string $default: Default value + :param mixed $default: Default value. An array is returned as is, any other value is converted to a string. :param bool $html_escape: Whether to turn off HTML escaping of the value :returns: Field value - :rtype: string + :rtype: array|string Permits you to set the value of an input or textarea element. You must supply the field name via the first parameter of the function. The From 85e6e80ff15c2b5c36df1ec62ee5503356ce51f0 Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Sun, 25 Oct 2026 09:15:49 +0100 Subject: [PATCH 22/52] fix: fix array handling in `ContentSecurityPolicy::setDefaultSrc()` and add `addDefaultSrc()` (#10635) --- system/HTTP/ContentSecurityPolicy.php | 20 ++++++++- .../system/HTTP/ContentSecurityPolicyTest.php | 44 +++++++++++++++++++ user_guide_src/source/changelogs/v4.7.6.rst | 3 ++ 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/system/HTTP/ContentSecurityPolicy.php b/system/HTTP/ContentSecurityPolicy.php index dfea52a62772..faaa526acc94 100644 --- a/system/HTTP/ContentSecurityPolicy.php +++ b/system/HTTP/ContentSecurityPolicy.php @@ -521,12 +521,30 @@ public function addConnectSrc($uri, ?bool $explicitReporting = null) * @see http://www.w3.org/TR/CSP/#directive-default-src * * @param list|string $uri + */ + public function addDefaultSrc(array|string $uri, ?bool $explicitReporting = null): static + { + $this->addOption($uri, 'defaultSrc', $explicitReporting ?? $this->reportOnly); + + return $this; + } + + /** + * Sets the `default-src` directive, replacing any existing sources. + * + * `default-src` is the URI that is used for many of the settings when + * no other source has been set. + * + * @see http://www.w3.org/TR/CSP/#directive-default-src + * + * @param list|string $uri * * @return $this */ public function setDefaultSrc($uri, ?bool $explicitReporting = null) { - $this->defaultSrc = [(string) $uri => $explicitReporting ?? $this->reportOnly]; + $this->defaultSrc = []; + $this->addDefaultSrc($uri, $explicitReporting); return $this; } diff --git a/tests/system/HTTP/ContentSecurityPolicyTest.php b/tests/system/HTTP/ContentSecurityPolicyTest.php index 780cb99f31ea..a85cd107e7c3 100644 --- a/tests/system/HTTP/ContentSecurityPolicyTest.php +++ b/tests/system/HTTP/ContentSecurityPolicyTest.php @@ -617,6 +617,50 @@ public function testDefaultSrc(): void $this->assertContains('default-src iffy.com', $this->getCspDirectives($header)); } + #[PreserveGlobalState(false)] + #[RunInSeparateProcess] + public function testDefaultSrcRich(): void + { + $this->csp->reportOnly(false); + $this->csp->setDefaultSrc(['self', 'maybe.com', 'iffy.com']); + $this->assertTrue($this->work()); + + $header = $this->getHeaderEmitted('Content-Security-Policy'); + $this->assertIsString($header); + $this->assertContains("default-src 'self' maybe.com iffy.com", $this->getCspDirectives($header)); + } + + #[PreserveGlobalState(false)] + #[RunInSeparateProcess] + public function testAddDefaultSrc(): void + { + $this->csp->addDefaultSrc('maybe.com'); + $this->csp->addDefaultSrc('iffy.com', true); + $this->assertTrue($this->work()); + + $header = $this->getHeaderEmitted('Content-Security-Policy-Report-Only'); + $this->assertIsString($header); + $this->assertContains('default-src iffy.com', $this->getCspDirectives($header)); + + $header = $this->getHeaderEmitted('Content-Security-Policy'); + $this->assertIsString($header); + $this->assertContains('default-src maybe.com', $this->getCspDirectives($header)); + } + + #[PreserveGlobalState(false)] + #[RunInSeparateProcess] + public function testAddDefaultSrcAppends(): void + { + $this->csp->reportOnly(false); + $this->csp->setDefaultSrc('maybe.com'); + $this->csp->addDefaultSrc('iffy.com'); + $this->assertTrue($this->work()); + + $header = $this->getHeaderEmitted('Content-Security-Policy'); + $this->assertIsString($header); + $this->assertContains('default-src maybe.com iffy.com', $this->getCspDirectives($header)); + } + #[PreserveGlobalState(false)] #[RunInSeparateProcess] public function testReportURI(): void diff --git a/user_guide_src/source/changelogs/v4.7.6.rst b/user_guide_src/source/changelogs/v4.7.6.rst index 75e6f890047c..843eff4722bd 100644 --- a/user_guide_src/source/changelogs/v4.7.6.rst +++ b/user_guide_src/source/changelogs/v4.7.6.rst @@ -24,6 +24,8 @@ Message Changes Changes ******* +- **HTTP:** Added ``ContentSecurityPolicy::addDefaultSrc()`` to allow appending sources to the ``default-src`` directive. + ************ Deprecations ************ @@ -35,6 +37,7 @@ Bugs Fixed - **Commands:** Fixed a bug where generator commands did not sort the imports of a template with CRLF line endings. - **Format:** Fixed ``XMLFormatter`` so that an array key that becomes empty after sanitization no longer triggers a ``ValueError``; the fallback tag name ``item`` is used instead. Values that cannot be converted to a string, such as objects not implementing ``Stringable``, now throw a ``FormatException`` instead of a fatal ``Error``. - **Helpers:** Fixed a regression in v4.7.5 where ``set_value()`` threw a ``TypeError`` when the default value was not a string or an array, such as an integer, a float, ``null``, or a ``Stringable`` object. Such values are now converted to a string. +- **HTTP:** Fixed a bug in ``ContentSecurityPolicy::setDefaultSrc()`` where passing an array of URIs resulted in an invalid ``'Array'`` directive. - **HTTP:** Fixed stale ``$_REQUEST`` values after ``SiteURIFactory`` normalizes GET parameters, so ``Validation::withRequest()`` reads the normalized values while unrelated REQUEST entries are preserved. - **HTTP:** Fixed a bug where ``IncomingRequest::isSecure()`` did not recognize ``https`` in the ``X-Forwarded-Proto`` header from a trusted proxy when the value contained uppercase letters or surrounding whitespace, or was represented as a single-element array. - **Security:** Fixed a bug where a valid JSON request body that is not an object (for example a JSON array) was corrupted after CSRF verification when the token was sent via the ``X-CSRF-TOKEN`` header. From 62f54202b5d701cd8fc16e0e7e7cb03f564f2c0c Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Sun, 25 Oct 2026 16:24:25 +0800 Subject: [PATCH 23/52] fix: reject missing keys in `CLI::promptByMultipleKeys()` (#10654) --- system/CLI/CLI.php | 66 +++++++-------------- tests/system/CLI/CLITest.php | 43 ++++++++++++++ user_guide_src/source/changelogs/v4.7.6.rst | 1 + 3 files changed, 65 insertions(+), 45 deletions(-) diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index 3fca34c3cdd2..2f5aa9f68844 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -310,61 +310,37 @@ public static function promptByMultipleKeys(string $text, array $options): array { CLI::isZeroOptions($options); - $extraOutputDefault = static::color('0', 'green'); - $opts = $options; - unset($opts[0]); - - if ($opts === []) { - $extraOutput = $extraOutputDefault; - } else { - $optsKey = array_keys($opts); - $extraOutput = '[' . $extraOutputDefault . ', ' . implode(', ', $optsKey) . ']'; - $extraOutput = 'You can specify multiple values separated by commas.' . PHP_EOL . $extraOutput; + $default = (string) array_key_first($options); + $others = $options; + unset($others[$default]); + + $field = static::color($default, 'green'); + + if ($others !== []) { + $field = sprintf( + 'You can specify multiple values separated by commas.%s[%s, %s]', + PHP_EOL, + $field, + implode(', ', array_keys($others)), + ); } CLI::write($text); CLI::printKeysAndValues($options); CLI::newLine(); - $input = static::prompt($extraOutput); - $input = ($input === '') ? '0' : $input; // 0 is default - - // validation while (true) { - $pattern = preg_match_all('/^\d+(,\d+)*$/', trim($input)); - - // separate input by comma and convert all to an int[] - $inputToArray = array_map(static fn ($value): int => (int) $value, explode(',', $input)); - // find max from key of $options - $maxOptions = array_key_last($options); - // find max from input - $maxInput = max($inputToArray); - - // return the prompt again if $input contain(s) non-numeric character, except a comma. - // And if max from $options less than max from input, - // it means user tried to access null value in $options - if ($pattern < 1 || $maxOptions < $maxInput) { - static::error('Please select correctly.'); - CLI::newLine(); - - $input = static::prompt($extraOutput); - $input = ($input === '') ? '0' : $input; - } else { - break; - } - } + $input = static::prompt($field); + $input = $input === '' ? $default : $input; + $keys = array_map(intval(...), explode(',', $input)); - $input = []; - - foreach ($options as $key => $description) { - foreach ($inputToArray as $inputKey) { - if ($key === $inputKey) { - $input[$key] = $description; - } + if (preg_match('/^\d+(,\d+)*$/', $input) === 1 && array_diff($keys, array_keys($options)) === []) { + return array_intersect_key($options, array_flip($keys)); } - } - return $input; + static::error('Please select correctly.'); + CLI::newLine(); + } } // -------------------------------------------------------------------- diff --git a/tests/system/CLI/CLITest.php b/tests/system/CLI/CLITest.php index 9c697b25afb9..2557adbc41b1 100644 --- a/tests/system/CLI/CLITest.php +++ b/tests/system/CLI/CLITest.php @@ -405,6 +405,49 @@ public function testPromptByMultipleKeysInputZero(): void $this->assertSame($expected, $output); } + public function testPromptByMultipleKeysAsksAgainForAMissingKeyBelowTheLast(): void + { + $io = new MockInputOutput(); + $io->setInputs(['1', '2']); + CLI::setInputOutput($io); + + $output = CLI::promptByMultipleKeys('Select your hobbies:', [0 => 'Playing game', 2 => 'Badminton']); + + CLI::resetInputOutput(); + + $this->assertSame([2 => 'Badminton'], $output); + $this->assertSame(1, substr_count($io->getOutput(), 'Please select correctly.')); + } + + public function testPromptByMultipleKeysAcceptsAKeyAboveTheLast(): void + { + $io = new MockInputOutput(); + $io->setInputs(['5']); + CLI::setInputOutput($io); + + $output = CLI::promptByMultipleKeys('Select your hobbies:', [0 => 'Playing game', 5 => 'Sleep', 2 => 'Badminton']); + + CLI::resetInputOutput(); + + $this->assertSame([5 => 'Sleep'], $output); + $this->assertStringNotContainsString('Please select correctly.', $io->getOutput()); + } + + public function testPromptByMultipleKeysDefaultsToTheFirstKey(): void + { + $io = new MockInputOutput(); + $io->setInputs(['']); + CLI::setInputOutput($io); + + $output = CLI::promptByMultipleKeys('Select your hobbies:', [2 => 'Badminton', 5 => 'Sleep']); + + CLI::resetInputOutput(); + + $this->assertSame([2 => 'Badminton'], $output); + $this->assertStringContainsString('[2, 5] : ', preg_replace('/\e\[[^m]+m/', '', $io->getOutput()) ?? ''); + $this->assertStringNotContainsString('Please select correctly.', $io->getOutput()); + } + public function testNewLine(): void { $this->expectOutputString(''); diff --git a/user_guide_src/source/changelogs/v4.7.6.rst b/user_guide_src/source/changelogs/v4.7.6.rst index 843eff4722bd..75baa337d7b0 100644 --- a/user_guide_src/source/changelogs/v4.7.6.rst +++ b/user_guide_src/source/changelogs/v4.7.6.rst @@ -34,6 +34,7 @@ Deprecations Bugs Fixed ********** +- **CLI:** Fixed a bug where ``CLI::promptByMultipleKeys()`` accepted a key missing from the options when it was below the last key, returning an empty selection, rejected a key above the last one when the options were not in key order, and offered ``0`` as the default when the first key was not ``0``. - **Commands:** Fixed a bug where generator commands did not sort the imports of a template with CRLF line endings. - **Format:** Fixed ``XMLFormatter`` so that an array key that becomes empty after sanitization no longer triggers a ``ValueError``; the fallback tag name ``item`` is used instead. Values that cannot be converted to a string, such as objects not implementing ``Stringable``, now throw a ``FormatException`` instead of a fatal ``Error``. - **Helpers:** Fixed a regression in v4.7.5 where ``set_value()`` threw a ``TypeError`` when the default value was not a string or an array, such as an integer, a float, ``null``, or a ``Stringable`` object. Such values are now converted to a string. From 7e9295eedf574c5843043a7b4ab21d0bba4f67aa Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Mon, 26 Oct 2026 06:46:04 +0100 Subject: [PATCH 24/52] fix: normalize string values and keys in `ContentSecurityPolicy::addOption()` (#10636) - Normalize space-separated and empty strings into array options via stringToOptions() - Cast numeric string array keys back to string to prevent TypeError under strict types - Preserve empty source lists as deny-all directives across all directives - Retain empty markers when other options move between reporting modes - Add comprehensive regression tests for config and runtime directives --- system/HTTP/ContentSecurityPolicy.php | 44 +++- .../system/HTTP/ContentSecurityPolicyTest.php | 231 ++++++++++++++++++ user_guide_src/source/changelogs/v4.7.6.rst | 1 + 3 files changed, 273 insertions(+), 3 deletions(-) diff --git a/system/HTTP/ContentSecurityPolicy.php b/system/HTTP/ContentSecurityPolicy.php index faaa526acc94..b668a606e19a 100644 --- a/system/HTTP/ContentSecurityPolicy.php +++ b/system/HTTP/ContentSecurityPolicy.php @@ -896,16 +896,40 @@ protected function addOption($options, string $target, ?bool $explicitReporting { // Ensure we have an array to work with... if (is_string($this->{$target})) { - $this->{$target} = [$this->{$target} => $this->reportOnly]; + $this->{$target} = $this->stringToOptions($this->{$target}); + } elseif (! is_array($this->{$target})) { + $this->{$target} = []; } $options = is_array($options) ? $options : [$options]; foreach ($options as $option) { - $this->{$target}[$option] = $explicitReporting ?? $this->reportOnly; + $this->{$target}[trim((string) $option)] = $explicitReporting ?? $this->reportOnly; } } + /** + * Converts a string directive value into an array of options. + * + * @return array + */ + private function stringToOptions(string $values): array + { + $valueStr = trim($values); + + if ($valueStr === '') { + return ['' => $this->reportOnly]; + } + + $options = []; + + foreach (preg_split('/\s+/', $valueStr) as $val) { + $options[$val] = $this->reportOnly; + } + + return $options; + } + /** * Scans the body of the request message and replaces any nonce * placeholders with actual nonces, that we'll then add to our @@ -1030,14 +1054,20 @@ protected function buildHeaders(ResponseInterface $response) */ protected function addToHeader(string $name, $values = null) { + if ($values === null) { + return; + } + if (is_string($values)) { - $values = [$values => $this->reportOnly]; + $values = $this->stringToOptions($values); } $sources = []; $reportSources = []; foreach ($values as $value => $reportOnly) { + $value = (string) $value; + if ( in_array($value, $this->validSources, true) || str_starts_with($value, 'nonce-') @@ -1056,10 +1086,18 @@ protected function addToHeader(string $name, $values = null) } if ($sources !== []) { + if (count($sources) > 1) { + $sources = array_values(array_filter($sources, static fn ($s): bool => $s !== '')); + } + $this->tempHeaders[$name] = implode(' ', $sources); } if ($reportSources !== []) { + if (count($reportSources) > 1) { + $reportSources = array_values(array_filter($reportSources, static fn ($s): bool => $s !== '')); + } + $this->reportOnlyHeaders[$name] = implode(' ', $reportSources); } } diff --git a/tests/system/HTTP/ContentSecurityPolicyTest.php b/tests/system/HTTP/ContentSecurityPolicyTest.php index a85cd107e7c3..08ee98d0ffe9 100644 --- a/tests/system/HTTP/ContentSecurityPolicyTest.php +++ b/tests/system/HTTP/ContentSecurityPolicyTest.php @@ -800,6 +800,237 @@ public function testSandboxFlags(): void $this->assertContains('sandbox allow-popups allow-top-navigation', $this->getCspDirectives($header)); } + #[PreserveGlobalState(false)] + #[RunInSeparateProcess] + public function testSandboxEmptyFlagThenFlags(): void + { + $this->csp->reportOnly(false); + $this->csp->addSandbox(''); + $this->csp->addSandbox('allow-scripts'); + $this->assertTrue($this->work()); + + $header = $this->getHeaderEmitted('Content-Security-Policy'); + $this->assertIsString($header); + $this->assertContains('sandbox allow-scripts', $this->getCspDirectives($header)); + } + + #[PreserveGlobalState(false)] + #[RunInSeparateProcess] + public function testAddOptionWithSpaceSeparatedString(): void + { + $config = new CSPConfig(); + $config->baseURI = 'self example.com'; + + $csp = new ContentSecurityPolicy($config); + + $response = new Response(config(App::class)); + $response->pretend(true); + + $csp->addBaseURI('another.com'); + $csp->finalize($response); + + $directives = $this->getCspDirectives($response->getHeaderLine('Content-Security-Policy')); + $this->assertContains("base-uri 'self' example.com another.com", $directives); + } + + #[PreserveGlobalState(false)] + #[RunInSeparateProcess] + public function testAddOptionWithEmptyStringTarget(): void + { + $config = new CSPConfig(); + $config->baseURI = ''; + + $csp = new ContentSecurityPolicy($config); + + $response = new Response(config(App::class)); + $response->pretend(true); + + $csp->addBaseURI('example.com'); + $csp->finalize($response); + + $directives = $this->getCspDirectives($response->getHeaderLine('Content-Security-Policy')); + $this->assertContains('base-uri example.com', $directives); + } + + #[PreserveGlobalState(false)] + #[RunInSeparateProcess] + public function testAddOptionNumericString(): void + { + $this->csp->addBaseURI('0'); + $this->assertTrue($this->work()); + + $header = $this->getHeaderEmitted('Content-Security-Policy'); + $this->assertIsString($header); + $this->assertContains('base-uri 0', $this->getCspDirectives($header)); + } + + #[PreserveGlobalState(false)] + #[RunInSeparateProcess] + public function testConfiguredBareSandboxWithAddSandboxNoop(): void + { + $config = new CSPConfig(); + $config->sandbox = ''; + + $csp = new ContentSecurityPolicy($config); + + $response = new Response(config(App::class)); + $response->pretend(true); + + $csp->addSandbox([]); + $csp->finalize($response); + + $directives = $this->getCspDirectives($response->getHeaderLine('Content-Security-Policy')); + $this->assertContains('sandbox', $directives); + } + + #[PreserveGlobalState(false)] + #[RunInSeparateProcess] + public function testAddFrameAncestorEmptyString(): void + { + $this->csp->addFrameAncestor(''); + $this->assertTrue($this->work()); + + $header = $this->getHeaderEmitted('Content-Security-Policy'); + $this->assertIsString($header); + $this->assertContains('frame-ancestors', $this->getCspDirectives($header)); + } + + #[PreserveGlobalState(false)] + #[RunInSeparateProcess] + public function testSandboxMixedModeEnforcedAndReportOnly(): void + { + $this->csp->addSandbox('', false); + $this->csp->addSandbox('allow-scripts', true); + $this->assertTrue($this->work()); + + $enforced = $this->getHeaderEmitted('Content-Security-Policy'); + $this->assertIsString($enforced); + $this->assertContains('sandbox', $this->getCspDirectives($enforced)); + + $reportOnly = $this->getHeaderEmitted('Content-Security-Policy-Report-Only'); + $this->assertIsString($reportOnly); + $this->assertContains('sandbox allow-scripts', $this->getCspDirectives($reportOnly)); + } + + #[PreserveGlobalState(false)] + #[RunInSeparateProcess] + public function testOptionMovedBetweenReportingModesPreservesEmptyMarker(): void + { + $this->csp->addSandbox('', false); + $this->csp->addSandbox('allow-scripts', false); + $this->csp->addSandbox('allow-scripts', true); + + $this->csp->addFrameAncestor('', false); + $this->csp->addFrameAncestor('https://example.com', false); + $this->csp->addFrameAncestor('https://example.com', true); + + $this->assertTrue($this->work()); + + $enforced = $this->getHeaderEmitted('Content-Security-Policy'); + $this->assertIsString($enforced); + $enforcedDirectives = $this->getCspDirectives($enforced); + $this->assertContains('sandbox', $enforcedDirectives); + $this->assertContains('frame-ancestors', $enforcedDirectives); + $this->assertNotContains('sandbox allow-scripts', $enforcedDirectives); + $this->assertNotContains('frame-ancestors https://example.com', $enforcedDirectives); + + $reportOnly = $this->getHeaderEmitted('Content-Security-Policy-Report-Only'); + $this->assertIsString($reportOnly); + $reportOnlyDirectives = $this->getCspDirectives($reportOnly); + $this->assertContains('sandbox allow-scripts', $reportOnlyDirectives); + $this->assertContains('frame-ancestors https://example.com', $reportOnlyDirectives); + } + + #[PreserveGlobalState(false)] + #[RunInSeparateProcess] + public function testConfiguredEmptyDirectivesEmitted(): void + { + $config = new CSPConfig(); + $config->sandbox = ''; + $config->frameAncestors = ''; + $config->formAction = ''; + $config->fontSrc = ''; + $config->connectSrc = ''; + + $csp = new ContentSecurityPolicy($config); + + $response = new Response(config(App::class)); + $response->pretend(true); + + $csp->finalize($response); + + $directives = $this->getCspDirectives($response->getHeaderLine('Content-Security-Policy')); + $this->assertContains('sandbox', $directives); + $this->assertContains('frame-ancestors', $directives); + $this->assertContains('form-action', $directives); + $this->assertContains('font-src', $directives); + $this->assertContains('connect-src', $directives); + } + + #[PreserveGlobalState(false)] + #[RunInSeparateProcess] + public function testConfiguredEmptyFormActionWithAddFormActionNoop(): void + { + $config = new CSPConfig(); + $config->formAction = ''; + + $csp = new ContentSecurityPolicy($config); + + $response = new Response(config(App::class)); + $response->pretend(true); + + $csp->addFormAction([]); + $csp->finalize($response); + + $directives = $this->getCspDirectives($response->getHeaderLine('Content-Security-Policy')); + $this->assertContains('form-action', $directives); + } + + #[PreserveGlobalState(false)] + #[RunInSeparateProcess] + public function testClearDirectiveThenAddEmptySource(): void + { + $this->csp->clearDirective('form-action'); + $this->csp->addFormAction(''); + $this->csp->clearDirective('connect-src'); + $this->csp->addConnectSrc(''); + $this->assertTrue($this->work()); + + $header = $this->getHeaderEmitted('Content-Security-Policy'); + $this->assertIsString($header); + $directives = $this->getCspDirectives($header); + $this->assertContains('form-action', $directives); + $this->assertContains('connect-src', $directives); + } + + #[PreserveGlobalState(false)] + #[RunInSeparateProcess] + public function testAddEmptySourceToFontSrc(): void + { + $this->csp->addFontSrc(''); + $this->assertTrue($this->work()); + + $header = $this->getHeaderEmitted('Content-Security-Policy'); + $this->assertIsString($header); + $directives = $this->getCspDirectives($header); + $this->assertContains('font-src', $directives); + } + + #[PreserveGlobalState(false)] + #[RunInSeparateProcess] + public function testRedundantEmptySourceFilteredWhenOtherSourcesPresent(): void + { + // connectSrc defaults to 'self' + $this->csp->addConnectSrc(''); + $this->assertTrue($this->work()); + + $header = $this->getHeaderEmitted('Content-Security-Policy'); + $this->assertIsString($header); + $directives = $this->getCspDirectives($header); + $this->assertContains("connect-src 'self'", $directives); + $this->assertNotContains('connect-src', $directives); + } + #[PreserveGlobalState(false)] #[RunInSeparateProcess] public function testUpgradeInsecureRequests(): void diff --git a/user_guide_src/source/changelogs/v4.7.6.rst b/user_guide_src/source/changelogs/v4.7.6.rst index 75baa337d7b0..16793a3d9aa9 100644 --- a/user_guide_src/source/changelogs/v4.7.6.rst +++ b/user_guide_src/source/changelogs/v4.7.6.rst @@ -38,6 +38,7 @@ Bugs Fixed - **Commands:** Fixed a bug where generator commands did not sort the imports of a template with CRLF line endings. - **Format:** Fixed ``XMLFormatter`` so that an array key that becomes empty after sanitization no longer triggers a ``ValueError``; the fallback tag name ``item`` is used instead. Values that cannot be converted to a string, such as objects not implementing ``Stringable``, now throw a ``FormatException`` instead of a fatal ``Error``. - **Helpers:** Fixed a regression in v4.7.5 where ``set_value()`` threw a ``TypeError`` when the default value was not a string or an array, such as an integer, a float, ``null``, or a ``Stringable`` object. Such values are now converted to a string. +- **HTTP:** Fixed a bug in ``ContentSecurityPolicy`` where a numeric source such as ``'0'`` threw a ``TypeError``, and a space-separated directive string in ``Config\\ContentSecurityPolicy`` was emitted as one unquoted source. - **HTTP:** Fixed a bug in ``ContentSecurityPolicy::setDefaultSrc()`` where passing an array of URIs resulted in an invalid ``'Array'`` directive. - **HTTP:** Fixed stale ``$_REQUEST`` values after ``SiteURIFactory`` normalizes GET parameters, so ``Validation::withRequest()`` reads the normalized values while unrelated REQUEST entries are preserved. - **HTTP:** Fixed a bug where ``IncomingRequest::isSecure()`` did not recognize ``https`` in the ``X-Forwarded-Proto`` header from a trusted proxy when the value contained uppercase letters or surrounding whitespace, or was represented as a single-element array. From 345262a51bec6065594426a27379cf2f695aab75 Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Mon, 26 Oct 2026 06:47:04 +0100 Subject: [PATCH 25/52] fix(HTTP): read request body when post_max_size is unlimited (#10646) PHP treats post_max_size values of 0 or -1 as unlimited, but IncomingRequest compared Content-Length against the raw value, so the request body was never read. Use ini_parse_quantity() and treat non-positive values as PHP_INT_MAX. --- system/HTTP/IncomingRequest.php | 18 +++++----- tests/system/HTTP/IncomingRequestTest.php | 40 +++++++++++++++++++++ user_guide_src/source/changelogs/v4.7.6.rst | 3 +- 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index f3063bcb6093..eb77b9277875 100644 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -140,7 +140,7 @@ public function __construct($config, ?URI $uri = null, $body = 'php://input', ?U // php://input is not available with enctype="multipart/form-data". // See https://www.php.net/manual/en/wrappers.php.php#wrappers.php.input && ! str_contains($this->getHeaderLine('Content-Type'), 'multipart/form-data') - && (int) $this->getHeaderLine('Content-Length') <= $this->getPostMaxSize() + && (int) $this->getHeaderLine('Content-Length') <= $this->getPostMaxSize((string) ini_get('post_max_size')) ) { // Get our body from php://input $body = file_get_contents('php://input'); @@ -167,16 +167,16 @@ public function __construct($config, ?URI $uri = null, $body = 'php://input', ?U $this->detectLocale($config); } - private function getPostMaxSize(): int + /** + * Converts the `post_max_size` ini value to bytes. + * + * A value of 0 or less means unlimited. + */ + private function getPostMaxSize(string $postMaxSize): int { - $postMaxSize = ini_get('post_max_size'); + $bytes = ini_parse_quantity($postMaxSize); - return match (strtoupper(substr($postMaxSize, -1))) { - 'G' => (int) str_replace('G', '', $postMaxSize) * 1024 ** 3, - 'M' => (int) str_replace('M', '', $postMaxSize) * 1024 ** 2, - 'K' => (int) str_replace('K', '', $postMaxSize) * 1024, - default => (int) $postMaxSize, - }; + return $bytes <= 0 ? PHP_INT_MAX : $bytes; } /** diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index 89646081e53c..cc0aee29ab2b 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -1081,6 +1081,46 @@ public function testSetPath(): void $this->assertSame('foobar', $request->getPath()); } + #[DataProvider('provideGetPostMaxSize')] + public function testGetPostMaxSize(string $value, int $expected): void + { + $getPostMaxSize = $this->getPrivateMethodInvoker($this->request, 'getPostMaxSize'); + + $this->assertSame($expected, $getPostMaxSize($value)); + } + + /** + * @return iterable + */ + public static function provideGetPostMaxSize(): iterable + { + yield 'zero means unlimited' => ['0', PHP_INT_MAX]; + + yield 'zero with suffix means unlimited' => ['0K', PHP_INT_MAX]; + + yield 'negative means unlimited' => ['-1', PHP_INT_MAX]; + + yield 'gigabytes' => ['1G', 1024 ** 3]; + + yield 'lowercased gigabytes' => ['1g', 1024 ** 3]; + + yield 'megabytes' => ['8M', 8 * 1024 ** 2]; + + yield 'lowercased megabytes' => ['8m', 8 * 1024 ** 2]; + + yield 'kilobytes' => ['512K', 512 * 1024]; + + yield 'lowercased kilobytes' => ['512k', 512 * 1024]; + + yield 'hexadecimal' => ['0x10M', 16 * 1024 ** 2]; + + yield 'octal' => ['010M', 8 * 1024 ** 2]; + + yield 'binary' => ['0b10M', 2 * 1024 ** 2]; + + yield 'bytes' => ['12345', 12345]; + } + public function testGetIPAddressNormal(): void { $expected = '123.123.123.123'; diff --git a/user_guide_src/source/changelogs/v4.7.6.rst b/user_guide_src/source/changelogs/v4.7.6.rst index 16793a3d9aa9..4c36d122395f 100644 --- a/user_guide_src/source/changelogs/v4.7.6.rst +++ b/user_guide_src/source/changelogs/v4.7.6.rst @@ -40,8 +40,9 @@ Bugs Fixed - **Helpers:** Fixed a regression in v4.7.5 where ``set_value()`` threw a ``TypeError`` when the default value was not a string or an array, such as an integer, a float, ``null``, or a ``Stringable`` object. Such values are now converted to a string. - **HTTP:** Fixed a bug in ``ContentSecurityPolicy`` where a numeric source such as ``'0'`` threw a ``TypeError``, and a space-separated directive string in ``Config\\ContentSecurityPolicy`` was emitted as one unquoted source. - **HTTP:** Fixed a bug in ``ContentSecurityPolicy::setDefaultSrc()`` where passing an array of URIs resulted in an invalid ``'Array'`` directive. -- **HTTP:** Fixed stale ``$_REQUEST`` values after ``SiteURIFactory`` normalizes GET parameters, so ``Validation::withRequest()`` reads the normalized values while unrelated REQUEST entries are preserved. +- **HTTP:** Fixed a bug where ``IncomingRequest`` did not read the request body when PHP's ``post_max_size`` was set to ``0`` or ``-1`` (unlimited). - **HTTP:** Fixed a bug where ``IncomingRequest::isSecure()`` did not recognize ``https`` in the ``X-Forwarded-Proto`` header from a trusted proxy when the value contained uppercase letters or surrounding whitespace, or was represented as a single-element array. +- **HTTP:** Fixed stale ``$_REQUEST`` values after ``SiteURIFactory`` normalizes GET parameters, so ``Validation::withRequest()`` reads the normalized values while unrelated REQUEST entries are preserved. - **Security:** Fixed a bug where a valid JSON request body that is not an object (for example a JSON array) was corrupted after CSRF verification when the token was sent via the ``X-CSRF-TOKEN`` header. See the repo's From 699fc2f86a92b1c9b29c18fa115e53c200fede54 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Mon, 26 Oct 2026 20:20:08 +0800 Subject: [PATCH 26/52] docs: convert the remaining user guide tables to pipe-table-friendly RST (#10664) * docs: convert the remaining user guide tables to pipe-table-friendly RST * docs: fix rendering for `mb_*` --- .../source/changelogs/v4.0.0-alpha.3.rst | 2 +- .../source/database/configuration.rst | 36 +++++----- .../installation/upgrade_migrations.rst | 2 +- .../source/libraries/validation.rst | 70 +++++++++---------- user_guide_src/source/models/model.rst | 7 +- .../source/outgoing/view_parser.rst | 57 +++++++-------- 6 files changed, 89 insertions(+), 85 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.0.0-alpha.3.rst b/user_guide_src/source/changelogs/v4.0.0-alpha.3.rst index fea90ed55aed..83213e5e3883 100644 --- a/user_guide_src/source/changelogs/v4.0.0-alpha.3.rst +++ b/user_guide_src/source/changelogs/v4.0.0-alpha.3.rst @@ -261,7 +261,7 @@ PRs merged - #1540 Update validation class User Guide - #1530 database performance improvement : use foreach() when possible - 2047b5a Don't run filters when using spark. -- #1539 remove mb_* (mb string usage) in CreditCardRules +- #1539 remove mb\_* (mb string usage) in CreditCardRules - #1536 ext-json in composer.json - #1525 remove unneeded try {} catch {} - #1524 Test routes resource with 'websafe' option diff --git a/user_guide_src/source/database/configuration.rst b/user_guide_src/source/database/configuration.rst index 3d523bcbf3d9..1a8c06cc59d5 100644 --- a/user_guide_src/source/database/configuration.rst +++ b/user_guide_src/source/database/configuration.rst @@ -148,9 +148,6 @@ Description of Values **username** The username used to connect to the database. (``SQLite3`` does not use this.) **password** The password used to connect to the database. (``SQLite3`` does not use this.) **database** The name of the database you want to connect to. - - .. note:: CodeIgniter doesn't support dots (``.``) in the table and column names. - Since v4.5.0, database names with dots are supported. **DBDriver** The database driver name. The case must match the driver name. You can set a fully qualified classname to use your custom driver. Supported drivers: ``MySQLi``, ``Postgre``, ``SQLite3``, ``SQLSRV``, and ``OCI8``. @@ -173,28 +170,35 @@ Description of Values strict SQL while developing an application. **port** The database port number - Empty string ``''`` for default port (or dynamic port with ``SQLSRV``). **foreignKeys** (``SQLite3`` only) true/false (boolean) - Whether to enable Foreign Key constraint. - - .. important:: SQLite3 Foreign Key constraint is disabled by default. - See `SQLite documentation `_. - To enforce Foreign Key constraint, set this config item to true. **busyTimeout** (``SQLite3`` only) milliseconds (int) - Sleeps for a specified amount of time when a table is locked. **synchronous** (``SQLite3`` only) flag (int) - How strict SQLite will be at flushing to disk during transactions. Use `null` to stay with the default setting. This can be used since v4.6.0. **numberNative** (``MySQLi`` only) true/false (boolean) - Whether to enable MYSQLI_OPT_INT_AND_FLOAT_NATIVE. **foundRows** (``MySQLi`` only) true/false (boolean) - Whether to enable MYSQLI_CLIENT_FOUND_ROWS. -**dateFormat** The default date/time formats as PHP's `DateTime format`_. - * ``date`` - date format - * ``datetime`` - date and time format - * ``datetime-ms`` - date and time with millisecond format - * ``datetime-us`` - date and time with microsecond format - * ``time`` - time format - This can be used since v4.5.0, and you can get the value, e.g., ``$db->dateFormat['datetime']``. - Currently, the database drivers do not use these values directly, - but :ref:`Model ` uses them. +**dateFormat** The default date/time formats as PHP's `DateTime format`_, with the keys listed below the table. ================ =========================================================================================================== .. _DateTime format: https://www.php.net/manual/en/datetime.format.php +.. note:: CodeIgniter doesn't support dots (``.``) in the table and column names. + Since v4.5.0, database names with dots are supported. + +.. important:: SQLite3 Foreign Key constraint is disabled by default. + See `SQLite documentation `_. + To enforce Foreign Key constraint, set **foreignKeys** to true. + +The **dateFormat** array has the following keys: + +* ``date`` - date format +* ``datetime`` - date and time format +* ``datetime-ms`` - date and time with millisecond format +* ``datetime-us`` - date and time with microsecond format +* ``time`` - time format + +This can be used since v4.5.0, and you can get the value, e.g., ``$db->dateFormat['datetime']``. +Currently, the database drivers do not use these values directly, +but :ref:`Model ` uses them. + .. note:: Depending on what database driver you are using (``MySQLi``, ``Postgre``, etc.) not all values will be needed. For example, when using ``SQLite3`` you will not need to supply a username or password, and the database name diff --git a/user_guide_src/source/installation/upgrade_migrations.rst b/user_guide_src/source/installation/upgrade_migrations.rst index 0936c48ad400..a506e58db647 100644 --- a/user_guide_src/source/installation/upgrade_migrations.rst +++ b/user_guide_src/source/installation/upgrade_migrations.rst @@ -70,7 +70,7 @@ Search & Replace You can use to following table to search & replace your old CI3 files. +------------------------------+----------------------------+ -| Search | Replace | +| Search | Replace | +==============================+============================+ | extends CI_Migration | extends Migration | +------------------------------+----------------------------+ diff --git a/user_guide_src/source/libraries/validation.rst b/user_guide_src/source/libraries/validation.rst index 399c84fa9c5b..d92d8048f533 100644 --- a/user_guide_src/source/libraries/validation.rst +++ b/user_guide_src/source/libraries/validation.rst @@ -1082,41 +1082,41 @@ See also :ref:`file-upload-form-tutorial`. ======================= ========== ============================================= =================================================== Rule Parameter Description Example ======================= ========== ============================================= =================================================== -uploaded Yes Fails if the name of the parameter does not ``uploaded[field_name]`` - match the name of any uploaded files. - If you want the file upload to be optional - (not required), do not define this rule. - -max_size Yes Fails if the uploaded file is larger ``max_size[field_name,2048]`` - than the second parameter - in kilobytes (kb). Or if the file - is larger than allowed maximum size declared - in php.ini config file - - ``upload_max_filesize`` directive. -max_dims Yes Fails if the maximum width and height of an ``max_dims[field_name,300,150]`` - uploaded image exceed values. The first - parameter is the field name. The second is - the width, and the third is the height. Will - also fail if the file cannot be determined - to be an image or the upload failed with an - error other than ``UPLOAD_ERR_NO_FILE``. -min_dims Yes Fails if the minimum width and height of an ``min_dims[field_name,300,150]`` - uploaded image not meet values. The first - parameter is the field name. The second is - the width, and the third is the height. Will - also fail if the file cannot be determined - to be an image or the upload failed with an - error other than ``UPLOAD_ERR_NO_FILE``. - (This rule was added in - v4.6.0.) -mime_in Yes Fails if the file's mime type is not one ``mime_in[field_name,image/png,image/jpeg]`` - listed in the parameters. -ext_in Yes Fails if the client filename's extension is ``ext_in[field_name,png,jpg,gif]`` - not one listed in the parameters, or if the - detected MIME type does not match the MIME - types associated with the extension. -is_image Yes Fails if the file cannot be determined to be ``is_image[field_name]`` - an image based on the mime type. +uploaded Yes Fails if the name of the parameter does not ``uploaded[field_name]`` + match the name of any uploaded files. + If you want the file upload to be optional + (not required), do not define this rule. + +max_size Yes Fails if the uploaded file is larger ``max_size[field_name,2048]`` + than the second parameter + in kilobytes (kb). Or if the file + is larger than allowed maximum size declared + in php.ini config file - + ``upload_max_filesize`` directive. +max_dims Yes Fails if the maximum width and height of an ``max_dims[field_name,300,150]`` + uploaded image exceed values. The first + parameter is the field name. The second is + the width, and the third is the height. Will + also fail if the file cannot be determined + to be an image or the upload failed with an + error other than ``UPLOAD_ERR_NO_FILE``. +min_dims Yes Fails if the minimum width and height of an ``min_dims[field_name,300,150]`` + uploaded image not meet values. The first + parameter is the field name. The second is + the width, and the third is the height. Will + also fail if the file cannot be determined + to be an image or the upload failed with an + error other than ``UPLOAD_ERR_NO_FILE``. + (This rule was added in + v4.6.0.) +mime_in Yes Fails if the file's mime type is not one ``mime_in[field_name,image/png,image/jpeg]`` + listed in the parameters. +ext_in Yes Fails if the client filename's extension is ``ext_in[field_name,png,jpg,gif]`` + not one listed in the parameters, or if the + detected MIME type does not match the MIME + types associated with the extension. +is_image Yes Fails if the file cannot be determined to be ``is_image[field_name]`` + an image based on the mime type. ======================= ========== ============================================= =================================================== The file validation rules apply for both single and multiple file uploads. diff --git a/user_guide_src/source/models/model.rst b/user_guide_src/source/models/model.rst index 76feb2b7c7c1..c37d72c9c2c7 100644 --- a/user_guide_src/source/models/model.rst +++ b/user_guide_src/source/models/model.rst @@ -1039,10 +1039,9 @@ afterUpdate **id** = the array of primary keys of the rows being passed to **data** = the key/value pairs being updated. **result** = the results of the ``update()`` method used through the Query Builder. beforeFind The name of the calling **method**, whether a **singleton** was requested, and these additional fields: -- ``first()`` No additional fields -- ``find()`` **id** = the primary key of the row being searched for. -- ``findAll()`` **limit** = the number of rows to find. - **offset** = the number of rows to skip during the search. + none for ``first()``, **id** = the primary key of the row being searched for, for ``find()``, + and **limit** = the number of rows to find, and **offset** = the number of rows to skip during + the search, for ``findAll()``. afterFind Same as **beforeFind** but including the resulting row(s) of data, or null if no result found. beforeDelete **id** = an array of primary key rows being passed to the ``delete()`` method. **purge** = boolean whether soft-delete rows should be hard deleted. diff --git a/user_guide_src/source/outgoing/view_parser.rst b/user_guide_src/source/outgoing/view_parser.rst index 33f0ef9bc31e..9effccf9ae74 100644 --- a/user_guide_src/source/outgoing/view_parser.rst +++ b/user_guide_src/source/outgoing/view_parser.rst @@ -438,63 +438,64 @@ Provided Filters The following filters are available when using the parser: ================ ================= =========================================================== ====================================== -Filter Arguments Description Example +Filter Arguments Description Example ================ ================= =========================================================== ====================================== -abs Displays the absolute value of a number. { v|abs } +abs Displays the absolute value of a number. ``{ v|abs }`` -capitalize Displays the string in sentence case: all lowercase { v|capitalize} +capitalize Displays the string in sentence case: all lowercase ``{ v|capitalize}`` with firstletter capitalized. -date format (Y-m-d) A PHP **date**-compatible formatting string. { v|date(Y-m-d) } +date format (Y-m-d) A PHP **date**-compatible formatting string. ``{ v|date(Y-m-d) }`` -date_modify value to add A **strtotime** compatible string to modify the date, { v|date_modify(+1 day) } - / subtract like ``+5 day`` or ``-1 week``. +date_modify value to add A **strtotime** compatible string to modify the date, ``{ v|date_modify(+1 day) }`` + / subtract like ``+5 day`` or ``-1 week``. -default default value Displays the default value if the variable is `empty()`_. { v|default(just in case) } +default default value Displays the default value if the variable is `empty()`_. ``{ v|default(just in case) }`` -esc html, attr, Specifies the context to escape the data. { v|esc(attr) } - css, js +esc html, attr, Specifies the context to escape the data. ``{ v|esc(attr) }`` + css, js -excerpt phrase, radius Returns the text within a radius of words from a given { v|excerpt(green giant, 20) } +excerpt phrase, radius Returns the text within a radius of words from a given ``{ v|excerpt(green giant, 20) }`` phrase. Same as **excerpt** helper function. -highlight phrase Highlights a given phrase within the text using { v|highlight(view parser) } - '' tags. +highlight phrase Highlights a given phrase within the text using ``{ v|highlight(view parser) }`` + ```` tags. -highlight_code Highlights code samples with HTML/CSS. { v|highlight_code } +highlight_code Highlights code samples with HTML/CSS. ``{ v|highlight_code }`` -limit_chars limit Limits the number of characters to $limit. { v|limit_chars(100) } +limit_chars limit Limits the number of characters to $limit. ``{ v|limit_chars(100) }`` -limit_words limit Limits the number of words to $limit. { v|limit_words(20) } +limit_words limit Limits the number of words to $limit. ``{ v|limit_words(20) }`` -local_currency currency, Displays a localized version of a currency. "currency" { v|local_currency(EUR,en_US) } - locale, valueis any 3-letter ISO 4217 currency code. - fraction +local_currency currency, Displays a localized version of a currency. "currency" ``{ v|local_currency(EUR,en_US) }`` + locale, valueis any 3-letter ISO 4217 currency code. + fraction -local_number type, precision, Displays a localized version of a number. "type" can be { v|local_number(decimal,2,en_US) } - locale one of: decimal, currency, percent, scientific, spellout, +local_number type, precision, Displays a localized version of a number. "type" can be ``{ v|local_number(decimal,2,en_US) }`` + locale one of: decimal, currency, percent, scientific, spellout, ordinal, duration. -lower Converts a string to lowercase. { v|lower } +lower Converts a string to lowercase. ``{ v|lower }`` -nl2br Replaces all newline characters (\n) to an HTML
tag. { v|nl2br } +nl2br Replaces all newline characters (``\n``) to an HTML ``{ v|nl2br }`` + ``
`` tag. -number_format places Wraps PHP **number_format** function for use within the { v|number_format(3) } +number_format places Wraps PHP **number_format** function for use within the ``{ v|number_format(3) }`` parser. -prose Takes a body of text and uses the **auto_typography()** { v|prose } +prose Takes a body of text and uses the **auto_typography()** ``{ v|prose }`` method to turn it into prettier, easier-to-read, prose. -round places, type Rounds a number to the specified places. Types of **ceil** { v|round(3) } { v|round(ceil) } +round places, type Rounds a number to the specified places. Types of **ceil** ``{ v|round(3) }`` ``{ v|round(ceil) }`` and **floor** can be passed to use those functions instead. -strip_tags allowed chars Wraps PHP **strip_tags**. Can accept a string of allowed { v|strip_tags(
) } +strip_tags allowed chars Wraps PHP **strip_tags**. Can accept a string of allowed ``{ v|strip_tags(
) }`` tags. -title Displays a "title case" version of the string, with all { v|title } +title Displays a "title case" version of the string, with all ``{ v|title }`` lowercase, and each word capitalized. -upper Displays the string in all uppercase. { v|upper } +upper Displays the string in all uppercase. ``{ v|upper }`` ================ ================= =========================================================== ====================================== .. _empty(): https://www.php.net/manual/en/function.empty.php From 45454ffe4539e0a0420ab3bda041e8e6efc648da Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Mon, 26 Oct 2026 22:52:52 +0700 Subject: [PATCH 27/52] chore: Bump Rector to 2.8.1 and clean up deprecated rules registration (#10666) --- composer.json | 2 +- rector.php | 11 ----------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/composer.json b/composer.json index 1ab6a1686f01..78b9895b5723 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "phpunit/phpcov": "^9.0.2 || ^10.0", "phpunit/phpunit": "^10.5.16 || ^11.2", "predis/predis": "^3.0", - "rector/rector": "2.7.0", + "rector/rector": "2.8.1", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { diff --git a/rector.php b/rector.php index 2698aec9f098..5e8f63feab59 100644 --- a/rector.php +++ b/rector.php @@ -39,7 +39,6 @@ use Rector\TypeDeclaration\Rector\Closure\ClosureReturnTypeRector; use Rector\TypeDeclaration\Rector\Function_\AddFunctionVoidReturnTypeWhereNoReturnRector; use Rector\TypeDeclaration\Rector\Property\TypedPropertyFromAssignsRector; -use Rector\TypeDeclaration\Rector\StmtsAwareInterface\DeclareStrictTypesRector; use Rector\TypeDeclaration\Rector\StmtsAwareInterface\SafeDeclareStrictTypesRector; use Utils\Rector\PassStrictParameterToFunctionParameterRector; use Utils\Rector\RemoveErrorSuppressInTryCatchStmtsRector; @@ -110,15 +109,6 @@ __DIR__ . '/tests/_support/Entity/CustomUser.php', ], - DeclareStrictTypesRector::class => [ - __DIR__ . '/app', - __DIR__ . '/system/CodeIgniter.php', - __DIR__ . '/system/Config/BaseConfig.php', - __DIR__ . '/system/Test/ControllerTestTrait.php', - __DIR__ . '/system/View/Parser.php', - __DIR__ . '/tests/system/Debug/ExceptionsTest.php', - ], - // use mt_rand instead of random_int on purpose on non-cryptographically random RandomFunctionRector::class, @@ -172,7 +162,6 @@ // auto import fully qualified class names ->withImportNames() ->withRules([ - DeclareStrictTypesRector::class, UnderscoreToCamelCaseVariableNameRector::class, PassStrictParameterToFunctionParameterRector::class, RemoveErrorSuppressInTryCatchStmtsRector::class, From 823b1c08c489db93fc2263de5a2e6277db81b83a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2026 18:15:39 +0100 Subject: [PATCH 28/52] chore(deps): bump actions/setup-python in / (#10667) Bumps [actions/setup-python](https://github.com/actions/setup-python) in `/` from 7.0.0 to 7.1.0. Updates `actions/setup-python` from 7.0.0 to 7.1.0 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/5fda3b95a4ea91299a34e894583c3862153e4b97...6d4fd22b20465ccc0f7354476588e8df7c9ca0e0) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: 7.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github_actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/deploy-distributables.yml | 2 +- .github/workflows/deploy-userguide-latest.yml | 2 +- .github/workflows/test-userguide.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-distributables.yml b/.github/workflows/deploy-distributables.yml index 1eeb86f772cb..38423b4e5229 100644 --- a/.github/workflows/deploy-distributables.yml +++ b/.github/workflows/deploy-distributables.yml @@ -172,7 +172,7 @@ jobs: persist-credentials: false - name: Setup Python - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: python-version: '3.12' diff --git a/.github/workflows/deploy-userguide-latest.yml b/.github/workflows/deploy-userguide-latest.yml index 6151ad411a14..65b1fe77b0be 100644 --- a/.github/workflows/deploy-userguide-latest.yml +++ b/.github/workflows/deploy-userguide-latest.yml @@ -36,7 +36,7 @@ jobs: coverage: none - name: Setup Python - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: python-version: '3.12' diff --git a/.github/workflows/test-userguide.yml b/.github/workflows/test-userguide.yml index 88f316888f02..b7ef79e0170d 100644 --- a/.github/workflows/test-userguide.yml +++ b/.github/workflows/test-userguide.yml @@ -27,7 +27,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Python - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: python-version: '3.12' From 7913c65005e7f7c8fc9a28641c1015d1254d0e9d Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Tue, 27 Oct 2026 13:22:56 +0800 Subject: [PATCH 29/52] docs: point the version switcher's in-progress option at the develop build (#10665) --- user_guide_src/source/_static/js/version_switcher.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/_static/js/version_switcher.js b/user_guide_src/source/_static/js/version_switcher.js index a201bb4ddc7a..d33c33ea1509 100644 --- a/user_guide_src/source/_static/js/version_switcher.js +++ b/user_guide_src/source/_static/js/version_switcher.js @@ -5,7 +5,7 @@ * scaffolding above the search box in the left sidebar, containing a