From e61e52f3a58144948e09ac69b23566317f81a334 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Fri, 22 May 2026 19:57:47 +0800 Subject: [PATCH 01/65] docs: add changelog and upgrade for v4.7.4 (#10230) --- system/CodeIgniter.php | 2 +- user_guide_src/source/changelogs/index.rst | 1 + user_guide_src/source/changelogs/v4.7.4.rst | 35 ++++++++++++ .../source/installation/upgrade_474.rst | 55 +++++++++++++++++++ .../source/installation/upgrading.rst | 1 + 5 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 user_guide_src/source/changelogs/v4.7.4.rst create mode 100644 user_guide_src/source/installation/upgrade_474.rst diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 66fe9d0b4983..01d585eb246e 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.3'; + public const CI_VERSION = '4.7.4-dev'; /** * App startup time. diff --git a/user_guide_src/source/changelogs/index.rst b/user_guide_src/source/changelogs/index.rst index b47fe1a7ffff..ca8f8c2cb715 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.4 v4.7.3 v4.7.2 v4.7.1 diff --git a/user_guide_src/source/changelogs/v4.7.4.rst b/user_guide_src/source/changelogs/v4.7.4.rst new file mode 100644 index 000000000000..c10b04436f8a --- /dev/null +++ b/user_guide_src/source/changelogs/v4.7.4.rst @@ -0,0 +1,35 @@ +############# +Version 4.7.4 +############# + +Release Date: Unreleased + +**4.7.4 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_474.rst b/user_guide_src/source/installation/upgrade_474.rst new file mode 100644 index 000000000000..a02083758727 --- /dev/null +++ b/user_guide_src/source/installation/upgrade_474.rst @@ -0,0 +1,55 @@ +############################# +Upgrading from 4.7.3 to 4.7.4 +############################# + +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 64dd632b1d1d..dc9199da84ca 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_474 upgrade_473 upgrade_472 upgrade_471 From f6b766c33ff4392c342a70e294118efeb4766475 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 23 May 2026 13:11:28 +0700 Subject: [PATCH 02/65] chore(deps-dev): update boundwize/structarmed requirement (#10231) Updates the requirements on [boundwize/structarmed](https://github.com/boundwize/structarmed) to permit the latest version. Updates `boundwize/structarmed` to 0.7.6 - [Release notes](https://github.com/boundwize/structarmed/releases) - [Commits](https://github.com/boundwize/structarmed/compare/0.6.15...0.7.6) --- updated-dependencies: - dependency-name: boundwize/structarmed dependency-version: 0.7.6 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 64183685f899..e42e8ab64f8d 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "psr/log": "^3.0" }, "require-dev": { - "boundwize/structarmed": "0.6.15", + "boundwize/structarmed": "0.7.6", "codeigniter/phpstan-codeigniter": "^1.5", "fakerphp/faker": "^1.24", "kint-php/kint": "^6.1", From 34a826254dd6bfb345bb95e9e92bdcca0c9fb823 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 May 2026 05:09:19 +0700 Subject: [PATCH 03/65] chore(deps-dev): update boundwize/structarmed requirement (#10239) Updates the requirements on [boundwize/structarmed](https://github.com/boundwize/structarmed) to permit the latest version. Updates `boundwize/structarmed` to 0.7.10 - [Release notes](https://github.com/boundwize/structarmed/releases) - [Commits](https://github.com/boundwize/structarmed/compare/0.7.6...0.7.10) --- updated-dependencies: - dependency-name: boundwize/structarmed dependency-version: 0.7.10 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e42e8ab64f8d..552c1590fdaf 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "psr/log": "^3.0" }, "require-dev": { - "boundwize/structarmed": "0.7.6", + "boundwize/structarmed": "0.7.10", "codeigniter/phpstan-codeigniter": "^1.5", "fakerphp/faker": "^1.24", "kint-php/kint": "^6.1", From 5c0cbfe92cc93b6f12e2a6528d5fdc20e13b919d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 May 2026 05:37:33 +0700 Subject: [PATCH 04/65] chore(deps-dev): bump the composer-dependencies group with 2 updates (#10241) Updates the requirements on [boundwize/structarmed](https://github.com/boundwize/structarmed) and [rector/rector](https://github.com/rectorphp/rector) to permit the latest version. Updates `boundwize/structarmed` to 0.7.12 - [Release notes](https://github.com/boundwize/structarmed/releases) - [Commits](https://github.com/boundwize/structarmed/compare/0.7.10...0.7.12) Updates `rector/rector` to 2.4.5 - [Release notes](https://github.com/rectorphp/rector/releases) - [Commits](https://github.com/rectorphp/rector/compare/2.4.4...2.4.5) --- updated-dependencies: - dependency-name: boundwize/structarmed dependency-version: 0.7.12 dependency-type: direct:development dependency-group: composer-dependencies - dependency-name: rector/rector dependency-version: 2.4.5 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 552c1590fdaf..b78ddbecf9eb 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "psr/log": "^3.0" }, "require-dev": { - "boundwize/structarmed": "0.7.10", + "boundwize/structarmed": "0.7.12", "codeigniter/phpstan-codeigniter": "^1.5", "fakerphp/faker": "^1.24", "kint-php/kint": "^6.1", @@ -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.4.4", + "rector/rector": "2.4.5", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { From 953309e3d1335d449a27c1584805b35f50abea08 Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Wed, 27 May 2026 08:35:07 +0200 Subject: [PATCH 05/65] fix: prevent updateBatch with existing where conditions (#10236) --- system/Database/BaseBuilder.php | 7 +++++++ tests/system/Database/Builder/UpdateTest.php | 18 ++++++++++++++++++ user_guide_src/source/changelogs/v4.7.4.rst | 2 ++ 3 files changed, 27 insertions(+) diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index d891b3d61954..0a27b0b33abc 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -2589,6 +2589,13 @@ protected function validateUpdate(): bool */ public function updateBatch($set = null, $constraints = null, int $batchSize = 100) { + if ($this->QBWhere !== []) { + throw new DatabaseException( + 'updateBatch() cannot be safely combined with existing Query Builder WHERE conditions. ' + . 'Use updateBatch($data, $constraints), onConstraint(), or include all required constraint fields in the batch data.', + ); + } + $this->onConstraint($constraints); if (isset($this->QBOptions['setQueryAsData'])) { diff --git a/tests/system/Database/Builder/UpdateTest.php b/tests/system/Database/Builder/UpdateTest.php index eb1cfc7d9c92..5bb93ad2a598 100644 --- a/tests/system/Database/Builder/UpdateTest.php +++ b/tests/system/Database/Builder/UpdateTest.php @@ -299,6 +299,24 @@ public function testUpdateBatchThrowsExceptionWithNoData(): void $builder->updateBatch(null, 'id'); } + public function testUpdateBatchThrowsExceptionWithWhere(): void + { + $builder = new BaseBuilder('jobs', $this->db); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage( + 'updateBatch() cannot be safely combined with existing Query Builder WHERE conditions. ' + . 'Use updateBatch($data, $constraints), onConstraint(), or include all required constraint fields in the batch data.', + ); + + $builder->where('description', 'old')->updateBatch([ + [ + 'id' => 2, + 'name' => 'Comedian', + ], + ], 'id'); + } + public function testUpdateBatchThrowsExceptionWithNoID(): void { $builder = new BaseBuilder('jobs', $this->db); diff --git a/user_guide_src/source/changelogs/v4.7.4.rst b/user_guide_src/source/changelogs/v4.7.4.rst index c10b04436f8a..244823a000f7 100644 --- a/user_guide_src/source/changelogs/v4.7.4.rst +++ b/user_guide_src/source/changelogs/v4.7.4.rst @@ -30,6 +30,8 @@ Deprecations Bugs Fixed ********** +- **Database:** Fixed a bug where ``updateBatch()`` could be called after Query Builder ``where()`` conditions, even though it's not supported. In this situation, now the ``DatabaseException`` is thrown. + See the repo's `CHANGELOG.md `_ for a complete list of bugs fixed. From 5f93f22bed5dad5d1e6e3c93628cef5c95ae4e8e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 01:15:56 +0700 Subject: [PATCH 06/65] chore(deps-dev): update boundwize/structarmed requirement (#10245) Updates the requirements on [boundwize/structarmed](https://github.com/boundwize/structarmed) to permit the latest version. Updates `boundwize/structarmed` to 0.8.0 - [Release notes](https://github.com/boundwize/structarmed/releases) - [Commits](https://github.com/boundwize/structarmed/compare/0.7.12...0.8.0) --- updated-dependencies: - dependency-name: boundwize/structarmed dependency-version: 0.8.0 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index b78ddbecf9eb..31ade5c8da14 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "psr/log": "^3.0" }, "require-dev": { - "boundwize/structarmed": "0.7.12", + "boundwize/structarmed": "0.8.0", "codeigniter/phpstan-codeigniter": "^1.5", "fakerphp/faker": "^1.24", "kint-php/kint": "^6.1", From 48200cd1f99f9f23896a119c09a64636c3d7a953 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Fri, 29 May 2026 01:00:07 +0700 Subject: [PATCH 07/65] chore: bump PHPStan to ^2.2.1 and regenerate baseline (#10247) --- composer.json | 2 +- .../function.alreadyNarrowedType.neon | 13 ------------- .../function.impossibleType.neon | 18 ------------------ utils/phpstan-baseline/loader.neon | 4 +--- 4 files changed, 2 insertions(+), 35 deletions(-) delete mode 100644 utils/phpstan-baseline/function.alreadyNarrowedType.neon delete mode 100644 utils/phpstan-baseline/function.impossibleType.neon diff --git a/composer.json b/composer.json index 31ade5c8da14..a886223ce131 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "mikey179/vfsstream": "^1.6.12", "nexusphp/tachycardia": "^2.0", "phpstan/extension-installer": "^1.4", - "phpstan/phpstan": "^2.1.55", + "phpstan/phpstan": "^2.2.1", "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpcov": "^9.0.2 || ^10.0", "phpunit/phpunit": "^10.5.16 || ^11.2", diff --git a/utils/phpstan-baseline/function.alreadyNarrowedType.neon b/utils/phpstan-baseline/function.alreadyNarrowedType.neon deleted file mode 100644 index 3ca000aeecb5..000000000000 --- a/utils/phpstan-baseline/function.alreadyNarrowedType.neon +++ /dev/null @@ -1,13 +0,0 @@ -# total 2 errors - -parameters: - ignoreErrors: - - - message: '#^Call to function property_exists\(\) with \$this\(class@anonymous/tests/system/API/ResponseTraitTest\.php\:198\) and ''stringAsHtml'' will always evaluate to true\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Call to function property_exists\(\) with \$this\(class@anonymous/tests/system/API/ResponseTraitTest\.php\:318\) and ''stringAsHtml'' will always evaluate to true\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php diff --git a/utils/phpstan-baseline/function.impossibleType.neon b/utils/phpstan-baseline/function.impossibleType.neon deleted file mode 100644 index f13d9a4010c2..000000000000 --- a/utils/phpstan-baseline/function.impossibleType.neon +++ /dev/null @@ -1,18 +0,0 @@ -# total 3 errors - -parameters: - ignoreErrors: - - - message: '#^Call to function property_exists\(\) with \$this\(CodeIgniter\\Debug\\ExceptionHandler\) and ''stringAsHtml'' will always evaluate to false\.$#' - count: 1 - path: ../../system/Debug/ExceptionHandler.php - - - - message: '#^Call to function property_exists\(\) with \$this\(class@anonymous/tests/system/API/ResponseTraitTest\.php\:140\) and ''stringAsHtml'' will always evaluate to false\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Call to function property_exists\(\) with \$this\(class@anonymous/tests/system/API/ResponseTraitTest\.php\:638\) and ''stringAsHtml'' will always evaluate to false\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 815fa2376cc0..32a5e05024d3 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 2015 errors +# total 2010 errors includes: - argument.type.neon @@ -10,8 +10,6 @@ includes: - codeigniter.superglobalAccessAssign.neon - deadCode.unreachable.neon - empty.notAllowed.neon - - function.alreadyNarrowedType.neon - - function.impossibleType.neon - method.alreadyNarrowedType.neon - method.childParameterType.neon - method.childReturnType.neon From d6cc89f231039fff3d078115ff76cb5719e679ca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 01:01:16 +0700 Subject: [PATCH 08/65] chore(deps-dev): update boundwize/structarmed requirement (#10246) Updates the requirements on [boundwize/structarmed](https://github.com/boundwize/structarmed) to permit the latest version. Updates `boundwize/structarmed` to 0.8.1 - [Release notes](https://github.com/boundwize/structarmed/releases) - [Commits](https://github.com/boundwize/structarmed/compare/0.8.0...0.8.1) --- updated-dependencies: - dependency-name: boundwize/structarmed dependency-version: 0.8.1 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a886223ce131..f75023929f88 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "psr/log": "^3.0" }, "require-dev": { - "boundwize/structarmed": "0.8.0", + "boundwize/structarmed": "0.8.1", "codeigniter/phpstan-codeigniter": "^1.5", "fakerphp/faker": "^1.24", "kint-php/kint": "^6.1", From bde6a7c9046d573de7922649d6603f47d5c97eca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 30 May 2026 09:07:34 +0700 Subject: [PATCH 09/65] chore(deps-dev): update boundwize/structarmed requirement (#10249) Updates the requirements on [boundwize/structarmed](https://github.com/boundwize/structarmed) to permit the latest version. Updates `boundwize/structarmed` to 0.9.1 - [Release notes](https://github.com/boundwize/structarmed/releases) - [Commits](https://github.com/boundwize/structarmed/compare/0.8.1...0.9.1) --- updated-dependencies: - dependency-name: boundwize/structarmed dependency-version: 0.9.1 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index f75023929f88..51568a0e0406 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "psr/log": "^3.0" }, "require-dev": { - "boundwize/structarmed": "0.8.1", + "boundwize/structarmed": "0.9.1", "codeigniter/phpstan-codeigniter": "^1.5", "fakerphp/faker": "^1.24", "kint-php/kint": "^6.1", From 900af132b4b39f1a3d196b3474545ce7f56b1e3d Mon Sep 17 00:00:00 2001 From: memleakd <121398829+memleakd@users.noreply.github.com> Date: Sun, 31 May 2026 10:47:58 +0200 Subject: [PATCH 10/65] fix(http): detect Safari version from Version token (#10251) Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com> --- system/HTTP/UserAgent.php | 9 ++++++- tests/system/HTTP/UserAgentTest.php | 30 ++++++++++++++++++++- user_guide_src/source/changelogs/v4.7.4.rst | 1 + 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/system/HTTP/UserAgent.php b/system/HTTP/UserAgent.php index ba9e544ae2f8..fe7679e59384 100644 --- a/system/HTTP/UserAgent.php +++ b/system/HTTP/UserAgent.php @@ -317,8 +317,15 @@ protected function setBrowser(): bool if (is_array($this->config->browsers) && $this->config->browsers !== []) { foreach ($this->config->browsers as $key => $val) { if (preg_match('|' . $key . '.*?([0-9\.]+)|i', $this->agent, $match)) { + $version = $match[1]; + + // Safari's browser version is reported in the Version token. + if ($val === 'Safari' && preg_match('|Version/([0-9\.]+).*?Safari|i', $this->agent, $safariMatch)) { + $version = $safariMatch[1]; + } + $this->isBrowser = true; - $this->version = $match[1]; + $this->version = $version; $this->browser = $val; $this->setMobile(); diff --git a/tests/system/HTTP/UserAgentTest.php b/tests/system/HTTP/UserAgentTest.php index c43c46354e87..b19e0c62dee5 100644 --- a/tests/system/HTTP/UserAgentTest.php +++ b/tests/system/HTTP/UserAgentTest.php @@ -74,10 +74,38 @@ public function testBrowserInfo(): void { $this->assertSame('Mac OS X', $this->agent->getPlatform()); $this->assertSame('Safari', $this->agent->getBrowser()); - $this->assertSame('533.20.27', $this->agent->getVersion()); + $this->assertSame('5.0.4', $this->agent->getVersion()); $this->assertSame('', $this->agent->getRobot()); } + public function testParseModernSafariUsesVersionToken(): void + { + $this->agent->parse('Mozilla/5.0 (Macintosh; Intel Mac OS X 13_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Safari/605.1.15'); + + $this->assertSame('Mac OS X', $this->agent->getPlatform()); + $this->assertSame('Safari', $this->agent->getBrowser()); + $this->assertSame('16.3', $this->agent->getVersion()); + } + + public function testParseMobileSafariUsesVersionToken(): void + { + $this->agent->parse($this->_mobile_ua); + + $this->assertSame('iOS', $this->agent->getPlatform()); + $this->assertSame('Safari', $this->agent->getBrowser()); + $this->assertSame('4.0.5', $this->agent->getVersion()); + $this->assertSame('Apple iPhone', $this->agent->getMobile()); + } + + public function testParseChromeWithSafariTokenUsesChromeVersion(): void + { + $this->agent->parse('Mozilla/5.0 (Macintosh; Intel Mac OS X 13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'); + + $this->assertSame('Mac OS X', $this->agent->getPlatform()); + $this->assertSame('Chrome', $this->agent->getBrowser()); + $this->assertSame('110.0.0.0', $this->agent->getVersion()); + } + public function testParse(): void { $newAgent = 'Mozilla/5.0 (Android; Mobile; rv:13.0) Gecko/13.0 Firefox/13.0'; diff --git a/user_guide_src/source/changelogs/v4.7.4.rst b/user_guide_src/source/changelogs/v4.7.4.rst index 244823a000f7..daf64bbda631 100644 --- a/user_guide_src/source/changelogs/v4.7.4.rst +++ b/user_guide_src/source/changelogs/v4.7.4.rst @@ -31,6 +31,7 @@ Bugs Fixed ********** - **Database:** Fixed a bug where ``updateBatch()`` could be called after Query Builder ``where()`` conditions, even though it's not supported. In this situation, now the ``DatabaseException`` is thrown. +- **HTTP:** Fixed a bug where the User Agent library reported Safari's WebKit version instead of the browser version from the ``Version`` token. See the repo's `CHANGELOG.md `_ From 3dafe0bd63ac69ec0efb9a5ff1045e300214fc06 Mon Sep 17 00:00:00 2001 From: memleakd <121398829+memleakd@users.noreply.github.com> Date: Sun, 31 May 2026 10:48:16 +0200 Subject: [PATCH 11/65] docs: update official package status (#10252) Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com> --- .../source/libraries/official_packages.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/user_guide_src/source/libraries/official_packages.rst b/user_guide_src/source/libraries/official_packages.rst index ccadc81599bc..b13f3c5d6930 100644 --- a/user_guide_src/source/libraries/official_packages.rst +++ b/user_guide_src/source/libraries/official_packages.rst @@ -41,18 +41,18 @@ while defaulting to the config files when not custom value has been stored. This an application to ship with the default config values, but adapt as the project grows or moves servers, without having to touch the code. -************ -Tasks (BETA) -************ +***** +Tasks +***** `CodeIgniter Tasks `_ is a simple task scheduler for CodeIgniter 4. It allows you to schedule tasks to run at specific times, or on a recurring basis. It is designed to be simple to use, but flexible enough to handle most use cases. -************ -Queue (BETA) -************ +***** +Queue +***** `CodeIgniter Queue `_ is a simple queue system for CodeIgniter 4. It allows you to queue up tasks to be run later. From 0d38ef120b4b532f815e0d59ee3fe7dac07e21ce Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Wed, 3 Jun 2026 17:24:47 +0700 Subject: [PATCH 12/65] chore: Bump structarmed to 0.11.0 and use new expanded +LayerName in the config (#10273) --- composer.json | 2 +- structarmed.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 51568a0e0406..1416b9e8e8cb 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "psr/log": "^3.0" }, "require-dev": { - "boundwize/structarmed": "0.9.1", + "boundwize/structarmed": "0.11.0", "codeigniter/phpstan-codeigniter": "^1.5", "fakerphp/faker": "^1.24", "kint-php/kint": "^6.1", diff --git a/structarmed.php b/structarmed.php index 69bfaf1d197f..e1027f578ce2 100644 --- a/structarmed.php +++ b/structarmed.php @@ -101,7 +101,7 @@ 'Pager' => ['URI', 'View'], 'Publisher' => ['Files', 'URI'], // +API = API + its allowed layers; +Controller = Controller + its allowed layers - 'RESTful' => ['API', 'Controller', 'Database', 'Format', 'HTTP', 'Model', 'Pager', 'URI', 'Validation'], + 'RESTful' => ['+API', '+Controller'], 'Router' => ['HTTP', 'I18n'], 'Security' => ['Cookie', 'HTTP', 'I18n', 'Session'], 'Session' => ['Cookie', 'Database', 'HTTP', 'I18n'], From 387034096f5d81f3b6ef7a1811a823096ddf0150 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 19:10:59 +0200 Subject: [PATCH 13/65] chore(deps): bump actions/checkout in / (#10272) Bumps [actions/checkout](https://github.com/actions/checkout) in `/` from 6.0.2 to 6.0.3. Updates `actions/checkout` from 6.0.2 to 6.0.3 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/de0fac2e4500dabe0009e67214ff5f5447ce83dd...df4cb1c069e1874edd31b4311f1884172cec0e10) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.3 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-apidocs.yml | 4 ++-- .github/workflows/deploy-distributables.yml | 14 +++++++------- .github/workflows/deploy-userguide-latest.yml | 2 +- .github/workflows/label-add-conflict-all-pr.yml | 2 +- .github/workflows/label-pr.yml | 2 +- .github/workflows/label-signing.yml | 2 +- .github/workflows/reusable-coveralls.yml | 2 +- .github/workflows/reusable-phpunit-test.yml | 2 +- .../reusable-serviceless-phpunit-test.yml | 2 +- .github/workflows/test-autoreview.yml | 2 +- .github/workflows/test-coding-standards.yml | 2 +- .github/workflows/test-file-permissions.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-scss.yml | 2 +- .github/workflows/test-structarmed.yml | 2 +- .github/workflows/test-userguide.yml | 2 +- 19 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.github/workflows/deploy-apidocs.yml b/.github/workflows/deploy-apidocs.yml index 6bcaf710b7f0..7272ec7bec60 100644 --- a/.github/workflows/deploy-apidocs.yml +++ b/.github/workflows/deploy-apidocs.yml @@ -30,13 +30,13 @@ jobs: git config --global user.name "${GITHUB_ACTOR}" - name: Checkout source - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: path: source persist-credentials: false - name: Checkout target - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: repository: codeigniter4/api token: ${{ secrets.ACCESS_TOKEN }} diff --git a/.github/workflows/deploy-distributables.yml b/.github/workflows/deploy-distributables.yml index 388829840852..7d7c9af27071 100644 --- a/.github/workflows/deploy-distributables.yml +++ b/.github/workflows/deploy-distributables.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 # fetch all tags persist-credentials: false @@ -50,13 +50,13 @@ jobs: git config --global user.name "${GITHUB_ACTOR}" - name: Checkout source - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: path: source persist-credentials: false - name: Checkout target - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: repository: codeigniter4/framework token: ${{ secrets.ACCESS_TOKEN }} @@ -104,13 +104,13 @@ jobs: git config --global user.name "${GITHUB_ACTOR}" - name: Checkout source - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: path: source persist-credentials: false - name: Checkout target - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: repository: codeigniter4/appstarter token: ${{ secrets.ACCESS_TOKEN }} @@ -158,13 +158,13 @@ jobs: git config --global user.name "${GITHUB_ACTOR}" - name: Checkout source - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: path: source persist-credentials: false - name: Checkout target - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: repository: codeigniter4/userguide token: ${{ secrets.ACCESS_TOKEN }} diff --git a/.github/workflows/deploy-userguide-latest.yml b/.github/workflows/deploy-userguide-latest.yml index 7be8a04ba246..0cba414e591f 100644 --- a/.github/workflows/deploy-userguide-latest.yml +++ b/.github/workflows/deploy-userguide-latest.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false diff --git a/.github/workflows/label-add-conflict-all-pr.yml b/.github/workflows/label-add-conflict-all-pr.yml index a6f5ba276cbe..fb4cad824f7e 100644 --- a/.github/workflows/label-add-conflict-all-pr.yml +++ b/.github/workflows/label-add-conflict-all-pr.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Get PR List id: PR-list diff --git a/.github/workflows/label-pr.yml b/.github/workflows/label-pr.yml index 3fbe8a7a4489..d6c402ebc5e2 100644 --- a/.github/workflows/label-pr.yml +++ b/.github/workflows/label-pr.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false diff --git a/.github/workflows/label-signing.yml b/.github/workflows/label-signing.yml index 00cda536ef05..617f459bd7df 100644 --- a/.github/workflows/label-signing.yml +++ b/.github/workflows/label-signing.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Check signed commits in PR uses: 1Password/check-signed-commits-action@ed2885f3ed2577a4f5d3c3fe895432a557d23d52 # v1.2.0 diff --git a/.github/workflows/reusable-coveralls.yml b/.github/workflows/reusable-coveralls.yml index 385fda85aba6..ee00cf9e0ee5 100644 --- a/.github/workflows/reusable-coveralls.yml +++ b/.github/workflows/reusable-coveralls.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false diff --git a/.github/workflows/reusable-phpunit-test.yml b/.github/workflows/reusable-phpunit-test.yml index 2a4ed98cd9b9..62c94e334f1a 100644 --- a/.github/workflows/reusable-phpunit-test.yml +++ b/.github/workflows/reusable-phpunit-test.yml @@ -170,7 +170,7 @@ jobs: sudo apt-get install -y imagemagick libmagickwand-dev ghostscript poppler-data libjbig2dec0:amd64 libopenjp2-7:amd64 - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false diff --git a/.github/workflows/reusable-serviceless-phpunit-test.yml b/.github/workflows/reusable-serviceless-phpunit-test.yml index fc0faa4cae41..3df45b47822b 100644 --- a/.github/workflows/reusable-serviceless-phpunit-test.yml +++ b/.github/workflows/reusable-serviceless-phpunit-test.yml @@ -66,7 +66,7 @@ jobs: sudo apt-get install -y imagemagick libmagickwand-dev ghostscript poppler-data libjbig2dec0:amd64 libopenjp2-7:amd64 - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false fetch-depth: 0 diff --git a/.github/workflows/test-autoreview.yml b/.github/workflows/test-autoreview.yml index 77394c0517eb..32759a6f348f 100644 --- a/.github/workflows/test-autoreview.yml +++ b/.github/workflows/test-autoreview.yml @@ -36,7 +36,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Setup PHP uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 diff --git a/.github/workflows/test-coding-standards.yml b/.github/workflows/test-coding-standards.yml index f4853751f79e..d19fffce17d9 100644 --- a/.github/workflows/test-coding-standards.yml +++ b/.github/workflows/test-coding-standards.yml @@ -35,7 +35,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Setup PHP uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 diff --git a/.github/workflows/test-file-permissions.yml b/.github/workflows/test-file-permissions.yml index 8c61248d215f..33e13e5859b5 100644 --- a/.github/workflows/test-file-permissions.yml +++ b/.github/workflows/test-file-permissions.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Detect unnecessary execution permissions run: php utils/check_permission_x.php diff --git a/.github/workflows/test-phpstan.yml b/.github/workflows/test-phpstan.yml index 0801ce4d2741..8f3b6689883f 100644 --- a/.github/workflows/test-phpstan.yml +++ b/.github/workflows/test-phpstan.yml @@ -46,7 +46,7 @@ jobs: fail-fast: false steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Setup PHP uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 diff --git a/.github/workflows/test-psalm.yml b/.github/workflows/test-psalm.yml index abd8b1b58073..19bbce540964 100644 --- a/.github/workflows/test-psalm.yml +++ b/.github/workflows/test-psalm.yml @@ -36,7 +36,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false diff --git a/.github/workflows/test-random-execution.yml b/.github/workflows/test-random-execution.yml index 7774419e4a38..adbb28fa57d6 100644 --- a/.github/workflows/test-random-execution.yml +++ b/.github/workflows/test-random-execution.yml @@ -168,7 +168,7 @@ jobs: -Q "CREATE DATABASE test COLLATE Latin1_General_100_CS_AS_SC_UTF8" - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false fetch-depth: 0 diff --git a/.github/workflows/test-rector.yml b/.github/workflows/test-rector.yml index b8bef42af1dc..c6c7efc8c8ea 100644 --- a/.github/workflows/test-rector.yml +++ b/.github/workflows/test-rector.yml @@ -53,7 +53,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Setup PHP uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 diff --git a/.github/workflows/test-scss.yml b/.github/workflows/test-scss.yml index 919fae137812..36a08dc089a7 100644 --- a/.github/workflows/test-scss.yml +++ b/.github/workflows/test-scss.yml @@ -33,7 +33,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Setup Node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 diff --git a/.github/workflows/test-structarmed.yml b/.github/workflows/test-structarmed.yml index 030028f3a16c..bdbf8c5e9247 100644 --- a/.github/workflows/test-structarmed.yml +++ b/.github/workflows/test-structarmed.yml @@ -51,7 +51,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Setup PHP uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 diff --git a/.github/workflows/test-userguide.yml b/.github/workflows/test-userguide.yml index 310eff3277f9..fb6b52fdb5a4 100644 --- a/.github/workflows/test-userguide.yml +++ b/.github/workflows/test-userguide.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Setup Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 From d5c27fb92f17fc0b7a14bb459eacc6bf7f4977d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 23:31:42 +0700 Subject: [PATCH 14/65] chore(deps-dev): update boundwize/structarmed requirement (#10276) Updates the requirements on [boundwize/structarmed](https://github.com/boundwize/structarmed) to permit the latest version. Updates `boundwize/structarmed` to 0.11.1 - [Release notes](https://github.com/boundwize/structarmed/releases) - [Commits](https://github.com/boundwize/structarmed/compare/0.11.0...0.11.1) --- updated-dependencies: - dependency-name: boundwize/structarmed dependency-version: 0.11.1 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 1416b9e8e8cb..d2fc86bc39c0 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "psr/log": "^3.0" }, "require-dev": { - "boundwize/structarmed": "0.11.0", + "boundwize/structarmed": "0.11.1", "codeigniter/phpstan-codeigniter": "^1.5", "fakerphp/faker": "^1.24", "kint-php/kint": "^6.1", From 53fa63b318086713e63d36eb099d765a6a176dc3 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Fri, 5 Jun 2026 13:14:51 +0330 Subject: [PATCH 15/65] docs: fix transformer generator namespace command example (#10277) --- user_guide_src/source/outgoing/api_transformers.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/outgoing/api_transformers.rst b/user_guide_src/source/outgoing/api_transformers.rst index e462b588c159..5e2ab90ca5cb 100644 --- a/user_guide_src/source/outgoing/api_transformers.rst +++ b/user_guide_src/source/outgoing/api_transformers.rst @@ -72,7 +72,7 @@ The ``make:transformer`` command supports several options: .. code-block:: console - php spark make:transformer User --namespace="MyCompany\\API" + php spark make:transformer User --namespace MyCompany\API **--force** Forces overwriting an existing file: From ba1d524b02f33844b880bb6a00077ecf0d8b9faf Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Fri, 5 Jun 2026 17:56:00 +0330 Subject: [PATCH 16/65] chore: handle intentional `d()` calls in static analysis (#10279) * chore: handle intentional d() calls in static analysis * chore: update PHPStan baseline for intentional d() calls --- utils/phpstan-baseline/function.resultUnused.neon | 13 +++++++++++++ utils/phpstan-baseline/loader.neon | 3 ++- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 utils/phpstan-baseline/function.resultUnused.neon diff --git a/utils/phpstan-baseline/function.resultUnused.neon b/utils/phpstan-baseline/function.resultUnused.neon new file mode 100644 index 000000000000..7f8539d49820 --- /dev/null +++ b/utils/phpstan-baseline/function.resultUnused.neon @@ -0,0 +1,13 @@ +# total 2 errors + +parameters: + ignoreErrors: + - + message: '#^Call to function d\(\) on a separate line has no effect\.$#' + count: 1 + path: ../../system/Commands/Utilities/ConfigCheck.php + + - + message: '#^Call to function d\(\) on a separate line has no effect\.$#' + count: 1 + path: ../../tests/system/CommonFunctionsTest.php diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 32a5e05024d3..405c9a54ac16 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 2010 errors +# total 2012 errors includes: - argument.type.neon @@ -10,6 +10,7 @@ includes: - codeigniter.superglobalAccessAssign.neon - deadCode.unreachable.neon - empty.notAllowed.neon + - function.resultUnused.neon - method.alreadyNarrowedType.neon - method.childParameterType.neon - method.childReturnType.neon From ac186549b6f0f69db00e3a22b14f94619959bd2e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 6 Jun 2026 11:56:51 +0700 Subject: [PATCH 17/65] chore(deps-dev): update boundwize/structarmed requirement (#10280) Updates the requirements on [boundwize/structarmed](https://github.com/boundwize/structarmed) to permit the latest version. Updates `boundwize/structarmed` to 0.12.0 - [Release notes](https://github.com/boundwize/structarmed/releases) - [Commits](https://github.com/boundwize/structarmed/compare/0.11.1...0.12.0) --- updated-dependencies: - dependency-name: boundwize/structarmed dependency-version: 0.12.0 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d2fc86bc39c0..3e594e090a89 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "psr/log": "^3.0" }, "require-dev": { - "boundwize/structarmed": "0.11.1", + "boundwize/structarmed": "0.12.0", "codeigniter/phpstan-codeigniter": "^1.5", "fakerphp/faker": "^1.24", "kint-php/kint": "^6.1", From 167b544c014e23ac49b937e557d2dd436f4b44dc Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Sun, 7 Jun 2026 19:27:03 +0200 Subject: [PATCH 18/65] fix: nested transformer request scope leakage (#10281) --- system/API/BaseTransformer.php | 46 +++-- tests/_support/API/ChildTransformer.php | 31 ++++ tests/_support/API/ParentTransformer.php | 67 ++++++++ tests/system/API/TransformerTest.php | 160 ++++++++++++++++++ user_guide_src/source/changelogs/v4.7.4.rst | 1 + .../source/outgoing/api_transformers.rst | 15 ++ 6 files changed, 306 insertions(+), 14 deletions(-) create mode 100644 tests/_support/API/ChildTransformer.php create mode 100644 tests/_support/API/ParentTransformer.php diff --git a/system/API/BaseTransformer.php b/system/API/BaseTransformer.php index 540d443d1cdc..4badb8267139 100644 --- a/system/API/BaseTransformer.php +++ b/system/API/BaseTransformer.php @@ -54,6 +54,11 @@ */ abstract class BaseTransformer implements TransformerInterface { + /** + * Nesting depth of the transformation currently in progress (0 = root). + */ + private static int $depth = 0; + /** * @var list|null */ @@ -69,17 +74,24 @@ abstract class BaseTransformer implements TransformerInterface public function __construct( private ?IncomingRequest $request = null, ) { + // An explicitly provided request is always honored - its scope is + // intentional. Only the implicit global fallback is suppressed for + // nested transformers, which is the actual leak vector. + $explicitRequest = $request instanceof IncomingRequest; + $this->request = $request ?? request(); - $fields = $this->request->getGet('fields'); - $this->fields = is_string($fields) - ? array_map(trim(...), explode(',', $fields)) - : $fields; + if ($explicitRequest || self::$depth === 0) { + $fields = $this->request->getGet('fields'); + $this->fields = is_string($fields) + ? array_map(trim(...), explode(',', $fields)) + : $fields; - $includes = $this->request->getGet('include'); - $this->includes = is_string($includes) - ? array_map(trim(...), explode(',', $includes)) - : $includes; + $includes = $this->request->getGet('include'); + $this->includes = is_string($includes) + ? array_map(trim(...), explode(',', $includes)) + : $includes; + } } /** @@ -207,13 +219,19 @@ private function insertIncludes(array $data): array } } - foreach ($this->includes as $include) { - $method = 'include' . ucfirst($include); - if (method_exists($this, $method)) { - $data[$include] = $this->{$method}(); - } else { - throw ApiException::forMissingInclude($include); + self::$depth++; + + try { + foreach ($this->includes as $include) { + $method = 'include' . ucfirst($include); + if (method_exists($this, $method)) { + $data[$include] = $this->{$method}(); + } else { + throw ApiException::forMissingInclude($include); + } } + } finally { + self::$depth--; } return $data; diff --git a/tests/_support/API/ChildTransformer.php b/tests/_support/API/ChildTransformer.php new file mode 100644 index 000000000000..a226dad97608 --- /dev/null +++ b/tests/_support/API/ChildTransformer.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Tests\Support\API; + +use CodeIgniter\API\BaseTransformer; + +/** + * Nested transformer used to verify that the root request's scope + * (fields/includes) does not leak into related resources. + */ +class ChildTransformer extends BaseTransformer +{ + public function toArray(mixed $resource): array + { + return [ + 'child_id' => $resource['id'] ?? null, + 'status' => 'transformed', + ]; + } +} diff --git a/tests/_support/API/ParentTransformer.php b/tests/_support/API/ParentTransformer.php new file mode 100644 index 000000000000..803b4dbb353f --- /dev/null +++ b/tests/_support/API/ParentTransformer.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Tests\Support\API; + +use CodeIgniter\API\BaseTransformer; + +/** + * Root transformer used to verify that nested transformers do not inherit + * the root request's `fields`/`include` query state. + */ +class ParentTransformer extends BaseTransformer +{ + public function toArray(mixed $resource): array + { + return [ + 'parent_id' => $resource['id'] ?? null, + ]; + } + + /** + * Includes a single related child resource. + * + * @return array + */ + protected function includeChildren(): array + { + return (new ChildTransformer())->transform(['id' => 99]); + } + + /** + * Includes a collection of related child resources. + * + * @return list> + */ + protected function includeChildrenCollection(): array + { + return (new ChildTransformer())->transformMany([ + ['id' => 77], + ['id' => 88], + ]); + } + + /** + * Includes a single child while explicitly forwarding the request, opting + * the child into the request-derived scope even though it is nested. + * + * @return array + */ + protected function includeExplicitChild(): array + { + $childRequest = clone request(); + $childRequest->setGlobal('get', ['fields' => 'child_id']); + + return (new ChildTransformer($childRequest))->transform(['id' => 99]); + } +} diff --git a/tests/system/API/TransformerTest.php b/tests/system/API/TransformerTest.php index 8c6f50857d23..11f37f0fd859 100644 --- a/tests/system/API/TransformerTest.php +++ b/tests/system/API/TransformerTest.php @@ -22,6 +22,8 @@ use Config\Services; use PHPUnit\Framework\Attributes\Group; use stdClass; +use Tests\Support\API\ChildTransformer; +use Tests\Support\API\ParentTransformer; /** * @internal @@ -33,12 +35,14 @@ protected function setUp(): void { parent::setUp(); + Services::resetSingle('request'); Services::superglobals()->setGetArray([]); } protected function tearDown(): void { Services::superglobals()->setGetArray([]); + Services::resetSingle('request'); parent::tearDown(); } @@ -641,4 +645,160 @@ protected function includePosts(): array $this->assertArrayHasKey('posts', $result); $this->assertSame([['id' => 1, 'title' => 'Post 1']], $result['posts']); } + + public function testNestedTransformerDoesNotInheritIncludeState(): void + { + // The child transformer has no includeChildren() method. If the root + // request's `include=children` leaked into it, transforming the child + // would raise an ApiException for the missing include method. + $request = $this->createMockRequest('include=children'); + Services::injectMock('request', $request); + + $transformer = new ParentTransformer($request); + + $result = $transformer->transform(['id' => 1]); + + $this->assertSame([ + 'parent_id' => 1, + 'children' => ['child_id' => 99, 'status' => 'transformed'], + ], $result); + } + + public function testNestedTransformerDoesNotInheritFieldFilter(): void + { + // `fields=parent_id` applies to the root only. If it leaked into the + // child, array_intersect_key would strip every child field, leaving []. + $request = $this->createMockRequest('include=children&fields=parent_id'); + Services::injectMock('request', $request); + + $transformer = new ParentTransformer($request); + + $result = $transformer->transform(['id' => 1]); + + $this->assertSame([ + 'parent_id' => 1, + 'children' => ['child_id' => 99, 'status' => 'transformed'], + ], $result); + } + + public function testNestedCollectionTransformerDoesNotInheritState(): void + { + $request = $this->createMockRequest('include=childrenCollection&fields=parent_id'); + Services::injectMock('request', $request); + + $transformer = new ParentTransformer($request); + + $result = $transformer->transform(['id' => 1]); + + $this->assertSame([ + 'parent_id' => 1, + 'childrenCollection' => [ + ['child_id' => 77, 'status' => 'transformed'], + ['child_id' => 88, 'status' => 'transformed'], + ], + ], $result); + } + + public function testBareNestedInstantiationDoesNotInheritState(): void + { + // Reproduces the exact leak vector: a child created with a bare + // `new ChildTransformer()` (no request passed) inside an include + // method must not pick up the root request's scope from request(). + $request = $this->createMockRequest('include=children&fields=parent_id'); + Services::injectMock('request', $request); + + $root = new ParentTransformer(); + + $result = $root->transform(['id' => 5]); + + $this->assertSame([ + 'parent_id' => 5, + 'children' => ['child_id' => 99, 'status' => 'transformed'], + ], $result); + } + + public function testNestedTransformerHonorsExplicitRequest(): void + { + // A child created with an explicitly passed request must honor that + // request's scope even while nested - the isolation only suppresses + // the implicit global fallback, not deliberate developer intent. + $request = $this->createMockRequest('include=explicitChild&fields=child_id,parent_id'); + Services::injectMock('request', $request); + + $transformer = new ParentTransformer($request); + + $result = $transformer->transform(['id' => 1]); + + $this->assertSame([ + 'parent_id' => 1, + 'explicitChild' => ['child_id' => 99], + ], $result); + } + + public function testRootScopeStillAppliesAfterNesting(): void + { + // Sanity check that the root transformer keeps applying its own scope + // while nested children are isolated. + $request = $this->createMockRequest('include=children&fields=parent_id'); + Services::injectMock('request', $request); + + $transformer = new ParentTransformer($request); + + $result = $transformer->transform(['id' => 1, 'secret' => 'hidden']); + + // Root keeps only parent_id (plus the include key), the child is intact. + $this->assertArrayHasKey('parent_id', $result); + $this->assertArrayNotHasKey('secret', $result); + $this->assertSame(['child_id' => 99, 'status' => 'transformed'], $result['children']); + } + + public function testDepthIsRestoredAfterIncludeThrows(): void + { + $request = $this->createMockRequest('include=nonexistent'); + Services::injectMock('request', $request); + + $throwingRoot = new class ($request) extends BaseTransformer { + public function toArray(mixed $resource): array + { + return $resource; + } + }; + + try { + $throwingRoot->transform(['id' => 1, 'name' => 'Test']); + $this->fail('Expected ApiException was not thrown.'); + } catch (ApiException) { + // expected + } + + // The nesting depth must be balanced after the exception, so a fresh + // root transformer still applies the request scope (depth back to 0). + $fieldsRequest = $this->createMockRequest('fields=id'); + Services::injectMock('request', $fieldsRequest); + + $nextRoot = new class ($fieldsRequest) extends BaseTransformer { + public function toArray(mixed $resource): array + { + return $resource; + } + }; + + $result = $nextRoot->transform(['id' => 1, 'name' => 'Test']); + + $this->assertSame(['id' => 1], $result); + } + + public function testBareNestedTransformerStillUsedByChildTransformerDirectly(): void + { + // When ChildTransformer is itself the root (no parent), it must apply + // request scope as usual - the isolation only affects nesting. + $request = $this->createMockRequest('fields=child_id'); + Services::injectMock('request', $request); + + $transformer = new ChildTransformer($request); + + $result = $transformer->transform(['id' => 42]); + + $this->assertSame(['child_id' => 42], $result); + } } diff --git a/user_guide_src/source/changelogs/v4.7.4.rst b/user_guide_src/source/changelogs/v4.7.4.rst index daf64bbda631..7a38176717c0 100644 --- a/user_guide_src/source/changelogs/v4.7.4.rst +++ b/user_guide_src/source/changelogs/v4.7.4.rst @@ -30,6 +30,7 @@ Deprecations Bugs Fixed ********** +- **API:** Fixed a bug in Transformers where the root request's ``fields`` and ``include`` query parameters leaked into nested transformers created inside ``include*()`` methods, causing incorrect field filtering, unexpected includes, or infinite recursion. - **Database:** Fixed a bug where ``updateBatch()`` could be called after Query Builder ``where()`` conditions, even though it's not supported. In this situation, now the ``DatabaseException`` is thrown. - **HTTP:** Fixed a bug where the User Agent library reported Safari's WebKit version instead of the browser version from the ``Version`` token. diff --git a/user_guide_src/source/outgoing/api_transformers.rst b/user_guide_src/source/outgoing/api_transformers.rst index 5e2ab90ca5cb..38ce1de1fc14 100644 --- a/user_guide_src/source/outgoing/api_transformers.rst +++ b/user_guide_src/source/outgoing/api_transformers.rst @@ -174,6 +174,14 @@ The response would include: ] } +.. note:: The ``fields`` and ``include`` query parameters describe the **root** resource only. + Transformers instantiated inside an ``include*()`` method (such as ``PostTransformer`` above) + are treated as nested resources: when created **without an explicit request** they do **not** + inherit the root request's ``fields``/``include`` state. This prevents the parent's query + parameters from leaking into related resources, which could otherwise cause incorrect field + filtering or unexpected/recursive includes. If you deliberately pass a request to a nested + transformer, that request's scope is honored. + Restricting Available Includes =============================== @@ -262,6 +270,13 @@ Class Reference Initializes the transformer and extracts the ``fields`` and ``include`` query parameters from the request. + .. note:: When no request is explicitly passed, the global request's ``fields`` and ``include`` + are read **only for the root transformer**. A transformer constructed during a nested + transformation (i.e. inside an ``include*()`` method) without an explicit request still uses + the global request object, but does not read ``fields``/``include`` from it, to avoid leaking + the root's scope. Passing an explicit request always applies that request's scope, regardless + of nesting. + .. php:method:: toArray(mixed $resource) :param mixed $resource: The resource being transformed (Entity, array, object, or null) From 8ffe6c941c09de54da22d13af32d242985d3cd1c Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Mon, 8 Jun 2026 07:32:39 +0200 Subject: [PATCH 19/65] fix(database): use currentRow instead of customResultObject in getRowObject and add null fallback (#10257) --- system/Database/BaseResult.php | 10 +- tests/system/Database/BaseResultTest.php | 332 +++++++++++++++++++++++ 2 files changed, 337 insertions(+), 5 deletions(-) create mode 100644 tests/system/Database/BaseResultTest.php diff --git a/system/Database/BaseResult.php b/system/Database/BaseResult.php index c0bdc2aa1025..5bede241a59d 100644 --- a/system/Database/BaseResult.php +++ b/system/Database/BaseResult.php @@ -312,7 +312,7 @@ public function getCustomRowObject(int $n, string $className) $this->currentRow = $n; } - return $this->customResultObject[$className][$this->currentRow]; + return $this->customResultObject[$className][$this->currentRow] ?? null; } /** @@ -333,7 +333,7 @@ public function getRowArray(int $n = 0) $this->currentRow = $n; } - return $result[$this->currentRow]; + return $result[$this->currentRow] ?? null; } /** @@ -350,11 +350,11 @@ public function getRowObject(int $n = 0) return null; } - if ($n !== $this->customResultObject && isset($result[$n])) { + if ($n !== $this->currentRow && isset($result[$n])) { $this->currentRow = $n; } - return $result[$this->currentRow]; + return $result[$this->currentRow] ?? null; } /** @@ -440,7 +440,7 @@ public function getPreviousRow(string $type = 'object') $this->currentRow--; } - return $result[$this->currentRow]; + return $result[$this->currentRow] ?? null; } /** diff --git a/tests/system/Database/BaseResultTest.php b/tests/system/Database/BaseResultTest.php new file mode 100644 index 000000000000..901faee6a565 --- /dev/null +++ b/tests/system/Database/BaseResultTest.php @@ -0,0 +1,332 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Database; + +use CodeIgniter\Test\CIUnitTestCase; +use PHPUnit\Framework\Attributes\Group; +use stdClass; + +/** + * @internal + */ +#[Group('Others')] +final class BaseResultTest extends CIUnitTestCase +{ + /** + * Create a minimal concrete implementation of BaseResult for testing. + * + * @param list> $resultArray Result set as arrays. + * @param list $resultObject Result set as objects. + * + * @return BaseResult + */ + private function createResultDouble(array $resultArray, array $resultObject): BaseResult + { + return new + /** + * @extends BaseResult + */ + class ($resultArray, $resultObject) extends BaseResult { + /** + * @param list> $resultArray Result set as arrays. + * @param list $resultObject Result set as objects. + */ + public function __construct(array $resultArray, array $resultObject) + { + $this->resultArray = $resultArray; + $this->resultObject = $resultObject; + $this->currentRow = 0; + + $connId = null; + $resultId = null; + parent::__construct($connId, $resultId); + } + + public function getFieldCount(): int + { + return 0; + } + + /** + * @return list + */ + public function getFieldNames(): array + { + return []; + } + + /** + * @return list + */ + public function getFieldData(): array + { + return []; + } + + public function freeResult(): void + { + } + + public function dataSeek(int $n = 0): bool + { + return true; + } + + /** + * @return false|list>|null + */ + protected function fetchAssoc(): array|bool|null + { + return false; + } + + protected function fetchObject(string $className = stdClass::class) + { + return false; + } + }; + } + + // -------------------------------------------------------------------- + // getRowArray() + // -------------------------------------------------------------------- + + public function testGetRowArrayReturnsRow(): void + { + $result = $this->createResultDouble( + [ + ['id' => 1, 'name' => 'John'], + ['id' => 2, 'name' => 'Jane'], + ], + [], + ); + + $this->assertSame(['id' => 1, 'name' => 'John'], $result->getRowArray(0)); + $this->assertSame(['id' => 2, 'name' => 'Jane'], $result->getRowArray(1)); + } + + public function testGetRowArrayReturnsNullForEmptyResult(): void + { + $result = $this->createResultDouble([], []); + + $this->assertNull($result->getRowArray(0)); + } + + public function testGetRowArrayReturnsFirstRowByDefault(): void + { + $result = $this->createResultDouble( + [ + ['id' => 1, 'name' => 'John'], + ['id' => 2, 'name' => 'Jane'], + ], + [], + ); + + $this->assertSame(['id' => 1, 'name' => 'John'], $result->getRowArray()); + } + + // -------------------------------------------------------------------- + // getRowObject() + // -------------------------------------------------------------------- + + public function testGetRowObjectReturnsObject(): void + { + $row1 = new stdClass(); + $row1->id = 1; + $row1->name = 'John'; + $row2 = new stdClass(); + $row2->id = 2; + $row2->name = 'Jane'; + + $result = $this->createResultDouble([], [$row1, $row2]); + + $this->assertSame($row1, $result->getRowObject(0)); + $this->assertSame($row2, $result->getRowObject(1)); + } + + public function testGetRowObjectReturnsNullForEmptyResult(): void + { + $result = $this->createResultDouble([], []); + + $this->assertNull($result->getRowObject(0)); + } + + public function testGetRowObjectReturnsFirstRowByDefault(): void + { + $row1 = new stdClass(); + $row1->id = 1; + $row1->name = 'John'; + + $result = $this->createResultDouble([], [$row1]); + + $this->assertSame($row1, $result->getRowObject()); + } + + public function testGetRowObjectAndGetRowArrayShareCurrentRow(): void + { + $row1 = new stdClass(); + $row1->id = 1; + $row1->name = 'John'; + $row2 = new stdClass(); + $row2->id = 2; + $row2->name = 'Jane'; + + $result = $this->createResultDouble( + [ + ['id' => 1, 'name' => 'John'], + ['id' => 2, 'name' => 'Jane'], + ], + [$row1, $row2], + ); + + // getRowObject(1) should advance currentRow to 1 (same as getRowArray would) + $result->getRowObject(1); + $this->assertSame(['id' => 2, 'name' => 'Jane'], $result->getRowArray(1)); + } + + public function testGetRowObjectUsesCurrentRowLikeGetRowArray(): void + { + $row1 = new stdClass(); + $row1->id = 1; + $row1->name = 'John'; + $row2 = new stdClass(); + $row2->id = 2; + $row2->name = 'Jane'; + + $result = $this->createResultDouble( + [ + ['id' => 1, 'name' => 'John'], + ['id' => 2, 'name' => 'Jane'], + ], + [$row1, $row2], + ); + + // Both methods should advance currentRow consistently + $result->getRowObject(1); + $result->getRowArray(); + $this->assertSame($row1, $result->getRowObject()); + } + + // -------------------------------------------------------------------- + // getRow() — convenience wrapper + // -------------------------------------------------------------------- + + public function testGetRowWithInvalidIndexReturnsFirstRow(): void + { + $result = $this->createResultDouble( + [['id' => 1, 'name' => 'John']], + [], + ); + + $this->assertSame(['id' => 1, 'name' => 'John'], $result->getRow(999, 'array')); + } + + public function testGetRowObjectWithInvalidIndexReturnsFirstRow(): void + { + $row1 = new stdClass(); + $row1->id = 1; + $row1->name = 'John'; + + $result = $this->createResultDouble([], [$row1]); + + $this->assertSame($row1, $result->getRow(999, 'object')); + } + + public function testGetRowNullForColumnNameNotFound(): void + { + $result = $this->createResultDouble( + [['id' => 1, 'name' => 'John']], + [], + ); + + $this->assertNull($result->getRow('nonexistent', 'array')); + } + + // -------------------------------------------------------------------- + // Custom Result Object + // -------------------------------------------------------------------- + + public function testGetCustomRowObjectWithInvalidIndexReturnsFirstRow(): void + { + $row = new stdClass(); + $row->id = 1; + $row->name = 'John'; + + $result = $this->createResultDouble([], []); + $result->customResultObject[stdClass::class] = [$row]; + + $this->assertSame($row, $result->getCustomRowObject(999, stdClass::class)); + } + + // -------------------------------------------------------------------- + // Fallback Tests (Null return on invalid currentRow) + // -------------------------------------------------------------------- + + public function testGetRowArrayReturnsNullWhenCurrentRowIsInvalid(): void + { + $result = $this->createResultDouble( + [['id' => 1, 'name' => 'John']], + [], + ); + + $result->currentRow = 999; + + $this->assertNull($result->getRowArray(999)); + } + + public function testGetRowObjectReturnsNullWhenCurrentRowIsInvalid(): void + { + $row1 = new stdClass(); + $row1->id = 1; + $row1->name = 'John'; + + $result = $this->createResultDouble( + [], + [$row1], + ); + + $result->currentRow = 999; + + $this->assertNull($result->getRowObject(999)); + } + + public function testGetCustomRowObjectReturnsNullWhenCurrentRowIsInvalid(): void + { + $row1 = new stdClass(); + $row1->id = 1; + $row1->name = 'John'; + + $result = $this->createResultDouble([], []); + $result->customResultObject[stdClass::class] = [$row1]; + + $result->currentRow = 999; + + $this->assertNotInstanceOf(stdClass::class, $result->getCustomRowObject(999, stdClass::class)); + } + + public function testGetPreviousRowReturnsNullWhenCurrentRowIsInvalid(): void + { + $result = $this->createResultDouble( + [ + ['id' => 1], + ['id' => 2], + ], + [], + ); + + $result->currentRow = -1; + + $this->assertNull($result->getPreviousRow('array')); + } +} From 60ca0a03bd31944f1b6d8e33afc0a42f34799850 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 22:58:24 +0700 Subject: [PATCH 20/65] chore(deps-dev): update boundwize/structarmed requirement (#10291) Updates the requirements on [boundwize/structarmed](https://github.com/boundwize/structarmed) to permit the latest version. Updates `boundwize/structarmed` to 0.12.5 - [Release notes](https://github.com/boundwize/structarmed/releases) - [Commits](https://github.com/boundwize/structarmed/compare/0.12.0...0.12.5) --- updated-dependencies: - dependency-name: boundwize/structarmed dependency-version: 0.12.5 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3e594e090a89..e88ac1dea26e 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "psr/log": "^3.0" }, "require-dev": { - "boundwize/structarmed": "0.12.0", + "boundwize/structarmed": "0.12.5", "codeigniter/phpstan-codeigniter": "^1.5", "fakerphp/faker": "^1.24", "kint-php/kint": "^6.1", From 7aade042a65280fd71b9aa08a8709ce81527d96e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 22:54:03 +0700 Subject: [PATCH 21/65] chore(deps-dev): update boundwize/structarmed requirement (#10292) Updates the requirements on [boundwize/structarmed](https://github.com/boundwize/structarmed) to permit the latest version. Updates `boundwize/structarmed` to 0.12.6 - [Release notes](https://github.com/boundwize/structarmed/releases) - [Commits](https://github.com/boundwize/structarmed/compare/0.12.5...0.12.6) --- updated-dependencies: - dependency-name: boundwize/structarmed dependency-version: 0.12.6 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e88ac1dea26e..6fc90fbb3340 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "psr/log": "^3.0" }, "require-dev": { - "boundwize/structarmed": "0.12.5", + "boundwize/structarmed": "0.12.6", "codeigniter/phpstan-codeigniter": "^1.5", "fakerphp/faker": "^1.24", "kint-php/kint": "^6.1", From 1bee8837ce13b071702bfec5d93c1b13c4d9c6f3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 19:23:09 +0200 Subject: [PATCH 22/65] chore(deps): bump shivammathur/setup-php in / (#10293) Bumps [shivammathur/setup-php](https://github.com/shivammathur/setup-php) in `/` from 2.37.1 to 2.37.2. Updates `shivammathur/setup-php` from 2.37.1 to 2.37.2 - [Release notes](https://github.com/shivammathur/setup-php/releases) - [Commits](https://github.com/shivammathur/setup-php/compare/7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc...f3e473d116dcccaddc5834248c87452386958240) --- updated-dependencies: - dependency-name: shivammathur/setup-php dependency-version: 2.37.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-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 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/deploy-apidocs.yml b/.github/workflows/deploy-apidocs.yml index 7272ec7bec60..01cd0c83d28a 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@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 with: php-version: '8.2' tools: phive diff --git a/.github/workflows/deploy-userguide-latest.yml b/.github/workflows/deploy-userguide-latest.yml index 0cba414e591f..92c7706eaa6a 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@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 with: php-version: '8.2' coverage: none diff --git a/.github/workflows/reusable-coveralls.yml b/.github/workflows/reusable-coveralls.yml index ee00cf9e0ee5..4786f6ee433c 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@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 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 62c94e334f1a..0fa024b71c61 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@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 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 3df45b47822b..04333f91ef6f 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@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 with: php-version: ${{ inputs.php-version }} tools: composer diff --git a/.github/workflows/test-autoreview.yml b/.github/workflows/test-autoreview.yml index 32759a6f348f..ff816aaeb519 100644 --- a/.github/workflows/test-autoreview.yml +++ b/.github/workflows/test-autoreview.yml @@ -39,7 +39,7 @@ jobs: uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Setup PHP - uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 with: php-version: '8.2' diff --git a/.github/workflows/test-coding-standards.yml b/.github/workflows/test-coding-standards.yml index d19fffce17d9..7bbe338c37c7 100644 --- a/.github/workflows/test-coding-standards.yml +++ b/.github/workflows/test-coding-standards.yml @@ -38,7 +38,7 @@ jobs: uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Setup PHP - uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 with: php-version: ${{ matrix.php-version }} extensions: tokenizer diff --git a/.github/workflows/test-phpstan.yml b/.github/workflows/test-phpstan.yml index 8f3b6689883f..6c1461cb3318 100644 --- a/.github/workflows/test-phpstan.yml +++ b/.github/workflows/test-phpstan.yml @@ -49,7 +49,7 @@ jobs: uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Setup PHP - uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 with: php-version: '8.2' extensions: intl diff --git a/.github/workflows/test-psalm.yml b/.github/workflows/test-psalm.yml index 19bbce540964..e417d3f99746 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@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 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 adbb28fa57d6..9b8d64160e47 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@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 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 c6c7efc8c8ea..25079edbd2b2 100644 --- a/.github/workflows/test-rector.yml +++ b/.github/workflows/test-rector.yml @@ -56,7 +56,7 @@ jobs: uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Setup PHP - uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 with: php-version: ${{ matrix.php-version }} extensions: intl diff --git a/.github/workflows/test-structarmed.yml b/.github/workflows/test-structarmed.yml index bdbf8c5e9247..905e26ab4f31 100644 --- a/.github/workflows/test-structarmed.yml +++ b/.github/workflows/test-structarmed.yml @@ -54,7 +54,7 @@ jobs: uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Setup PHP - uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 with: php-version: ${{ matrix.php-version }} extensions: intl From e7ee5b08d4d4da479d2a59b0965f2a709ddebcaa Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Thu, 11 Jun 2026 08:26:28 +0200 Subject: [PATCH 23/65] fix(model): pass $recursive parameter to parent in objectToRawArray (#10258) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: pass $recursive parameter to parent in Model::objectToRawArray * Update tests/system/Models/ObjectToRawArrayModelTest.php Co-authored-by: Michal Sniatala * fix: remove redundant objectToRawArray method and update changelog * Update tests/system/Models/ObjectToRawArrayModelTest.php Co-authored-by: Michal Sniatala * Update tests/system/Models/ObjectToRawArrayModelTest.php Co-authored-by: Michal Sniatala * Update test comment: indicate fix for model‑recursive PR #10258 * test: Fix PHPStan missing type in callObjectToRawArray * style: remove redundant comment in ObjectToRawArrayModelTest.php --------- Co-authored-by: Michal Sniatala --- system/Model.php | 5 - .../Models/ObjectToRawArrayModelTest.php | 129 ++++++++++++++++++ user_guide_src/source/changelogs/v4.7.4.rst | 1 + 3 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 tests/system/Models/ObjectToRawArrayModelTest.php diff --git a/system/Model.php b/system/Model.php index f4ddd75276d3..3599ba963176 100644 --- a/system/Model.php +++ b/system/Model.php @@ -711,11 +711,6 @@ public function update($id = null, $row = null): bool return parent::update($id, $row); } - protected function objectToRawArray($object, bool $onlyChanged = true, bool $recursive = false): array - { - return parent::objectToRawArray($object, $onlyChanged); - } - /** * Provides/instantiates the builder/db connection and model's table/primary key names and return type. * diff --git a/tests/system/Models/ObjectToRawArrayModelTest.php b/tests/system/Models/ObjectToRawArrayModelTest.php new file mode 100644 index 000000000000..8ee7460c9d52 --- /dev/null +++ b/tests/system/Models/ObjectToRawArrayModelTest.php @@ -0,0 +1,129 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Models; + +use CodeIgniter\Entity\Entity; +use CodeIgniter\Model; +use CodeIgniter\Test\CIUnitTestCase; +use PHPUnit\Framework\Attributes\Group; + +/** + * @internal + */ +#[Group('Others')] +final class ObjectToRawArrayModelTest extends CIUnitTestCase +{ + private function createModel(): Model + { + return new class () extends Model { + public function __construct() + { + // Skip DB connection — we only test objectToRawArray + } + + protected $table = 'test'; + protected $allowedFields = ['name', 'nested', 'entity']; + protected $returnType = 'array'; + protected $useSoftDeletes = false; + }; + } + + /** + * Call protected objectToRawArray via reflection. + * + * @return array + */ + private function callObjectToRawArray(Model $model, object $object, bool $onlyChanged, bool $recursive): array + { + $method = self::getPrivateMethodInvoker($model, 'objectToRawArray'); + + return $method($object, $onlyChanged, $recursive); + } + + public function testObjectToRawArrayPassesRecursiveTrue(): void + { + $model = $this->createModel(); + + $inner = new class () extends Entity { + protected $attributes = ['name' => 'inner']; + protected $original = ['name' => 'inner']; + }; + + $outer = new class () extends Entity { + protected $attributes = ['name' => 'outer', 'nested' => null]; + protected $original = ['name' => 'outer', 'nested' => null]; + }; + $outer->nested = $inner; + + $result = $this->callObjectToRawArray($model, $outer, false, true); + + $this->assertArrayHasKey('name', $result); + $this->assertSame('outer', $result['name']); + $this->assertArrayHasKey('nested', $result); + $this->assertIsArray($result['nested']); + $this->assertSame(['name' => 'inner'], $result['nested']); + } + + public function testObjectToRawArrayPassesRecursiveFalse(): void + { + $model = $this->createModel(); + + $inner = new class () extends Entity { + protected $attributes = ['name' => 'inner']; + protected $original = ['name' => 'inner']; + }; + + $outer = new class () extends Entity { + protected $attributes = ['name' => 'outer', 'nested' => null]; + protected $original = ['name' => 'outer', 'nested' => null]; + }; + $outer->nested = $inner; + + $result = $this->callObjectToRawArray($model, $outer, false, false); + + $this->assertArrayHasKey('name', $result); + $this->assertSame('outer', $result['name']); + $this->assertArrayHasKey('nested', $result); + // With recursive=false, nested Entity should remain as object + $this->assertInstanceOf(Entity::class, $result['nested']); + } + + public function testObjectToRawArrayNonEntity(): void + { + $model = $this->createModel(); + + $obj = new class () { + public string $name = 'test'; + public string $value = '123'; + }; + + $result = $this->callObjectToRawArray($model, $obj, false, false); + + $this->assertSame(['name' => 'test', 'value' => '123'], $result); + } + + public function testObjectToRawArrayOnlyChanged(): void + { + $model = $this->createModel(); + $entity = new class () extends Entity { + protected $attributes = ['name' => 'original', 'value' => 'keep']; + protected $original = ['name' => 'original', 'value' => 'keep']; + }; + $entity->name = 'modified'; + + $result = $this->callObjectToRawArray($model, $entity, true, false); + + $this->assertSame(['name' => 'modified'], $result); + } +} diff --git a/user_guide_src/source/changelogs/v4.7.4.rst b/user_guide_src/source/changelogs/v4.7.4.rst index 7a38176717c0..f9cc995ad22a 100644 --- a/user_guide_src/source/changelogs/v4.7.4.rst +++ b/user_guide_src/source/changelogs/v4.7.4.rst @@ -33,6 +33,7 @@ Bugs Fixed - **API:** Fixed a bug in Transformers where the root request's ``fields`` and ``include`` query parameters leaked into nested transformers created inside ``include*()`` methods, causing incorrect field filtering, unexpected includes, or infinite recursion. - **Database:** Fixed a bug where ``updateBatch()`` could be called after Query Builder ``where()`` conditions, even though it's not supported. In this situation, now the ``DatabaseException`` is thrown. - **HTTP:** Fixed a bug where the User Agent library reported Safari's WebKit version instead of the browser version from the ``Version`` token. +- **Model:** Fixed a bug in ``Model::objectToRawArray()`` where the ``$recursive`` parameter was ignored. See the repo's `CHANGELOG.md `_ From bce872b40cccbe8b324ce5497a3e3619581e5644 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Sat, 13 Jun 2026 02:47:25 +0800 Subject: [PATCH 24/65] fix: preserve enclosing stream filter when using `MockInputOutput` (#10307) --- system/Test/Filters/CITestStreamFilter.php | 16 +++++ system/Test/Mock/MockInputOutput.php | 32 +++++++++ .../system/Test/Mock/MockInputOutputTest.php | 68 +++++++++++++++++++ user_guide_src/source/changelogs/v4.7.4.rst | 1 + 4 files changed, 117 insertions(+) create mode 100644 tests/system/Test/Mock/MockInputOutputTest.php diff --git a/system/Test/Filters/CITestStreamFilter.php b/system/Test/Filters/CITestStreamFilter.php index 538c0741ba8d..fa91aa69ef55 100644 --- a/system/Test/Filters/CITestStreamFilter.php +++ b/system/Test/Filters/CITestStreamFilter.php @@ -92,6 +92,22 @@ public static function removeOutputFilter(): void self::removeFilter(self::$out); } + /** + * Whether an output filter is currently attached to STDOUT. + */ + public static function hasOutputFilter(): bool + { + return self::$out !== null; + } + + /** + * Whether an error filter is currently attached to STDERR. + */ + public static function hasErrorFilter(): bool + { + return self::$err !== null; + } + /** * @param resource|null $stream * diff --git a/system/Test/Mock/MockInputOutput.php b/system/Test/Mock/MockInputOutput.php index 6aa4779fb742..9bf8a4f54810 100644 --- a/system/Test/Mock/MockInputOutput.php +++ b/system/Test/Mock/MockInputOutput.php @@ -35,6 +35,16 @@ final class MockInputOutput extends InputOutput */ private array $outputs = []; + /** + * Snapshot of the shared CITestStreamFilter state captured before this + * object attaches its own filters, restored once it is done. Lets a test + * combine MockInputOutput with an enclosing StreamFilterTrait without the + * latter's filters being torn down. + * + * @var array{output: bool, error: bool, buffer: string}|null + */ + private ?array $priorFilterState = null; + /** * Sets user inputs. * @@ -88,6 +98,12 @@ public function getOutputs(): array private function addStreamFilters(): void { + $this->priorFilterState = [ + 'output' => CITestStreamFilter::hasOutputFilter(), + 'error' => CITestStreamFilter::hasErrorFilter(), + 'buffer' => CITestStreamFilter::$buffer, + ]; + CITestStreamFilter::registration(); CITestStreamFilter::addOutputFilter(); CITestStreamFilter::addErrorFilter(); @@ -97,6 +113,22 @@ private function removeStreamFilters(): void { CITestStreamFilter::removeOutputFilter(); CITestStreamFilter::removeErrorFilter(); + + if ($this->priorFilterState === null) { + return; + } + + CITestStreamFilter::$buffer = $this->priorFilterState['buffer']; + + if ($this->priorFilterState['output']) { + CITestStreamFilter::addOutputFilter(); + } + + if ($this->priorFilterState['error']) { + CITestStreamFilter::addErrorFilter(); + } + + $this->priorFilterState = null; } public function input(?string $prefix = null): string diff --git a/tests/system/Test/Mock/MockInputOutputTest.php b/tests/system/Test/Mock/MockInputOutputTest.php new file mode 100644 index 000000000000..9ac7aefb547a --- /dev/null +++ b/tests/system/Test/Mock/MockInputOutputTest.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Test\Mock; + +use CodeIgniter\CLI\CLI; +use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\StreamFilterTrait; +use PHPUnit\Framework\Attributes\Group; + +/** + * @internal + */ +#[Group('Others')] +final class MockInputOutputTest extends CIUnitTestCase +{ + use StreamFilterTrait; + + protected function tearDown(): void + { + parent::tearDown(); + + CLI::resetInputOutput(); + } + + public function testFwriteThroughMockPreservesEnclosingStreamFilter(): void + { + CLI::write('before mock'); + $this->assertStringContainsString('before mock', $this->getStreamFilterBuffer()); + + $io = new MockInputOutput(); + CLI::setInputOutput($io); + CLI::write('through mock'); + CLI::resetInputOutput(); + + // The mock captured its own write into its own buffer... + $this->assertStringContainsString('through mock', $io->getOutput()); + // ...and left the enclosing StreamFilterTrait buffer untouched. + $this->assertStringNotContainsString('through mock', $this->getStreamFilterBuffer()); + + // The enclosing filter is still attached, so later writes are captured. + CLI::write('after mock'); + $this->assertStringContainsString('before mock', $this->getStreamFilterBuffer()); + $this->assertStringContainsString('after mock', $this->getStreamFilterBuffer()); + } + + public function testInputThroughMockPreservesEnclosingStreamFilter(): void + { + $io = new MockInputOutput(); + $io->setInputs(['y']); + CLI::setInputOutput($io); + CLI::prompt('Continue?', ['y', 'n']); + CLI::resetInputOutput(); + + CLI::write('after prompt'); + $this->assertStringContainsString('after prompt', $this->getStreamFilterBuffer()); + } +} diff --git a/user_guide_src/source/changelogs/v4.7.4.rst b/user_guide_src/source/changelogs/v4.7.4.rst index f9cc995ad22a..41ecf19e9b04 100644 --- a/user_guide_src/source/changelogs/v4.7.4.rst +++ b/user_guide_src/source/changelogs/v4.7.4.rst @@ -34,6 +34,7 @@ Bugs Fixed - **Database:** Fixed a bug where ``updateBatch()`` could be called after Query Builder ``where()`` conditions, even though it's not supported. In this situation, now the ``DatabaseException`` is thrown. - **HTTP:** Fixed a bug where the User Agent library reported Safari's WebKit version instead of the browser version from the ``Version`` token. - **Model:** Fixed a bug in ``Model::objectToRawArray()`` where the ``$recursive`` parameter was ignored. +- **Testing:** Fixed a bug where using ``MockInputOutput`` within a test that also uses ``StreamFilterTrait`` tore down the trait's stream filters, so CLI output produced after the ``MockInputOutput`` interaction (such as in ``tearDown()``) was no longer captured and leaked to the console. See the repo's `CHANGELOG.md `_ From dd409abfa09b4e15216eb5e3bc1ae790f0530579 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Sat, 13 Jun 2026 02:48:08 +0800 Subject: [PATCH 25/65] fix: skip already-translated keys in `spark lang:find` (#10308) --- .../Translation/LocalizationFinder.php | 37 ++++++++++- .../Translation/LocalizationFinderTest.php | 65 +++++++++++++++++++ user_guide_src/source/changelogs/v4.7.4.rst | 1 + .../source/outgoing/localization.rst | 4 ++ 4 files changed, 105 insertions(+), 2 deletions(-) diff --git a/system/Commands/Translation/LocalizationFinder.php b/system/Commands/Translation/LocalizationFinder.php index e6557b74d1c4..ca7a1fb20230 100644 --- a/system/Commands/Translation/LocalizationFinder.php +++ b/system/Commands/Translation/LocalizationFinder.php @@ -133,13 +133,17 @@ private function process(string $currentDir, string $currentLocale): void $languageStoredKeys = require $languageFilePath; } - $languageDiff = ArrayHelper::recursiveDiff($foundLanguageKeys[$langFileName], $languageStoredKeys); + // Keys already resolvable from any namespace's language file (framework, packages, or app) + // are not new and must not be re-reported or written. + $resolvedKeys = $this->findResolvedTranslations($langFileName, $currentLocale); + + $languageDiff = ArrayHelper::recursiveDiff($foundLanguageKeys[$langFileName], $resolvedKeys); $countNewKeys += ArrayHelper::recursiveCount($languageDiff); if ($this->showNew) { $tableRows = array_merge($this->arrayToTableRows($langFileName, $languageDiff), $tableRows); } else { - $newLanguageKeys = array_replace_recursive($foundLanguageKeys[$langFileName], $languageStoredKeys); + $newLanguageKeys = array_replace_recursive($languageDiff, $languageStoredKeys); if ($languageDiff !== []) { if (file_put_contents($languageFilePath, $this->templateFile($newLanguageKeys)) === false) { @@ -177,6 +181,35 @@ private function process(string $currentDir, string $currentLocale): void } } + /** + * Loads the translations already resolvable for the given file and locale + * from every registered namespace (framework, packages, and app). + * + * @return array + */ + private function findResolvedTranslations(string $langFileName, string $currentLocale): array + { + $translations = []; + + foreach (service('locator')->search("Language/{$currentLocale}/{$langFileName}.php", 'php', false) as $file) { + if (! is_file($file)) { + continue; + } + + $keys = require $file; + + if (is_array($keys)) { + $translations[] = $keys; + } + } + + if ($translations === []) { + return []; + } + + return array_replace_recursive(...$translations); + } + /** * @param SplFileInfo|string $file * diff --git a/tests/system/Commands/Translation/LocalizationFinderTest.php b/tests/system/Commands/Translation/LocalizationFinderTest.php index 3baa82290f1f..bfbd00d8bf83 100644 --- a/tests/system/Commands/Translation/LocalizationFinderTest.php +++ b/tests/system/Commands/Translation/LocalizationFinderTest.php @@ -30,6 +30,7 @@ final class LocalizationFinderTest extends CIUnitTestCase private static string $locale; private static string $languageTestPath; private string $originalLocale; + private ?string $tempSourceDir = null; /** * @var list @@ -57,6 +58,7 @@ protected function tearDown(): void parent::tearDown(); $this->clearGeneratedFiles(); + $this->clearSourceFixture(); Locale::setDefault($this->originalLocale); config(App::class)->supportedLocales = $this->originalSupportedLocales; } @@ -131,6 +133,39 @@ public function testShowBadTranslation(): void $this->assertStringContainsString($this->getActualTableWithBadKeys(), $this->getStreamFilterBuffer()); } + public function testShowNewSkipsKeysAlreadyTranslatedByFramework(): void + { + $this->makeLocaleDirectory(); + $this->makeSourceFixture( + 'ResolvedKeys', + "getStreamFilterBuffer(); + $this->assertStringNotContainsString('Errors.pageNotFound', $buffer); + $this->assertStringContainsString('Errors.thisKeyIsBrandNew', $buffer); + } + + public function testWriteSkipsKeysAlreadyTranslatedByFramework(): void + { + $this->makeLocaleDirectory(); + $this->makeSourceFixture( + 'ResolvedKeys', + "assertFileExists($generatedFile); + + $generatedKeys = require $generatedFile; + $this->assertArrayHasKey('thisKeyIsBrandNew', $generatedKeys); + $this->assertArrayNotHasKey('pageNotFound', $generatedKeys); + } + private function getActualTranslationOneKeys(): array { return [ @@ -261,6 +296,32 @@ private function makeLocaleDirectory(): void @mkdir(self::$languageTestPath . self::$locale, 0777, true); } + private function makeSourceFixture(string $dirName, string $contents): void + { + $this->tempSourceDir = SUPPORTPATH . 'Services' . DIRECTORY_SEPARATOR . $dirName; + @mkdir($this->tempSourceDir, 0777, true); + file_put_contents($this->tempSourceDir . DIRECTORY_SEPARATOR . 'Source.php', $contents); + } + + private function clearSourceFixture(): void + { + if ($this->tempSourceDir === null) { + return; + } + + $sourceFile = $this->tempSourceDir . DIRECTORY_SEPARATOR . 'Source.php'; + + if (is_file($sourceFile)) { + unlink($sourceFile); + } + + if (is_dir($this->tempSourceDir)) { + rmdir($this->tempSourceDir); + } + + $this->tempSourceDir = null; + } + private function clearGeneratedFiles(): void { if (is_file(self::$languageTestPath . self::$locale . '/TranslationOne.php')) { @@ -275,6 +336,10 @@ private function clearGeneratedFiles(): void unlink(self::$languageTestPath . self::$locale . '/Translation-Four.php'); } + if (is_file(self::$languageTestPath . self::$locale . '/Errors.php')) { + unlink(self::$languageTestPath . self::$locale . '/Errors.php'); + } + if (is_dir(self::$languageTestPath . '/test_locale_incorrect')) { rmdir(self::$languageTestPath . '/test_locale_incorrect'); } diff --git a/user_guide_src/source/changelogs/v4.7.4.rst b/user_guide_src/source/changelogs/v4.7.4.rst index 41ecf19e9b04..87029861aaa0 100644 --- a/user_guide_src/source/changelogs/v4.7.4.rst +++ b/user_guide_src/source/changelogs/v4.7.4.rst @@ -31,6 +31,7 @@ Bugs Fixed ********** - **API:** Fixed a bug in Transformers where the root request's ``fields`` and ``include`` query parameters leaked into nested transformers created inside ``include*()`` methods, causing incorrect field filtering, unexpected includes, or infinite recursion. +- **Commands:** Fixed a bug where ``spark lang:find`` treated translation keys already provided by the framework or another namespace (such as ``Errors.*`` in ``system/Language``) as new, listing them under ``--show-new`` and writing untranslated placeholders into ``app/Language`` that overrode the existing translations. - **Database:** Fixed a bug where ``updateBatch()`` could be called after Query Builder ``where()`` conditions, even though it's not supported. In this situation, now the ``DatabaseException`` is thrown. - **HTTP:** Fixed a bug where the User Agent library reported Safari's WebKit version instead of the browser version from the ``Version`` token. - **Model:** Fixed a bug in ``Model::objectToRawArray()`` where the ``$recursive`` parameter was ignored. diff --git a/user_guide_src/source/outgoing/localization.rst b/user_guide_src/source/outgoing/localization.rst index 4d9facce4bfa..c73cf9449d00 100644 --- a/user_guide_src/source/outgoing/localization.rst +++ b/user_guide_src/source/outgoing/localization.rst @@ -317,6 +317,10 @@ After the operation, you need to translate the language keys yourself. The command is able to recognize nested keys normally ``File.array.nested.text``. Previously saved keys do not change. +.. note:: Keys that ``lang()`` can already resolve (for example ``Errors.*`` + from **system/Language**) are treated as already translated, so they are + neither listed by ``--show-new`` nor written into **app/Language**. + .. code-block:: console php spark lang:find From 56bace9ac04b47e8962bfaf2c1ca4d8465dbd77c Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Sun, 14 Jun 2026 14:12:46 +0200 Subject: [PATCH 26/65] fix(Filters): check both keys and values in `InvalidChars` arrays (#10303) * fix(Filters): check both keys and values in InvalidChars arrays * test(Filters): use DataProvider for InvalidChars control characters test * style: fix PHPStan iterable return type in tests * chore: re-trigger CI * docs: update changelog for InvalidChars array keys fix --- system/Filters/InvalidChars.php | 16 +++++++- tests/system/Filters/InvalidCharsTest.php | 42 ++++++++++++++++++++- user_guide_src/source/changelogs/v4.7.4.rst | 1 + 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/system/Filters/InvalidChars.php b/system/Filters/InvalidChars.php index 1cd775153c0e..85e71c5d4f59 100644 --- a/system/Filters/InvalidChars.php +++ b/system/Filters/InvalidChars.php @@ -87,11 +87,18 @@ public function after(RequestInterface $request, ResponseInterface $response, $a * @param array|string $value * * @return array|string + * + * @throws SecurityException */ protected function checkEncoding($value) { if (is_array($value)) { - array_map($this->checkEncoding(...), $value); + foreach ($value as $key => $item) { + if (is_string($key)) { + $this->checkEncoding($key); + } + $this->checkEncoding($item); + } return $value; } @@ -113,7 +120,12 @@ protected function checkEncoding($value) protected function checkControl($value) { if (is_array($value)) { - array_map($this->checkControl(...), $value); + foreach ($value as $key => $item) { + if (is_string($key)) { + $this->checkControl($key); + } + $this->checkControl($item); + } return $value; } diff --git a/tests/system/Filters/InvalidCharsTest.php b/tests/system/Filters/InvalidCharsTest.php index 8aa935b14383..4cf112b6da91 100644 --- a/tests/system/Filters/InvalidCharsTest.php +++ b/tests/system/Filters/InvalidCharsTest.php @@ -103,13 +103,51 @@ public function testBeforeInvalidUTF8StringCausesException(): void $this->invalidChars->before($this->request); } - public function testBeforeInvalidControlCharCausesException(): void + public function testBeforeInvalidUTF8StringInArrayKeyCausesException(): void + { + $this->expectException(SecurityException::class); + $this->expectExceptionMessage('Invalid UTF-8 characters in post:'); + + $sjisString = mb_convert_encoding('SJISの文字列です。', 'SJIS'); + service('superglobals')->setPost('val', [ + $sjisString => 'valid string', + ]); + + $this->invalidChars->before($this->request); + } + + #[DataProvider('provideBeforeInvalidControlCharCausesException')] + public function testBeforeInvalidControlCharCausesException(string $invalidString): void + { + $this->expectException(SecurityException::class); + $this->expectExceptionMessage('Invalid Control characters in cookie:'); + + service('superglobals')->setCookie('val', $invalidString); + + $this->invalidChars->before($this->request); + } + + /** + * @return iterable> + */ + public static function provideBeforeInvalidControlCharCausesException(): iterable + { + yield 'null byte' => ["String with null char \0"]; + + yield 'backspace' => ["String with backspace \x08"]; + + yield 'escape' => ["String with escape \x1b"]; + } + + public function testBeforeInvalidControlCharInArrayKeyCausesException(): void { $this->expectException(SecurityException::class); $this->expectExceptionMessage('Invalid Control characters in cookie:'); $stringWithNullChar = "String contains null char and line break.\0\n"; - service('superglobals')->setCookie('val', $stringWithNullChar); + service('superglobals')->setCookie('val', [ + $stringWithNullChar => 'valid string', + ]); $this->invalidChars->before($this->request); } diff --git a/user_guide_src/source/changelogs/v4.7.4.rst b/user_guide_src/source/changelogs/v4.7.4.rst index 87029861aaa0..83d21af99f14 100644 --- a/user_guide_src/source/changelogs/v4.7.4.rst +++ b/user_guide_src/source/changelogs/v4.7.4.rst @@ -33,6 +33,7 @@ Bugs Fixed - **API:** Fixed a bug in Transformers where the root request's ``fields`` and ``include`` query parameters leaked into nested transformers created inside ``include*()`` methods, causing incorrect field filtering, unexpected includes, or infinite recursion. - **Commands:** Fixed a bug where ``spark lang:find`` treated translation keys already provided by the framework or another namespace (such as ``Errors.*`` in ``system/Language``) as new, listing them under ``--show-new`` and writing untranslated placeholders into ``app/Language`` that overrode the existing translations. - **Database:** Fixed a bug where ``updateBatch()`` could be called after Query Builder ``where()`` conditions, even though it's not supported. In this situation, now the ``DatabaseException`` is thrown. +- **Filters:** Fixed a bug in ``InvalidChars`` filter where invalid UTF-8 or control characters in array keys were not checked. - **HTTP:** Fixed a bug where the User Agent library reported Safari's WebKit version instead of the browser version from the ``Version`` token. - **Model:** Fixed a bug in ``Model::objectToRawArray()`` where the ``$recursive`` parameter was ignored. - **Testing:** Fixed a bug where using ``MockInputOutput`` within a test that also uses ``StreamFilterTrait`` tore down the trait's stream filters, so CLI output produced after the ``MockInputOutput`` interaction (such as in ``tearDown()``) was no longer captured and leaked to the console. From fc45198ea92c32e12924e142a65deb9d46a7387a Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Sun, 14 Jun 2026 14:14:23 +0200 Subject: [PATCH 27/65] fix: use dynamic lockMaxRetries limit in RedisHandler (#10295) * fix: use dynamic lockMaxRetries limit in RedisHandler * fix: read correct config property names in RedisHandler lockSession * fix: add lockMaxRetries test and remove stale PHPStan baseline entries * fix: add setLogger to RedisHandlerTest to prevent null logger error * Update system/Session/Handlers/RedisHandler.php Co-authored-by: Michal Sniatala * Fix PHPStan warnings for RedisHandler and update changelog * Update user_guide_src/source/changelogs/v4.7.4.rst Co-authored-by: Michal Sniatala * docs: alphabetize changelog entries and properties in v4.7.4.rst --------- Co-authored-by: Michal Sniatala --- system/Session/Handlers/RedisHandler.php | 8 ++--- .../Handlers/Database/RedisHandlerTest.php | 29 ++++++++++++++++++- user_guide_src/source/changelogs/v4.7.4.rst | 1 + utils/phpstan-baseline/property.notFound.neon | 12 +------- 4 files changed, 34 insertions(+), 16 deletions(-) diff --git a/system/Session/Handlers/RedisHandler.php b/system/Session/Handlers/RedisHandler.php index 4c8f78ba3894..54f628c4789b 100644 --- a/system/Session/Handlers/RedisHandler.php +++ b/system/Session/Handlers/RedisHandler.php @@ -98,8 +98,8 @@ public function __construct(SessionConfig $config, string $ipAddress) $this->keyPrefix .= $this->ipAddress . ':'; } - $this->lockRetryInterval = $config->lockWait ?? $this->lockRetryInterval; - $this->lockMaxRetries = $config->lockAttempts ?? $this->lockMaxRetries; + $this->lockRetryInterval = $config->lockRetryInterval ?? $this->lockRetryInterval; // @phpstan-ignore nullCoalesce.property + $this->lockMaxRetries = $config->lockMaxRetries ?? $this->lockMaxRetries; // @phpstan-ignore nullCoalesce.property } protected function setSavePath(): void @@ -386,10 +386,10 @@ protected function lockSession(string $sessionID): bool break; } while (++$attempt < $this->lockMaxRetries); - if ($attempt === 300) { + if ($attempt === $this->lockMaxRetries) { $this->logger->error( 'Session: Unable to obtain lock for ' . $this->keyPrefix . $sessionID - . ' after 300 attempts, aborting.', + . ' after ' . $this->lockMaxRetries . ' attempts, aborting.', ); return false; diff --git a/tests/system/Session/Handlers/Database/RedisHandlerTest.php b/tests/system/Session/Handlers/Database/RedisHandlerTest.php index 2e7821446fe3..637132669aec 100644 --- a/tests/system/Session/Handlers/Database/RedisHandlerTest.php +++ b/tests/system/Session/Handlers/Database/RedisHandlerTest.php @@ -15,6 +15,8 @@ use CodeIgniter\Session\Handlers\RedisHandler; use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\TestLogger; +use Config\Logger as LoggerConfig; use Config\Session as SessionConfig; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; @@ -54,7 +56,10 @@ protected function getInstance($options = []): RedisHandler $sessionConfig->{$key} = $value; } - return new RedisHandler($sessionConfig, $this->userIpAddress); + $handler = new RedisHandler($sessionConfig, $this->userIpAddress); + $handler->setLogger(new TestLogger(new LoggerConfig())); + + return $handler; } protected function tearDown(): void @@ -359,4 +364,26 @@ public function testResetPersistentConnections(): void $handler1->close(); $handler2->close(); } + + public function testLockMaxRetries(): void + { + $options = [ + 'lockRetryInterval' => 10_000, // 10ms + 'lockMaxRetries' => 3, + ]; + + $handler1 = $this->getInstance($options); + $handler1->open($this->sessionSavePath, $this->sessionName); + $handler1->read('lock_test_session'); // Acquires lock + + $handler2 = $this->getInstance($options); + $handler2->open($this->sessionSavePath, $this->sessionName); + + // Before the fix, this would incorrectly return true (since $attempt === 3 !== 300). + // With the fix, it should return false after 3 attempts. + $this->assertFalse($handler2->read('lock_test_session')); + + $handler1->close(); + $handler2->close(); + } } diff --git a/user_guide_src/source/changelogs/v4.7.4.rst b/user_guide_src/source/changelogs/v4.7.4.rst index 83d21af99f14..6944823bd776 100644 --- a/user_guide_src/source/changelogs/v4.7.4.rst +++ b/user_guide_src/source/changelogs/v4.7.4.rst @@ -36,6 +36,7 @@ Bugs Fixed - **Filters:** Fixed a bug in ``InvalidChars`` filter where invalid UTF-8 or control characters in array keys were not checked. - **HTTP:** Fixed a bug where the User Agent library reported Safari's WebKit version instead of the browser version from the ``Version`` token. - **Model:** Fixed a bug in ``Model::objectToRawArray()`` where the ``$recursive`` parameter was ignored. +- **Session:** Fixed a bug in ``RedisHandler`` where the configured ``$lockMaxRetries`` and ``$lockRetryInterval`` values were not respected when acquiring session locks. - **Testing:** Fixed a bug where using ``MockInputOutput`` within a test that also uses ``StreamFilterTrait`` tore down the trait's stream filters, so CLI output produced after the ``MockInputOutput`` interaction (such as in ``tearDown()``) was no longer captured and leaked to the console. See the repo's diff --git a/utils/phpstan-baseline/property.notFound.neon b/utils/phpstan-baseline/property.notFound.neon index 9e6fc64adb15..b64acebff15b 100644 --- a/utils/phpstan-baseline/property.notFound.neon +++ b/utils/phpstan-baseline/property.notFound.neon @@ -1,4 +1,4 @@ -# total 49 errors +# total 47 errors parameters: ignoreErrors: @@ -17,16 +17,6 @@ parameters: count: 14 path: ../../system/Database/SQLSRV/Forge.php - - - message: '#^Access to an undefined property Config\\Session\:\:\$lockAttempts\.$#' - count: 1 - path: ../../system/Session/Handlers/RedisHandler.php - - - - message: '#^Access to an undefined property Config\\Session\:\:\$lockWait\.$#' - count: 1 - path: ../../system/Session/Handlers/RedisHandler.php - - message: '#^Access to an undefined property CodeIgniter\\Database\\BaseConnection\:\:\$mysqli\.$#' count: 1 From 32eeb5d2cea44d887159799d71be7b8056b76112 Mon Sep 17 00:00:00 2001 From: Michael Martins Date: Sun, 14 Jun 2026 09:15:24 -0300 Subject: [PATCH 28/65] fix: preserve sub-namespace when generating Entity from Model (i.e., `--return entity`) (#10232) * Preserve sub-namespace when generating Entity from Model (--return=entity) * test: add test for preserving sub-namespace in entity generation * docs: add changelog entry for #10232 --- system/Commands/Generators/ModelGenerator.php | 17 +++++++----- .../Generators/ModelGeneratorTest.php | 27 +++++++++++++++++++ user_guide_src/source/changelogs/v4.7.4.rst | 1 + 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/system/Commands/Generators/ModelGenerator.php b/system/Commands/Generators/ModelGenerator.php index 5450bda79b4c..30b7422238fd 100644 --- a/system/Commands/Generators/ModelGenerator.php +++ b/system/Commands/Generators/ModelGenerator.php @@ -114,18 +114,23 @@ protected function prepare(string $class): string } if ($return === 'entity') { - $return = str_replace('Models', 'Entities', $class); + // Build the fully-qualified entity class from the model class so + // that the generated Entity keeps any sub-namespaces (eg. Admin). + $entityClass = str_replace('Models', 'Entities', $class); - if (preg_match('/^(\S+)Model$/i', $return, $match) === 1) { - $return = $match[1]; + if (preg_match('/^(\S+)Model$/i', $entityClass, $match) === 1) { + $entityClass = $match[1]; if ($this->getOption('suffix')) { - $return .= 'Entity'; + $entityClass .= 'Entity'; } } - $return = '\\' . trim($return, '\\') . '::class'; - $this->call('make:entity', array_merge([$baseClass], $this->params)); + // Call the entity generator with the fully-qualified class name so + // it ends up under the correct sub-namespace/folder (eg. Admin). + $this->call('make:entity', array_merge([trim($entityClass, '\\')], $this->params)); + + $return = '\\' . trim($entityClass, '\\') . '::class'; } else { $return = "'{$return}'"; } diff --git a/tests/system/Commands/Generators/ModelGeneratorTest.php b/tests/system/Commands/Generators/ModelGeneratorTest.php index 520bbbdc7a3b..8dab8b007f19 100644 --- a/tests/system/Commands/Generators/ModelGeneratorTest.php +++ b/tests/system/Commands/Generators/ModelGeneratorTest.php @@ -134,6 +134,33 @@ public function testGenerateModelWithOptionSuffix(): void rmdir(dirname($entity)); } + public function testGenerateModelWithSubNamespaceAndReturnEntity(): void + { + command('make:model admin/class --return entity'); + + $model = APPPATH . 'Models/Admin/Class.php'; + $entity = APPPATH . 'Entities/Admin/Class.php'; + + $this->assertFileExists($model); + $this->assertFileExists($entity); + + if (is_file($model)) { + unlink($model); + } + $modelDir = dirname($model); + if (is_dir($modelDir)) { + rmdir($modelDir); + } + + if (is_file($entity)) { + unlink($entity); + } + $entityDir = dirname($entity); + if (is_dir($entityDir)) { + rmdir($entityDir); + } + } + /** * @see https://github.com/codeigniter4/CodeIgniter4/issues/5050 */ diff --git a/user_guide_src/source/changelogs/v4.7.4.rst b/user_guide_src/source/changelogs/v4.7.4.rst index 6944823bd776..b0145d2d17af 100644 --- a/user_guide_src/source/changelogs/v4.7.4.rst +++ b/user_guide_src/source/changelogs/v4.7.4.rst @@ -31,6 +31,7 @@ Bugs Fixed ********** - **API:** Fixed a bug in Transformers where the root request's ``fields`` and ``include`` query parameters leaked into nested transformers created inside ``include*()`` methods, causing incorrect field filtering, unexpected includes, or infinite recursion. +- **Commands:** Fixed a bug where ``make:model --return entity`` did not preserve sub-namespaces when generating the related Entity class. - **Commands:** Fixed a bug where ``spark lang:find`` treated translation keys already provided by the framework or another namespace (such as ``Errors.*`` in ``system/Language``) as new, listing them under ``--show-new`` and writing untranslated placeholders into ``app/Language`` that overrode the existing translations. - **Database:** Fixed a bug where ``updateBatch()`` could be called after Query Builder ``where()`` conditions, even though it's not supported. In this situation, now the ``DatabaseException`` is thrown. - **Filters:** Fixed a bug in ``InvalidChars`` filter where invalid UTF-8 or control characters in array keys were not checked. From e793e58ffb056710eefca6fffe25a759f951a5e6 Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Mon, 15 Jun 2026 07:35:35 +0200 Subject: [PATCH 29/65] fix: `env()` TypeError for non-string `$_SERVER` values + `esc()` fixes (#10305) * fix: env() TypeError for non-string $_SERVER values + esc() fixes - env(): guard non-string values (int argc, array argv in CLI) before strtolower() to prevent TypeError under declare(strict_types=1) - esc(): propagate $encoding in recursive array calls (was ignored before), add early return after array processing, replace single static $escaper with static $escapers[] cache keyed by encoding - tests: data-provider test for env() non-string types, three tests for esc() foreach reference leak * Apply suggestion from @michalsn Co-authored-by: Michal Sniatala * Apply suggestion from @michalsn Co-authored-by: Michal Sniatala * Update system/Common.php Co-authored-by: Michal Sniatala * style: cs-fix * test: add tests for esc() encoding changes and update changelog * Update tests/system/CommonFunctionsTest.php Co-authored-by: Michal Sniatala * docs: move env/esc fix changelog entry to v4.7.4.rst * docs: split Common changelog entry --------- Co-authored-by: Michal Sniatala --- system/Common.php | 22 +++++---- tests/system/CommonFunctionsTest.php | 53 +++++++++++++++++++++ user_guide_src/source/changelogs/v4.7.4.rst | 2 + 3 files changed, 69 insertions(+), 8 deletions(-) diff --git a/system/Common.php b/system/Common.php index ea0c476423a0..f3f20a7a0db5 100644 --- a/system/Common.php +++ b/system/Common.php @@ -416,6 +416,12 @@ function env(string $key, $default = null) return $default; } + // Non-string values (e.g. $_SERVER['argc'] is int, $_SERVER['argv'] is array in CLI) + // must be returned as-is to avoid TypeError from strtolower(). + if (! is_string($value)) { + return $value; + } + // Handle any boolean values return match (strtolower($value)) { 'true' => true, @@ -459,8 +465,10 @@ function esc($data, string $context = 'html', ?string $encoding = null) if (is_array($data)) { foreach ($data as &$value) { - $value = esc($value, $context); + $value = esc($value, $context, $encoding); } + + return $data; } if (is_string($data)) { @@ -470,16 +478,14 @@ function esc($data, string $context = 'html', ?string $encoding = null) $method = $context === 'attr' ? 'escapeHtmlAttr' : 'escape' . ucfirst($context); - static $escaper; - if (! $escaper) { - $escaper = new Escaper($encoding); - } + static $escapers = []; + $cacheKey = strtolower($encoding ?? 'utf-8'); - if ($encoding !== null && $escaper->getEncoding() !== $encoding) { - $escaper = new Escaper($encoding); + if (! isset($escapers[$cacheKey])) { + $escapers[$cacheKey] = new Escaper($encoding); } - $data = $escaper->{$method}($data); + $data = $escapers[$cacheKey]->{$method}($data); } return $data; diff --git a/tests/system/CommonFunctionsTest.php b/tests/system/CommonFunctionsTest.php index 7f79b07381fd..30f99d79a515 100644 --- a/tests/system/CommonFunctionsTest.php +++ b/tests/system/CommonFunctionsTest.php @@ -42,6 +42,7 @@ use Config\Services; use Config\Session as SessionConfig; use Exception; +use InvalidArgumentException; use Kint; use PHPUnit\Framework\Attributes\BackupGlobals; use PHPUnit\Framework\Attributes\DataProvider; @@ -131,6 +132,42 @@ public function testEnvBooleans(): void $this->assertNull(env('p4')); } + #[DataProvider('provideEnvReturnsCorrectTypesWithoutTypeError')] + public function testEnvReturnsCorrectTypesWithoutTypeError(string $source, mixed $value): void + { + $key = 'ci_test_var'; + + if ($source === 'SERVER' || $source === 'BOTH') { + service('superglobals')->setServer($key, $value); + } + + if ($source === 'ENV' || $source === 'BOTH') { + $_ENV[$key] = $value; + } + + $this->assertSame($value, env($key)); + } + + /** + * @return iterable + */ + public static function provideEnvReturnsCorrectTypesWithoutTypeError(): iterable + { + yield 'integer from SERVER' => ['SERVER', 2]; + + yield 'array from SERVER' => ['SERVER', ['spark', 'migrate']]; + + yield 'int 1 is not true' => ['SERVER', 1]; + + yield 'int 0 is not false' => ['SERVER', 0]; + + yield 'float from SERVER' => ['SERVER', 3.14]; + + yield 'integer from ENV' => ['ENV', 42]; + + yield 'CLI simulation BOTH' => ['BOTH', 3]; + } + private function createRouteCollection(): RouteCollection { return new RouteCollection(Services::locator(), new Modules(), new Routing()); @@ -276,6 +313,22 @@ public function testEscapeRecursiveArrayRaw(): void $this->assertSame($data, esc($data, 'raw')); } + public function testEscapeArrayPropagatesEncoding(): void + { + $this->expectException(InvalidArgumentException::class); + // If encoding is not propagated, it would not instantiate the Escaper with the invalid encoding and wouldn't throw. + esc(['test'], 'html', 'invalid-encoding'); + } + + public function testEscapeWithChangingArrayEncoding(): void + { + $data = [hex2bin('E9')]; + + $this->assertSame(['é'], esc($data, 'attr', 'iso-8859-1')); + $this->assertSame(['й'], esc($data, 'attr', 'windows-1251')); + $this->assertSame(['é'], esc($data, 'attr', 'iso-8859-1')); + } + #[PreserveGlobalState(false)] #[RunInSeparateProcess] #[WithoutErrorHandler] diff --git a/user_guide_src/source/changelogs/v4.7.4.rst b/user_guide_src/source/changelogs/v4.7.4.rst index b0145d2d17af..1373f9fa1720 100644 --- a/user_guide_src/source/changelogs/v4.7.4.rst +++ b/user_guide_src/source/changelogs/v4.7.4.rst @@ -32,6 +32,8 @@ Bugs Fixed - **API:** Fixed a bug in Transformers where the root request's ``fields`` and ``include`` query parameters leaked into nested transformers created inside ``include*()`` methods, causing incorrect field filtering, unexpected includes, or infinite recursion. - **Commands:** Fixed a bug where ``make:model --return entity`` did not preserve sub-namespaces when generating the related Entity class. +- **Common:** Fixed a bug in ``env()`` where a ``TypeError`` could be thrown when non-string values were passed. +- **Common:** Fixed ``esc()`` to propagate encoding correctly and prevent reference leaks. - **Commands:** Fixed a bug where ``spark lang:find`` treated translation keys already provided by the framework or another namespace (such as ``Errors.*`` in ``system/Language``) as new, listing them under ``--show-new`` and writing untranslated placeholders into ``app/Language`` that overrode the existing translations. - **Database:** Fixed a bug where ``updateBatch()`` could be called after Query Builder ``where()`` conditions, even though it's not supported. In this situation, now the ``DatabaseException`` is thrown. - **Filters:** Fixed a bug in ``InvalidChars`` filter where invalid UTF-8 or control characters in array keys were not checked. From b08160c4c490fe3291d106233f0c374c7b371fb3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 01:18:49 +0800 Subject: [PATCH 30/65] chore(deps-dev): update boundwize/structarmed requirement (#10311) Updates the requirements on [boundwize/structarmed](https://github.com/boundwize/structarmed) to permit the latest version. Updates `boundwize/structarmed` to 0.13.4 - [Release notes](https://github.com/boundwize/structarmed/releases) - [Commits](https://github.com/boundwize/structarmed/compare/0.12.6...0.13.4) --- updated-dependencies: - dependency-name: boundwize/structarmed dependency-version: 0.13.4 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 6fc90fbb3340..3d257784b2f0 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "psr/log": "^3.0" }, "require-dev": { - "boundwize/structarmed": "0.12.6", + "boundwize/structarmed": "0.13.4", "codeigniter/phpstan-codeigniter": "^1.5", "fakerphp/faker": "^1.24", "kint-php/kint": "^6.1", From b61bbb3a88f40562cecbe005e260ff15534a9b02 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 22:31:42 +0200 Subject: [PATCH 31/65] chore(deps): bump actions/checkout in / (#10324) Bumps [actions/checkout](https://github.com/actions/checkout) in `/` from 6.0.3 to 7.0.0. Updates `actions/checkout` from 6.0.3 to 7.0.0 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/df4cb1c069e1874edd31b4311f1884172cec0e10...9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major 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 | 4 ++-- .github/workflows/deploy-distributables.yml | 14 +++++++------- .github/workflows/deploy-userguide-latest.yml | 2 +- .github/workflows/label-add-conflict-all-pr.yml | 2 +- .github/workflows/label-pr.yml | 2 +- .github/workflows/label-signing.yml | 2 +- .github/workflows/reusable-coveralls.yml | 2 +- .github/workflows/reusable-phpunit-test.yml | 2 +- .../reusable-serviceless-phpunit-test.yml | 2 +- .github/workflows/test-autoreview.yml | 2 +- .github/workflows/test-coding-standards.yml | 2 +- .github/workflows/test-file-permissions.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-scss.yml | 2 +- .github/workflows/test-structarmed.yml | 2 +- .github/workflows/test-userguide.yml | 2 +- 19 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.github/workflows/deploy-apidocs.yml b/.github/workflows/deploy-apidocs.yml index 01cd0c83d28a..3d24470ed846 100644 --- a/.github/workflows/deploy-apidocs.yml +++ b/.github/workflows/deploy-apidocs.yml @@ -30,13 +30,13 @@ jobs: git config --global user.name "${GITHUB_ACTOR}" - name: Checkout source - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: path: source persist-credentials: false - name: Checkout target - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: repository: codeigniter4/api token: ${{ secrets.ACCESS_TOKEN }} diff --git a/.github/workflows/deploy-distributables.yml b/.github/workflows/deploy-distributables.yml index 7d7c9af27071..d74cf7e54d05 100644 --- a/.github/workflows/deploy-distributables.yml +++ b/.github/workflows/deploy-distributables.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 # fetch all tags persist-credentials: false @@ -50,13 +50,13 @@ jobs: git config --global user.name "${GITHUB_ACTOR}" - name: Checkout source - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: path: source persist-credentials: false - name: Checkout target - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: repository: codeigniter4/framework token: ${{ secrets.ACCESS_TOKEN }} @@ -104,13 +104,13 @@ jobs: git config --global user.name "${GITHUB_ACTOR}" - name: Checkout source - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: path: source persist-credentials: false - name: Checkout target - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: repository: codeigniter4/appstarter token: ${{ secrets.ACCESS_TOKEN }} @@ -158,13 +158,13 @@ jobs: git config --global user.name "${GITHUB_ACTOR}" - name: Checkout source - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: path: source persist-credentials: false - name: Checkout target - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: repository: codeigniter4/userguide token: ${{ secrets.ACCESS_TOKEN }} diff --git a/.github/workflows/deploy-userguide-latest.yml b/.github/workflows/deploy-userguide-latest.yml index 92c7706eaa6a..a56d4b48ca94 100644 --- a/.github/workflows/deploy-userguide-latest.yml +++ b/.github/workflows/deploy-userguide-latest.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false diff --git a/.github/workflows/label-add-conflict-all-pr.yml b/.github/workflows/label-add-conflict-all-pr.yml index fb4cad824f7e..774012da1f32 100644 --- a/.github/workflows/label-add-conflict-all-pr.yml +++ b/.github/workflows/label-add-conflict-all-pr.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Get PR List id: PR-list diff --git a/.github/workflows/label-pr.yml b/.github/workflows/label-pr.yml index d6c402ebc5e2..2e2659c6963a 100644 --- a/.github/workflows/label-pr.yml +++ b/.github/workflows/label-pr.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false diff --git a/.github/workflows/label-signing.yml b/.github/workflows/label-signing.yml index 617f459bd7df..80c321c5b117 100644 --- a/.github/workflows/label-signing.yml +++ b/.github/workflows/label-signing.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Check signed commits in PR uses: 1Password/check-signed-commits-action@ed2885f3ed2577a4f5d3c3fe895432a557d23d52 # v1.2.0 diff --git a/.github/workflows/reusable-coveralls.yml b/.github/workflows/reusable-coveralls.yml index 4786f6ee433c..9acb9e374910 100644 --- a/.github/workflows/reusable-coveralls.yml +++ b/.github/workflows/reusable-coveralls.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false diff --git a/.github/workflows/reusable-phpunit-test.yml b/.github/workflows/reusable-phpunit-test.yml index 0fa024b71c61..0ff6e04dc7f9 100644 --- a/.github/workflows/reusable-phpunit-test.yml +++ b/.github/workflows/reusable-phpunit-test.yml @@ -170,7 +170,7 @@ jobs: sudo apt-get install -y imagemagick libmagickwand-dev ghostscript poppler-data libjbig2dec0:amd64 libopenjp2-7:amd64 - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false diff --git a/.github/workflows/reusable-serviceless-phpunit-test.yml b/.github/workflows/reusable-serviceless-phpunit-test.yml index 04333f91ef6f..7bb45e2e74db 100644 --- a/.github/workflows/reusable-serviceless-phpunit-test.yml +++ b/.github/workflows/reusable-serviceless-phpunit-test.yml @@ -66,7 +66,7 @@ jobs: sudo apt-get install -y imagemagick libmagickwand-dev ghostscript poppler-data libjbig2dec0:amd64 libopenjp2-7:amd64 - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false fetch-depth: 0 diff --git a/.github/workflows/test-autoreview.yml b/.github/workflows/test-autoreview.yml index ff816aaeb519..fd7c83248b2e 100644 --- a/.github/workflows/test-autoreview.yml +++ b/.github/workflows/test-autoreview.yml @@ -36,7 +36,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Setup PHP uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 diff --git a/.github/workflows/test-coding-standards.yml b/.github/workflows/test-coding-standards.yml index 7bbe338c37c7..9b91c83a809f 100644 --- a/.github/workflows/test-coding-standards.yml +++ b/.github/workflows/test-coding-standards.yml @@ -35,7 +35,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Setup PHP uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 diff --git a/.github/workflows/test-file-permissions.yml b/.github/workflows/test-file-permissions.yml index 33e13e5859b5..7116f4620869 100644 --- a/.github/workflows/test-file-permissions.yml +++ b/.github/workflows/test-file-permissions.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Detect unnecessary execution permissions run: php utils/check_permission_x.php diff --git a/.github/workflows/test-phpstan.yml b/.github/workflows/test-phpstan.yml index 6c1461cb3318..318f29307add 100644 --- a/.github/workflows/test-phpstan.yml +++ b/.github/workflows/test-phpstan.yml @@ -46,7 +46,7 @@ jobs: fail-fast: false steps: - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Setup PHP uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 diff --git a/.github/workflows/test-psalm.yml b/.github/workflows/test-psalm.yml index e417d3f99746..01b6d1f701f8 100644 --- a/.github/workflows/test-psalm.yml +++ b/.github/workflows/test-psalm.yml @@ -36,7 +36,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false diff --git a/.github/workflows/test-random-execution.yml b/.github/workflows/test-random-execution.yml index 9b8d64160e47..57841b4d19d1 100644 --- a/.github/workflows/test-random-execution.yml +++ b/.github/workflows/test-random-execution.yml @@ -168,7 +168,7 @@ jobs: -Q "CREATE DATABASE test COLLATE Latin1_General_100_CS_AS_SC_UTF8" - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false fetch-depth: 0 diff --git a/.github/workflows/test-rector.yml b/.github/workflows/test-rector.yml index 25079edbd2b2..d51d36fcc8ba 100644 --- a/.github/workflows/test-rector.yml +++ b/.github/workflows/test-rector.yml @@ -53,7 +53,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Setup PHP uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 diff --git a/.github/workflows/test-scss.yml b/.github/workflows/test-scss.yml index 36a08dc089a7..3fcce79de33c 100644 --- a/.github/workflows/test-scss.yml +++ b/.github/workflows/test-scss.yml @@ -33,7 +33,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Setup Node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 diff --git a/.github/workflows/test-structarmed.yml b/.github/workflows/test-structarmed.yml index 905e26ab4f31..24e39b73cb91 100644 --- a/.github/workflows/test-structarmed.yml +++ b/.github/workflows/test-structarmed.yml @@ -51,7 +51,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Setup PHP uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 diff --git a/.github/workflows/test-userguide.yml b/.github/workflows/test-userguide.yml index fb6b52fdb5a4..211ac47ab481 100644 --- a/.github/workflows/test-userguide.yml +++ b/.github/workflows/test-userguide.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Setup Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 From 5e3ab98d22cb834cc2fac8ad06615f48872dcdf4 Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Fri, 19 Jun 2026 07:58:39 +0200 Subject: [PATCH 32/65] docs: note about user-controlled ImageMagick save targets (#10317) --- user_guide_src/source/libraries/images.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/libraries/images.rst b/user_guide_src/source/libraries/images.rst index 8f039c9561ec..4696b0d126d4 100644 --- a/user_guide_src/source/libraries/images.rst +++ b/user_guide_src/source/libraries/images.rst @@ -91,6 +91,10 @@ only applies to JPEG and WebP images, will be ignored otherwise: .. note:: The parameter ``$quality`` for WebP can be used since v4.4.0. +.. note:: When using ImageMagick, the target path passed to ``save()`` determines the output format by its extension. + ImageMagick may also interpret explicit format prefixes like ``jpg:filename.png``. If the target path is based on user + input, validate it against the formats your application allows, including any format prefix or extension. + .. literalinclude:: images/005.php .. note:: Higher quality will result in larger file sizes. See also https://www.php.net/manual/en/function.imagejpeg.php @@ -153,8 +157,7 @@ The ``convert()`` method changes the library's internal indicator for the desire .. literalinclude:: images/009.php -.. note:: ImageMagick already saves files in the type - indicated by their extension, ignoring ``$imageType``. +.. note:: ImageMagick already saves files in the type indicated by their extension, ignoring ``$imageType``. Fitting Images ============== From 7ae2c3b1433001db2577c4ef39db315d6693e91e Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Fri, 19 Jun 2026 22:22:58 +0200 Subject: [PATCH 33/65] refactor: narrow Image original dimension types to int (#10326) --- system/Images/Image.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/Images/Image.php b/system/Images/Image.php index 55754e339535..0634405c6ec0 100644 --- a/system/Images/Image.php +++ b/system/Images/Image.php @@ -26,14 +26,14 @@ class Image extends File /** * The original image width in pixels. * - * @var float|int + * @var int */ public $origWidth; /** * The original image height in pixels. * - * @var float|int + * @var int */ public $origHeight; From 1bcfb27e319ffc73c623fdda5d632a31a0357781 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 21 Jun 2026 07:43:03 +0700 Subject: [PATCH 34/65] chore(deps-dev): bump the composer-dependencies group across 1 directory with 2 updates (#10318) * chore(deps-dev): bump the composer-dependencies group across 1 directory with 2 updates Updates the requirements on [codeigniter/phpstan-codeigniter](https://github.com/CodeIgniter/phpstan-codeigniter) and [rector/rector](https://github.com/rectorphp/rector) to permit the latest version. Updates `codeigniter/phpstan-codeigniter` to 2.1.0 - [Release notes](https://github.com/CodeIgniter/phpstan-codeigniter/releases) - [Changelog](https://github.com/CodeIgniter/phpstan-codeigniter/blob/2.x/CHANGELOG.md) - [Commits](https://github.com/CodeIgniter/phpstan-codeigniter/compare/v1.5.0...v2.1.0) Updates `rector/rector` to 2.4.6 - [Release notes](https://github.com/rectorphp/rector/releases) - [Commits](https://github.com/rectorphp/rector/compare/2.4.5...2.4.6) --- updated-dependencies: - dependency-name: codeigniter/phpstan-codeigniter dependency-version: 2.1.0 dependency-type: direct:development dependency-group: composer-dependencies - dependency-name: rector/rector dependency-version: 2.4.6 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] * re-run rector * eof * set schemaCacheDirectory * use phpstan.neon.dist on last * regenerate phpstan baseline * regenerate phpstan baseline * fix phpstan:check --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Abdul Malik Ikhsan --- composer.json | 4 +- phpstan.neon.dist | 1 + rector.php | 2 +- system/Filters/Filters.php | 2 +- tests/system/CommonSingleServiceTest.php | 8 +- tests/system/Config/DotEnvTest.php | 4 +- tests/system/Entity/EntityTest.php | 4 +- .../system/Models/DataConverterModelTest.php | 22 +-- tests/system/Models/TimestampModelTest.php | 16 +- utils/phpstan-baseline/argument.type.neon | 12 +- .../codeigniter.getReassignArray.neon | 18 --- .../codeigniter.superglobalAccess.neon | 123 --------------- .../codeigniter.superglobalAccessAssign.neon | 28 ---- .../codeigniter.superglobalsGlobalAssign.neon | 83 ++++++++++ .../codeigniter.superglobalsOffsetAccess.neon | 148 ++++++++++++++++++ .../codeigniter.superglobalsOffsetAssign.neon | 28 ++++ .../codeigniter.superglobalsOffsetUnset.neon | 33 ++++ utils/phpstan-baseline/loader.neon | 9 +- .../phpstan-baseline/property.nonObject.neon | 22 +-- 19 files changed, 341 insertions(+), 226 deletions(-) delete mode 100644 utils/phpstan-baseline/codeigniter.getReassignArray.neon delete mode 100644 utils/phpstan-baseline/codeigniter.superglobalAccess.neon delete mode 100644 utils/phpstan-baseline/codeigniter.superglobalAccessAssign.neon create mode 100644 utils/phpstan-baseline/codeigniter.superglobalsGlobalAssign.neon create mode 100644 utils/phpstan-baseline/codeigniter.superglobalsOffsetAccess.neon create mode 100644 utils/phpstan-baseline/codeigniter.superglobalsOffsetAssign.neon create mode 100644 utils/phpstan-baseline/codeigniter.superglobalsOffsetUnset.neon diff --git a/composer.json b/composer.json index 3d257784b2f0..6f15a3622b7a 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ }, "require-dev": { "boundwize/structarmed": "0.13.4", - "codeigniter/phpstan-codeigniter": "^1.5", + "codeigniter/phpstan-codeigniter": "^2.1", "fakerphp/faker": "^1.24", "kint-php/kint": "^6.1", "mikey179/vfsstream": "^1.6.12", @@ -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.4.5", + "rector/rector": "2.4.6", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { diff --git a/phpstan.neon.dist b/phpstan.neon.dist index a508f84c696a..f90fcc94ee33 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -47,5 +47,6 @@ parameters: additionalModelNamespaces: - Tests\Support\Models checkArgumentTypeOfModel: false + schemaCacheDirectory: %currentWorkingDirectory%/build/phpstan shipmonkBaselinePerIdentifier: directory: %currentWorkingDirectory% diff --git a/rector.php b/rector.php index eeede43de2a7..4552a898aee3 100644 --- a/rector.php +++ b/rector.php @@ -77,10 +77,10 @@ __DIR__ . '/phpstan-bootstrap.php', ]) ->withPHPStanConfigs([ - __DIR__ . '/phpstan.neon.dist', __DIR__ . '/vendor/codeigniter/phpstan-codeigniter/extension.neon', __DIR__ . '/vendor/phpstan/phpstan-strict-rules/rules.neon', __DIR__ . '/vendor/shipmonk/phpstan-baseline-per-identifier/extension.neon', + __DIR__ . '/phpstan.neon.dist', ]) // is there a file you need to skip? ->withSkip([ diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index 5ec5fc0fa074..bf5419eab560 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -375,7 +375,7 @@ public function getRequiredFilters(string $position = 'before'): array { // For backward compatibility. For users who do not update Config\Filters. if (! isset($this->config->required[$position])) { - $baseConfig = config(BaseFiltersConfig::class); // @phpstan-ignore-line + $baseConfig = config(BaseFiltersConfig::class); $filters = $baseConfig->required[$position]; $aliases = $baseConfig->aliases; } else { diff --git a/tests/system/CommonSingleServiceTest.php b/tests/system/CommonSingleServiceTest.php index e423d1feaee0..829f0f6e2d96 100644 --- a/tests/system/CommonSingleServiceTest.php +++ b/tests/system/CommonSingleServiceTest.php @@ -34,8 +34,8 @@ public function testSingleServiceWithNoParamsSupplied(string $service): void { Services::injectMock('security', new MockSecurity(new SecurityConfig())); - $service1 = single_service($service); // @phpstan-ignore codeigniter.unknownServiceMethod - $service2 = single_service($service); // @phpstan-ignore codeigniter.unknownServiceMethod + $service1 = single_service($service); + $service2 = single_service($service); $this->assertNotNull($service1); @@ -62,8 +62,8 @@ public function testSingleServiceWithAtLeastOneParamSupplied(string $service): v $params[] = $method->getNumberOfParameters() === 1 ? true : $method->getParameters()[0]->getDefaultValue(); - $service1 = single_service($service, ...$params); // @phpstan-ignore codeigniter.unknownServiceMethod - $service2 = single_service($service, ...$params); // @phpstan-ignore codeigniter.unknownServiceMethod + $service1 = single_service($service, ...$params); + $service2 = single_service($service, ...$params); $this->assertNotNull($service1); diff --git a/tests/system/Config/DotEnvTest.php b/tests/system/Config/DotEnvTest.php index 505f4bedcbcb..67a0e05fd18b 100644 --- a/tests/system/Config/DotEnvTest.php +++ b/tests/system/Config/DotEnvTest.php @@ -93,7 +93,7 @@ public static function provideLoadsVars(): iterable public function testLoadsHex2Bin(): void { putenv('encryption.key'); - unset($_ENV['encryption.key'], $_SERVER['encryption.key']); // @phpstan-ignore codeigniter.superglobalAccess + unset($_ENV['encryption.key'], $_SERVER['encryption.key']); $dotenv = new DotEnv($this->fixturesFolder, 'encryption.env'); $dotenv->load(); @@ -108,7 +108,7 @@ public function testLoadsHex2Bin(): void public function testLoadsBase64(): void { putenv('encryption.key'); - unset($_ENV['encryption.key'], $_SERVER['encryption.key']); // @phpstan-ignore codeigniter.superglobalAccess + unset($_ENV['encryption.key'], $_SERVER['encryption.key']); $dotenv = new DotEnv($this->fixturesFolder, 'base64encryption.env'); $dotenv->load(); diff --git a/tests/system/Entity/EntityTest.php b/tests/system/Entity/EntityTest.php index 52d1de2f5a8b..5233a28deeaf 100644 --- a/tests/system/Entity/EntityTest.php +++ b/tests/system/Entity/EntityTest.php @@ -906,7 +906,7 @@ public function testCastEnumNullable(): void $entity->status = null; - $this->assertNull($entity->status); + $this->assertNotInstanceOf(StatusEnum::class, $entity->status); $entity->status = 'pending'; @@ -1573,7 +1573,7 @@ public function testDataCasterInitEmptyCasts(): void $getDataCaster = $this->getPrivateMethodInvoker($entity, 'dataCaster'); - $this->assertNull($getDataCaster()); + $this->assertNotInstanceOf(DataCaster::class, $getDataCaster()); $this->assertNull($this->getPrivateProperty($entity, 'dataCaster')); $this->assertSame('12345', $entity->first); diff --git a/tests/system/Models/DataConverterModelTest.php b/tests/system/Models/DataConverterModelTest.php index 7d647790d62b..eb9a36c4668d 100644 --- a/tests/system/Models/DataConverterModelTest.php +++ b/tests/system/Models/DataConverterModelTest.php @@ -36,7 +36,7 @@ public function testFindAsArray(): void $user = $this->model->find($id); - $this->assertIsInt($user['id']); // @phpstan-ignore offsetAccess.notFound + $this->assertIsInt($user['id']); $this->assertInstanceOf(Time::class, $user['created_at']); $this->assertSame('John Smith', $user['name']); // `name` is cast by custom CastBase64 handler. @@ -128,9 +128,9 @@ public function testFindAllAsArray(): void $users = $this->model->findAll(); - $this->assertIsInt($users[0]['id']); // @phpstan-ignore offsetAccess.notFound + $this->assertIsInt($users[0]['id']); $this->assertInstanceOf(Time::class, $users[0]['created_at']); - $this->assertIsInt($users[1]['id']); // @phpstan-ignore offsetAccess.notFound + $this->assertIsInt($users[1]['id']); $this->assertInstanceOf(Time::class, $users[1]['created_at']); } @@ -208,7 +208,7 @@ public function testFirstAsArray(): void $user = $this->model->first(); - $this->assertIsInt($user['id']); // @phpstan-ignore offsetAccess.notFound + $this->assertIsInt($user['id']); $this->assertInstanceOf(Time::class, $user['created_at']); } @@ -265,7 +265,7 @@ public function testInsertArray(): void $user = $this->model->find($id); - $this->assertSame(['joe@example.com'], $user['email']); // @phpstan-ignore offsetAccess.notFound + $this->assertSame(['joe@example.com'], $user['email']); } public function testInsertObject(): void @@ -281,7 +281,7 @@ public function testInsertObject(): void $user = $this->model->find($id); - $this->assertSame(['joe@example.com'], $user['email']); // @phpstan-ignore offsetAccess.notFound + $this->assertSame(['joe@example.com'], $user['email']); } public function testUpdateArray(): void @@ -290,14 +290,14 @@ public function testUpdateArray(): void $user = $this->model->find($id); $user['email'][] = 'private@example.org'; - $this->model->update($user['id'], $user); // @phpstan-ignore offsetAccess.notFound + $this->model->update($user['id'], $user); $user = $this->model->find($id); $this->assertSame([ 'john@example.com', 'private@example.org', - ], $user['email']); // @phpstan-ignore offsetAccess.notFound + ], $user['email']); } public function testUpdateObject(): void @@ -313,7 +313,7 @@ public function testUpdateObject(): void $this->assertSame([ 'john@example.com', 'private@example.org', - ], $user['email']); // @phpstan-ignore offsetAccess.notFound + ], $user['email']); } public function testUpdateCustomObject(): void @@ -365,7 +365,7 @@ public function testSaveArray(): void $this->assertSame([ 'john@example.com', 'private@example.org', - ], $user['email']); // @phpstan-ignore offsetAccess.notFound + ], $user['email']); } public function testSaveObject(): void @@ -381,7 +381,7 @@ public function testSaveObject(): void $this->assertSame([ 'john@example.com', 'private@example.org', - ], $user['email']); // @phpstan-ignore offsetAccess.notFound + ], $user['email']); } public function testSaveCustomObject(): void diff --git a/tests/system/Models/TimestampModelTest.php b/tests/system/Models/TimestampModelTest.php index 9af7cd3b9547..e2b5feb2da14 100644 --- a/tests/system/Models/TimestampModelTest.php +++ b/tests/system/Models/TimestampModelTest.php @@ -98,7 +98,7 @@ public function testDoNotAllowDatesInsertArrayWithoutDatesSetsTimestamp(): void $expected .= '.000'; } - $this->assertSame($expected, $user['created_at']); // @phpstan-ignore offsetAccess.notFound + $this->assertSame($expected, $user['created_at']); $this->assertSame($expected, $user['updated_at']); } @@ -123,7 +123,7 @@ public function testDoNotAllowDatesInsertArrayWithDatesSetsTimestamp(): void $expected .= '.000'; } - $this->assertSame($expected, $user['created_at']); // @phpstan-ignore offsetAccess.notFound + $this->assertSame($expected, $user['created_at']); $this->assertSame($expected, $user['updated_at']); } @@ -143,7 +143,7 @@ public function testDoNotAllowDatesUpdateArrayUpdatesUpdatedAt(): void $user = $this->model->find($id); $user['country'] = 'CA'; - $this->model->update($user['id'], $user); // @phpstan-ignore offsetAccess.notFound + $this->model->update($user['id'], $user); $user = $this->model->find($id); @@ -153,7 +153,7 @@ public function testDoNotAllowDatesUpdateArrayUpdatesUpdatedAt(): void $expected .= '.000'; } - $this->assertSame($expected, $user['created_at']); // @phpstan-ignore offsetAccess.notFound + $this->assertSame($expected, $user['created_at']); $this->assertSame($expected, $user['updated_at']); } @@ -208,7 +208,7 @@ public function testAllowDatesInsertArrayWithoutDatesSetsTimestamp(): void $expected .= '.000'; } - $this->assertSame($expected, $user['created_at']); // @phpstan-ignore offsetAccess.notFound + $this->assertSame($expected, $user['created_at']); $this->assertSame($expected, $user['updated_at']); } @@ -237,7 +237,7 @@ public function testAllowDatesInsertArrayWithDatesSetsTimestamp(): void $expected .= '.000'; } - $this->assertSame($expected, $user['created_at']); // @phpstan-ignore offsetAccess.notFound + $this->assertSame($expected, $user['created_at']); $this->assertSame($expected, $user['updated_at']); } @@ -261,7 +261,7 @@ public function testAllowDatesUpdateArrayUpdatesUpdatedAt(): void $user = $this->model->find($id); $user['country'] = 'CA'; - $this->model->update($user['id'], $user); // @phpstan-ignore offsetAccess.notFound + $this->model->update($user['id'], $user); $user = $this->model->find($id); @@ -271,7 +271,7 @@ public function testAllowDatesUpdateArrayUpdatesUpdatedAt(): void $expected .= '.000'; } - $this->assertSame($expected, $user['created_at']); // @phpstan-ignore offsetAccess.notFound + $this->assertSame($expected, $user['created_at']); $this->assertSame($expected, $user['updated_at']); } diff --git a/utils/phpstan-baseline/argument.type.neon b/utils/phpstan-baseline/argument.type.neon index 9697915545a3..65d03b5b278d 100644 --- a/utils/phpstan-baseline/argument.type.neon +++ b/utils/phpstan-baseline/argument.type.neon @@ -1,4 +1,4 @@ -# total 78 errors +# total 76 errors parameters: ignoreErrors: @@ -177,21 +177,11 @@ parameters: count: 1 path: ../../tests/system/Log/Handlers/ChromeLoggerHandlerTest.php - - - message: '#^Parameter \#1 \$row of method CodeIgniter\\BaseModel\:\:save\(\) expects array\\|object, array\\> given\.$#' - count: 1 - path: ../../tests/system/Models/DataConverterModelTest.php - - message: '#^Parameter \#1 \$row of method CodeIgniter\\Model\:\:insert\(\) expects array\\|object\|null, array\\|string\> given\.$#' count: 3 path: ../../tests/system/Models/DataConverterModelTest.php - - - message: '#^Parameter \#2 \$row of method CodeIgniter\\Model\:\:update\(\) expects array\\|object\|null, array\\> given\.$#' - count: 1 - path: ../../tests/system/Models/DataConverterModelTest.php - - message: '#^Parameter \#1 \$format of method CodeIgniter\\RESTful\\ResourceController\:\:setFormat\(\) expects ''json''\|''xml'', ''Nonsense'' given\.$#' count: 1 diff --git a/utils/phpstan-baseline/codeigniter.getReassignArray.neon b/utils/phpstan-baseline/codeigniter.getReassignArray.neon deleted file mode 100644 index f663c67ee50e..000000000000 --- a/utils/phpstan-baseline/codeigniter.getReassignArray.neon +++ /dev/null @@ -1,18 +0,0 @@ -# total 3 errors - -parameters: - ignoreErrors: - - - message: '#^Re\-assigning arrays to \$_GET directly is discouraged\.$#' - count: 1 - path: ../../tests/system/HTTP/IncomingRequestTest.php - - - - message: '#^Re\-assigning arrays to \$_GET directly is discouraged\.$#' - count: 1 - path: ../../tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php - - - - message: '#^Re\-assigning arrays to \$_GET directly is discouraged\.$#' - count: 1 - path: ../../tests/system/Helpers/FormHelperTest.php diff --git a/utils/phpstan-baseline/codeigniter.superglobalAccess.neon b/utils/phpstan-baseline/codeigniter.superglobalAccess.neon deleted file mode 100644 index 68f9316f8ee1..000000000000 --- a/utils/phpstan-baseline/codeigniter.superglobalAccess.neon +++ /dev/null @@ -1,123 +0,0 @@ -# total 29 errors - -parameters: - ignoreErrors: - - - message: '#^Accessing offset ''ANSICON'' directly on \$_SERVER is discouraged\.$#' - count: 1 - path: ../../system/CLI/CLI.php - - - - message: '#^Accessing offset ''NO_COLOR'' directly on \$_SERVER is discouraged\.$#' - count: 1 - path: ../../system/CLI/CLI.php - - - - message: '#^Accessing offset ''encryption\.key'' directly on \$_SERVER is discouraged\.$#' - count: 1 - path: ../../system/Commands/Encryption/GenerateKey.php - - - - message: '#^Accessing offset ''REMOTE_ADDR'' directly on \$_SERVER is discouraged\.$#' - count: 1 - path: ../../system/Common.php - - - - message: '#^Accessing offset ''REQUEST_METHOD'' directly on \$_SERVER is discouraged\.$#' - count: 1 - path: ../../system/Common.php - - - - message: '#^Accessing offset string directly on \$_SERVER is discouraged\.$#' - count: 1 - path: ../../system/Common.php - - - - message: '#^Accessing offset ''CI_ENVIRONMENT'' directly on \$_SERVER is discouraged\.$#' - count: 2 - path: ../../system/Config/AutoloadConfig.php - - - - message: '#^Accessing offset non\-falsy\-string directly on \$_SERVER is discouraged\.$#' - count: 4 - path: ../../system/Config/BaseConfig.php - - - - message: '#^Accessing offset string directly on \$_SERVER is discouraged\.$#' - count: 2 - path: ../../system/Config/DotEnv.php - - - - message: '#^Accessing offset string directly on \$_GET is discouraged\.$#' - count: 1 - path: ../../system/Superglobals.php - - - - message: '#^Accessing offset string directly on \$_SERVER is discouraged\.$#' - count: 1 - path: ../../system/Superglobals.php - - - - message: '#^Accessing offset ''foo'' directly on \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/CommonFunctionsSendTest.php - - - - message: '#^Accessing offset ''foo'' directly on \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/CommonFunctionsTest.php - - - - message: '#^Accessing offset ''BAR'' directly on \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/Config/DotEnvTest.php - - - - message: '#^Accessing offset ''FOO'' directly on \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/Config/DotEnvTest.php - - - - message: '#^Accessing offset ''NULL'' directly on \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/Config/DotEnvTest.php - - - - message: '#^Accessing offset ''SPACED'' directly on \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/Config/DotEnvTest.php - - - - message: '#^Accessing offset ''SimpleConfig_simple_name'' directly on \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/Config/DotEnvTest.php - - - - message: '#^Accessing offset ''CODEIGNITER_SCREAM_DEPRECATIONS'' directly on \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/Debug/ExceptionsTest.php - - - - message: '#^Accessing offset ''QUERY_STRING'' directly on \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php - - - - message: '#^Accessing offset ''CUSTOM_SERVER'' directly on \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/SuperglobalsTest.php - - - - message: '#^Accessing offset ''TEST_KEY'' directly on \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/SuperglobalsTest.php - - - - message: '#^Accessing offset ''custom_get'' directly on \$_GET is discouraged\.$#' - count: 1 - path: ../../tests/system/SuperglobalsTest.php - - - - message: '#^Accessing offset ''test'' directly on \$_GET is discouraged\.$#' - count: 1 - path: ../../tests/system/SuperglobalsTest.php diff --git a/utils/phpstan-baseline/codeigniter.superglobalAccessAssign.neon b/utils/phpstan-baseline/codeigniter.superglobalAccessAssign.neon deleted file mode 100644 index ab0d015d5677..000000000000 --- a/utils/phpstan-baseline/codeigniter.superglobalAccessAssign.neon +++ /dev/null @@ -1,28 +0,0 @@ -# total 5 errors - -parameters: - ignoreErrors: - - - message: '#^Assigning string directly on offset string of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../system/Config/DotEnv.php - - - - message: '#^Assigning ''1'' directly on offset ''CODEIGNITER_SCREAM_DEPRECATIONS'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/Debug/ExceptionsTest.php - - - - message: '#^Assigning ''test'' directly on offset ''HTTPS'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/HomeTest.php - - - - message: '#^Assigning ''test'' directly on offset ''HTTPS'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/Test/FeatureTestAutoRoutingImprovedTest.php - - - - message: '#^Assigning ''test'' directly on offset ''HTTPS'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/Test/FeatureTestTraitTest.php diff --git a/utils/phpstan-baseline/codeigniter.superglobalsGlobalAssign.neon b/utils/phpstan-baseline/codeigniter.superglobalsGlobalAssign.neon new file mode 100644 index 000000000000..e34453095d8e --- /dev/null +++ b/utils/phpstan-baseline/codeigniter.superglobalsGlobalAssign.neon @@ -0,0 +1,83 @@ +# total 17 errors + +parameters: + ignoreErrors: + - + message: '#^Direct global assignment to \$_SERVER is not allowed\.$#' + count: 1 + path: ../../tests/system/Config/BaseConfigTest.php + + - + message: '#^Direct global assignment to \$_SERVER is not allowed\.$#' + count: 1 + path: ../../tests/system/Config/ServicesTest.php + + - + message: '#^Direct global assignment to \$_SERVER is not allowed\.$#' + count: 2 + path: ../../tests/system/ControllerTest.php + + - + message: '#^Direct global assignment to \$_SERVER is not allowed\.$#' + count: 1 + path: ../../tests/system/Filters/FiltersTest.php + + - + message: '#^Direct global assignment to \$_COOKIE is not allowed\.$#' + count: 1 + path: ../../tests/system/HTTP/IncomingRequestTest.php + + - + message: '#^Direct global assignment to \$_GET is not allowed\.$#' + count: 1 + path: ../../tests/system/HTTP/IncomingRequestTest.php + + - + message: '#^Direct global assignment to \$_POST is not allowed\.$#' + count: 1 + path: ../../tests/system/HTTP/IncomingRequestTest.php + + - + message: '#^Direct global assignment to \$_REQUEST is not allowed\.$#' + count: 1 + path: ../../tests/system/HTTP/IncomingRequestTest.php + + - + message: '#^Direct global assignment to \$_SERVER is not allowed\.$#' + count: 1 + path: ../../tests/system/HTTP/IncomingRequestTest.php + + - + message: '#^Direct global assignment to \$_GET is not allowed\.$#' + count: 1 + path: ../../tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php + + - + message: '#^Direct global assignment to \$_SERVER is not allowed\.$#' + count: 1 + path: ../../tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php + + - + message: '#^Direct global assignment to \$_COOKIE is not allowed\.$#' + count: 1 + path: ../../tests/system/Helpers/CookieHelperTest.php + + - + message: '#^Direct global assignment to \$_GET is not allowed\.$#' + count: 1 + path: ../../tests/system/Helpers/FormHelperTest.php + + - + message: '#^Direct global assignment to \$_POST is not allowed\.$#' + count: 1 + path: ../../tests/system/Helpers/FormHelperTest.php + + - + message: '#^Direct global assignment to \$_SERVER is not allowed\.$#' + count: 1 + path: ../../tests/system/Helpers/URLHelper/SiteUrlCliTest.php + + - + message: '#^Direct global assignment to \$_SERVER is not allowed\.$#' + count: 1 + path: ../../tests/system/Helpers/URLHelper/SiteUrlTest.php diff --git a/utils/phpstan-baseline/codeigniter.superglobalsOffsetAccess.neon b/utils/phpstan-baseline/codeigniter.superglobalsOffsetAccess.neon new file mode 100644 index 000000000000..81cec98356ff --- /dev/null +++ b/utils/phpstan-baseline/codeigniter.superglobalsOffsetAccess.neon @@ -0,0 +1,148 @@ +# total 35 errors + +parameters: + ignoreErrors: + - + message: '#^Direct access to \$_SERVER\[''argv''\] is not allowed\.$#' + count: 2 + path: ../../system/Boot.php + + - + message: '#^Direct access to \$_SERVER\[''ANSICON''\] is not allowed\.$#' + count: 1 + path: ../../system/CLI/CLI.php + + - + message: '#^Direct access to \$_SERVER\[''NO_COLOR''\] is not allowed\.$#' + count: 1 + path: ../../system/CLI/CLI.php + + - + message: '#^Direct access to \$_SERVER\[''argv''\] is not allowed\.$#' + count: 1 + path: ../../system/CLI/CLI.php + + - + message: '#^Direct access to \$_SERVER\[''REMOTE_ADDR''\] is not allowed\.$#' + count: 1 + path: ../../system/Common.php + + - + message: '#^Direct access to \$_SERVER\[''REQUEST_METHOD''\] is not allowed\.$#' + count: 1 + path: ../../system/Common.php + + - + message: '#^Direct access to \$_SERVER\[\$key\] is not allowed\.$#' + count: 1 + path: ../../system/Common.php + + - + message: '#^Direct access to \$_SERVER\[''CI_ENVIRONMENT''\] is not allowed\.$#' + count: 2 + path: ../../system/Config/AutoloadConfig.php + + - + message: '#^Direct access to \$_SERVER\["\{\$prefix\}\.\{\$property\}"\] is not allowed\.$#' + count: 1 + path: ../../system/Config/BaseConfig.php + + - + message: '#^Direct access to \$_SERVER\["\{\$prefix\}_\{\$underscoreProperty\}"\] is not allowed\.$#' + count: 1 + path: ../../system/Config/BaseConfig.php + + - + message: '#^Direct access to \$_SERVER\["\{\$shortPrefix\}\.\{\$property\}"\] is not allowed\.$#' + count: 1 + path: ../../system/Config/BaseConfig.php + + - + message: '#^Direct access to \$_SERVER\["\{\$shortPrefix\}_\{\$underscoreProperty\}"\] is not allowed\.$#' + count: 1 + path: ../../system/Config/BaseConfig.php + + - + message: '#^Direct access to \$_COOKIE\[\$this\-\>config\-\>cookieName\] is not allowed\.$#' + count: 5 + path: ../../system/Session/Session.php + + - + message: '#^Direct access to \$_SERVER\[''BAR''\] is not allowed\.$#' + count: 1 + path: ../../tests/system/Config/DotEnvTest.php + + - + message: '#^Direct access to \$_SERVER\[''FOO''\] is not allowed\.$#' + count: 1 + path: ../../tests/system/Config/DotEnvTest.php + + - + message: '#^Direct access to \$_SERVER\[''NULL''\] is not allowed\.$#' + count: 1 + path: ../../tests/system/Config/DotEnvTest.php + + - + message: '#^Direct access to \$_SERVER\[''SPACED''\] is not allowed\.$#' + count: 1 + path: ../../tests/system/Config/DotEnvTest.php + + - + message: '#^Direct access to \$_SERVER\[''SimpleConfig_simple_name''\] is not allowed\.$#' + count: 1 + path: ../../tests/system/Config/DotEnvTest.php + + - + message: '#^Direct access to \$_SERVER\[''QUERY_STRING''\] is not allowed\.$#' + count: 1 + path: ../../tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php + + - + message: '#^Direct access to \$_COOKIE\[''custom_cookie''\] is not allowed\.$#' + count: 1 + path: ../../tests/system/SuperglobalsTest.php + + - + message: '#^Direct access to \$_COOKIE\[''session''\] is not allowed\.$#' + count: 1 + path: ../../tests/system/SuperglobalsTest.php + + - + message: '#^Direct access to \$_GET\[''custom_get''\] is not allowed\.$#' + count: 1 + path: ../../tests/system/SuperglobalsTest.php + + - + message: '#^Direct access to \$_GET\[''test''\] is not allowed\.$#' + count: 1 + path: ../../tests/system/SuperglobalsTest.php + + - + message: '#^Direct access to \$_POST\[''custom_post''\] is not allowed\.$#' + count: 1 + path: ../../tests/system/SuperglobalsTest.php + + - + message: '#^Direct access to \$_POST\[''test''\] is not allowed\.$#' + count: 1 + path: ../../tests/system/SuperglobalsTest.php + + - + message: '#^Direct access to \$_REQUEST\[''custom_request''\] is not allowed\.$#' + count: 1 + path: ../../tests/system/SuperglobalsTest.php + + - + message: '#^Direct access to \$_REQUEST\[''test''\] is not allowed\.$#' + count: 1 + path: ../../tests/system/SuperglobalsTest.php + + - + message: '#^Direct access to \$_SERVER\[''CUSTOM_SERVER''\] is not allowed\.$#' + count: 1 + path: ../../tests/system/SuperglobalsTest.php + + - + message: '#^Direct access to \$_SERVER\[''TEST_KEY''\] is not allowed\.$#' + count: 1 + path: ../../tests/system/SuperglobalsTest.php diff --git a/utils/phpstan-baseline/codeigniter.superglobalsOffsetAssign.neon b/utils/phpstan-baseline/codeigniter.superglobalsOffsetAssign.neon new file mode 100644 index 000000000000..9012f8e762f8 --- /dev/null +++ b/utils/phpstan-baseline/codeigniter.superglobalsOffsetAssign.neon @@ -0,0 +1,28 @@ +# total 5 errors + +parameters: + ignoreErrors: + - + message: '#^Direct assignment of string\|null to \$_COOKIE\[''csrf_cookie_name''\] is not allowed\.$#' + count: 1 + path: ../../system/Test/Mock/MockSecurity.php + + - + message: '#^Direct assignment of ''1'' to \$_SERVER\[''CODEIGNITER_SCREAM_DEPRECATIONS''\] is not allowed\.$#' + count: 1 + path: ../../tests/system/Debug/ExceptionsTest.php + + - + message: '#^Direct assignment of ''test'' to \$_SERVER\[''HTTPS''\] is not allowed\.$#' + count: 1 + path: ../../tests/system/HomeTest.php + + - + message: '#^Direct assignment of ''test'' to \$_SERVER\[''HTTPS''\] is not allowed\.$#' + count: 1 + path: ../../tests/system/Test/FeatureTestAutoRoutingImprovedTest.php + + - + message: '#^Direct assignment of ''test'' to \$_SERVER\[''HTTPS''\] is not allowed\.$#' + count: 1 + path: ../../tests/system/Test/FeatureTestTraitTest.php diff --git a/utils/phpstan-baseline/codeigniter.superglobalsOffsetUnset.neon b/utils/phpstan-baseline/codeigniter.superglobalsOffsetUnset.neon new file mode 100644 index 000000000000..bbdc3228eb79 --- /dev/null +++ b/utils/phpstan-baseline/codeigniter.superglobalsOffsetUnset.neon @@ -0,0 +1,33 @@ +# total 7 errors + +parameters: + ignoreErrors: + - + message: '#^Direct unset of \$_SERVER\[''encryption\.key''\] is not allowed\.$#' + count: 1 + path: ../../system/Commands/Encryption/GenerateKey.php + + - + message: '#^Direct unset of \$_COOKIE\[\$this\-\>config\-\>cookieName\] is not allowed\.$#' + count: 1 + path: ../../system/Session/Session.php + + - + message: '#^Direct unset of \$_SERVER\[''foo''\] is not allowed\.$#' + count: 1 + path: ../../tests/system/CommonFunctionsSendTest.php + + - + message: '#^Direct unset of \$_SERVER\[''foo''\] is not allowed\.$#' + count: 1 + path: ../../tests/system/CommonFunctionsTest.php + + - + message: '#^Direct unset of \$_SERVER\[''encryption\.key''\] is not allowed\.$#' + count: 2 + path: ../../tests/system/Config/DotEnvTest.php + + - + message: '#^Direct unset of \$_SERVER\[''CODEIGNITER_SCREAM_DEPRECATIONS''\] is not allowed\.$#' + count: 1 + path: ../../tests/system/Debug/ExceptionsTest.php diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 405c9a54ac16..d464ba204c47 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,13 +1,14 @@ -# total 2012 errors +# total 2035 errors includes: - argument.type.neon - arguments.count.neon - assign.propertyType.neon - - codeigniter.getReassignArray.neon - codeigniter.modelArgumentType.neon - - codeigniter.superglobalAccess.neon - - codeigniter.superglobalAccessAssign.neon + - codeigniter.superglobalsGlobalAssign.neon + - codeigniter.superglobalsOffsetAccess.neon + - codeigniter.superglobalsOffsetAssign.neon + - codeigniter.superglobalsOffsetUnset.neon - deadCode.unreachable.neon - empty.notAllowed.neon - function.resultUnused.neon diff --git a/utils/phpstan-baseline/property.nonObject.neon b/utils/phpstan-baseline/property.nonObject.neon index 33d6299465f8..26e874602857 100644 --- a/utils/phpstan-baseline/property.nonObject.neon +++ b/utils/phpstan-baseline/property.nonObject.neon @@ -28,57 +28,57 @@ parameters: path: ../../tests/system/Database/Live/GetTest.php - - message: '#^Cannot access property \$id on array\.$#' + message: '#^Cannot access property \$id on array\\.$#' count: 2 path: ../../tests/system/Models/FindModelTest.php - - message: '#^Cannot access property \$created_at on array\.$#' + message: '#^Cannot access property \$created_at on array\\.$#' count: 3 path: ../../tests/system/Models/InsertModelTest.php - - message: '#^Cannot access property \$created_at on array\.$#' + message: '#^Cannot access property \$created_at on array\\.$#' count: 1 path: ../../tests/system/Models/MiscellaneousModelTest.php - - message: '#^Cannot access property \$description on array\.$#' + message: '#^Cannot access property \$description on array\\.$#' count: 1 path: ../../tests/system/Models/SaveModelTest.php - - message: '#^Cannot access property \$country on array\.$#' + message: '#^Cannot access property \$country on array\\.$#' count: 2 path: ../../tests/system/Models/TimestampModelTest.php - - message: '#^Cannot access property \$created_at on array\.$#' + message: '#^Cannot access property \$created_at on array\\.$#' count: 2 path: ../../tests/system/Models/TimestampModelTest.php - - message: '#^Cannot access property \$id on array\.$#' + message: '#^Cannot access property \$id on array\\.$#' count: 2 path: ../../tests/system/Models/TimestampModelTest.php - - message: '#^Cannot access property \$updated_at on array\.$#' + message: '#^Cannot access property \$updated_at on array\\.$#' count: 2 path: ../../tests/system/Models/TimestampModelTest.php - - message: '#^Cannot access property \$value on list\\.$#' + message: '#^Cannot access property \$value on array\\|string, mixed\>\.$#' count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Cannot access property \$key on array\.$#' + message: '#^Cannot access property \$key on array\\.$#' count: 7 path: ../../tests/system/Models/WhenWhenNotModelTest.php - - message: '#^Cannot access property \$value on array\.$#' + message: '#^Cannot access property \$value on array\\.$#' count: 1 path: ../../tests/system/Models/WhenWhenNotModelTest.php From 18887b2f8be5d78e710b6bd56f9adf334bd13808 Mon Sep 17 00:00:00 2001 From: Thomas Meschke Date: Sun, 21 Jun 2026 18:17:59 +0200 Subject: [PATCH 35/65] fix: unify casing in BaseService::injectMock (#10316) --- system/Config/BaseService.php | 5 +++-- user_guide_src/source/changelogs/v4.7.4.rst | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/system/Config/BaseService.php b/system/Config/BaseService.php index 483da962d5ad..579879e9c768 100644 --- a/system/Config/BaseService.php +++ b/system/Config/BaseService.php @@ -445,8 +445,9 @@ public static function resetSingle(string $name) */ public static function injectMock(string $name, $mock) { - static::$instances[$name] = $mock; - static::$mocks[strtolower($name)] = $mock; + $name = strtolower($name); + static::$instances[$name] = $mock; + static::$mocks[$name] = $mock; } /** diff --git a/user_guide_src/source/changelogs/v4.7.4.rst b/user_guide_src/source/changelogs/v4.7.4.rst index 1373f9fa1720..1cf22169f347 100644 --- a/user_guide_src/source/changelogs/v4.7.4.rst +++ b/user_guide_src/source/changelogs/v4.7.4.rst @@ -35,6 +35,7 @@ Bugs Fixed - **Common:** Fixed a bug in ``env()`` where a ``TypeError`` could be thrown when non-string values were passed. - **Common:** Fixed ``esc()`` to propagate encoding correctly and prevent reference leaks. - **Commands:** Fixed a bug where ``spark lang:find`` treated translation keys already provided by the framework or another namespace (such as ``Errors.*`` in ``system/Language``) as new, listing them under ``--show-new`` and writing untranslated placeholders into ``app/Language`` that overrode the existing translations. +- **Config:** Fixed a bug where ``BaseService::injectMock`` did not apply ``strtolower`` consistently, causing inconsistent Service and Mock registration and resolution. - **Database:** Fixed a bug where ``updateBatch()`` could be called after Query Builder ``where()`` conditions, even though it's not supported. In this situation, now the ``DatabaseException`` is thrown. - **Filters:** Fixed a bug in ``InvalidChars`` filter where invalid UTF-8 or control characters in array keys were not checked. - **HTTP:** Fixed a bug where the User Agent library reported Safari's WebKit version instead of the browser version from the ``Version`` token. From cad7f2d74b203080cdb02c1b5bfc5e9ae8a65eda Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Tue, 23 Jun 2026 00:47:24 +0700 Subject: [PATCH 36/65] chore: Bump to Rector 2.5.2 and clean up unused skip config (#10332) * chore: Bump to Rector ~2.5.2 and clean up unused skip config * chore: fix re-run rector * chore: fix skip bug * chore: clean up config * chore: clean up config * chore: clean up config --- composer.json | 2 +- rector.php | 34 +++++++--------------------------- system/CLI/CLI.php | 2 +- system/HTTP/CLIRequest.php | 2 +- system/Test/DOMParser.php | 2 +- 5 files changed, 11 insertions(+), 31 deletions(-) diff --git a/composer.json b/composer.json index 6f15a3622b7a..9de4dacdf72f 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.4.6", + "rector/rector": "2.5.2", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { diff --git a/rector.php b/rector.php index 4552a898aee3..cfcbbd236bad 100644 --- a/rector.php +++ b/rector.php @@ -24,20 +24,19 @@ use Rector\Config\RectorConfig; use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedConstructorParamRector; use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPrivateMethodRector; -use Rector\DeadCode\Rector\If_\UnwrapFutureCompatibleIfPhpVersionRector; use Rector\DeadCode\Rector\MethodCall\RemoveNullArgOnNullDefaultParamRector; use Rector\EarlyReturn\Rector\Foreach_\ChangeNestedForeachIfsToEarlyContinueRector; use Rector\EarlyReturn\Rector\If_\ChangeIfElseValueAssignToEarlyReturnRector; use Rector\EarlyReturn\Rector\If_\RemoveAlwaysElseRector; use Rector\EarlyReturn\Rector\Return_\PreparedValueToEarlyReturnRector; use Rector\Php70\Rector\FuncCall\RandomFunctionRector; -use Rector\Php70\Rector\StaticCall\StaticCallOnNonStaticToInstanceCallRector; use Rector\Php71\Rector\FuncCall\RemoveExtraParametersRector; use Rector\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector; use Rector\Php81\Rector\FuncCall\NullToStrictStringFuncCallArgRector; use Rector\PHPUnit\CodeQuality\Rector\Class_\YieldDataProviderRector; use Rector\PHPUnit\CodeQuality\Rector\FuncCall\AssertFuncCallToPHPUnitAssertRector; use Rector\PHPUnit\CodeQuality\Rector\StmtsAwareInterface\DeclareStrictTypesTestsRector; +use Rector\PostRector\Rector\UnusedImportRemovingPostRector; use Rector\Privatization\Rector\Class_\FinalizeTestCaseClassRector; use Rector\Privatization\Rector\Property\PrivatizeFinalClassPropertyRector; use Rector\Renaming\Rector\ConstFetch\RenameConstantRector; @@ -96,17 +95,10 @@ RemoveUnusedPrivateMethodRector::class => [ // private method called via getPrivateMethodInvoker - __DIR__ . '/tests/system/Test/ReflectionHelperTest.php', __DIR__ . '/tests/_support/Test/TestForReflectionHelper.php', ], RemoveUnusedConstructorParamRector::class => [ - // there are deprecated parameters - __DIR__ . '/system/Debug/Exceptions.php', - // @TODO remove if deprecated $httpVerb is removed - __DIR__ . '/system/Router/AutoRouterImproved.php', - // @TODO remove if deprecated $config is removed - __DIR__ . '/system/HTTP/Request.php', __DIR__ . '/system/HTTP/Response.php', ], @@ -115,11 +107,6 @@ __DIR__ . '/tests/system/Debug/ToolbarTest.php', ], - // check on constant compare - UnwrapFutureCompatibleIfPhpVersionRector::class => [ - __DIR__ . '/system/Autoloader/Autoloader.php', - ], - UnderscoreToCamelCaseVariableNameRector::class => [ // session handlers have the gc() method with underscored parameter `$max_lifetime` __DIR__ . '/system/Session/Handlers', @@ -130,10 +117,7 @@ __DIR__ . '/app', __DIR__ . '/system/CodeIgniter.php', __DIR__ . '/system/Config/BaseConfig.php', - __DIR__ . '/system/Commands/Generators/Views', - __DIR__ . '/system/Pager/Views', __DIR__ . '/system/Test/ControllerTestTrait.php', - __DIR__ . '/system/Validation/Views', __DIR__ . '/system/View/Parser.php', __DIR__ . '/tests/system/Debug/ExceptionsTest.php', ], @@ -149,15 +133,11 @@ __DIR__ . '/system/Filters/Filters.php', __DIR__ . '/system/HTTP/CURLRequest.php', __DIR__ . '/system/HTTP/DownloadResponse.php', - __DIR__ . '/system/HTTP/IncomingRequest.php', __DIR__ . '/system/Security/Security.php', __DIR__ . '/system/Session/Session.php', ], ReturnNeverTypeRector::class => [ - __DIR__ . '/system/Cache/Handlers/BaseHandler.php', - __DIR__ . '/system/Cache/Handlers/MemcachedHandler.php', - __DIR__ . '/system/Cache/Handlers/WincacheHandler.php', __DIR__ . '/system/CodeIgniter.php', __DIR__ . '/system/Database/MySQLi/Utils.php', __DIR__ . '/system/Database/OCI8/Utils.php', @@ -166,8 +146,6 @@ __DIR__ . '/system/Database/SQLite3/Utils.php', __DIR__ . '/system/HTTP/DownloadResponse.php', __DIR__ . '/system/HTTP/SiteURI.php', - __DIR__ . '/system/Helpers/kint_helper.php', - __DIR__ . '/tests/_support/Autoloader/FatalLocator.php', ], // Unnecessary (string) is inserted @@ -197,12 +175,13 @@ __DIR__ . '/tests/system/Models', ], - StaticCallOnNonStaticToInstanceCallRector::class => [ - __DIR__ . '/tests/_support/Config/Services.php', + UnusedImportRemovingPostRector::class => [ + // buggy on auto import removed + __DIR__ . '/system/HTTP/Response.php', ], ]) // auto import fully qualified class names - ->withImportNames(removeUnusedImports: true) + ->withImportNames() ->withRules([ DeclareStrictTypesRector::class, UnderscoreToCamelCaseVariableNameRector::class, @@ -231,4 +210,5 @@ ->withConfiguredRule(RenameConstantRector::class, [ 'FILTER_DEFAULT' => 'FILTER_UNSAFE_RAW', ]) - ->withCodeQualityLevel(61); + ->withCodeQualityLevel(61) + ->reportUnusedSkips(); diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index 9cdbbbada0b5..d26681213914 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -1012,7 +1012,7 @@ public static function getOptionString(bool $useLongOpts = false, bool $trim = f continue; } - if (mb_strpos($value, ' ') !== false) { + if (str_contains($value, ' ')) { $out .= "\"{$value}\" "; } elseif ($value !== null) { $out .= "{$value} "; diff --git a/system/HTTP/CLIRequest.php b/system/HTTP/CLIRequest.php index bde4e1b8e3f8..453272a4f59a 100644 --- a/system/HTTP/CLIRequest.php +++ b/system/HTTP/CLIRequest.php @@ -164,7 +164,7 @@ public function getOptionString(bool $useLongOpts = false): string continue; } - if (mb_strpos($value, ' ') !== false) { + if (str_contains($value, ' ')) { $out .= '"' . $value . '" '; } else { $out .= "{$value} "; diff --git a/system/Test/DOMParser.php b/system/Test/DOMParser.php index d8521edfbc86..52765a4d656b 100644 --- a/system/Test/DOMParser.php +++ b/system/Test/DOMParser.php @@ -112,7 +112,7 @@ public function see(?string $search = null, ?string $element = null): bool if ($element === null) { $content = $this->dom->saveHTML($this->dom->documentElement); - return mb_strpos($content, $search) !== false; + return str_contains($content, $search); } $result = $this->doXPath($search, $element); From 27e46b8b58552cf55fe22d9a968c56614eb170f9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 01:14:25 +0700 Subject: [PATCH 37/65] chore(deps-dev): update boundwize/structarmed requirement (#10334) Updates the requirements on [boundwize/structarmed](https://github.com/boundwize/structarmed) to permit the latest version. Updates `boundwize/structarmed` to 0.13.8 - [Release notes](https://github.com/boundwize/structarmed/releases) - [Commits](https://github.com/boundwize/structarmed/compare/0.13.4...0.13.8) --- updated-dependencies: - dependency-name: boundwize/structarmed dependency-version: 0.13.8 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 9de4dacdf72f..160daedc0d8e 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "psr/log": "^3.0" }, "require-dev": { - "boundwize/structarmed": "0.13.4", + "boundwize/structarmed": "0.13.8", "codeigniter/phpstan-codeigniter": "^2.1", "fakerphp/faker": "^1.24", "kint-php/kint": "^6.1", From 28c2f53c811ce51b5b4eaa6ee0b5f1e0b86c50d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 21:52:39 +0200 Subject: [PATCH 38/65] chore(deps): bump actions/cache in / (#10336) Bumps [actions/cache](https://github.com/actions/cache) in `/` from 5.0.5 to 6.0.0. Updates `actions/cache` from 5.0.5 to 6.0.0 - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/27d5ce7f107fe9357f9df03efb73ab90386fccae...2c8a9bd7457de244a408f35966fab2fb45fda9c8) --- updated-dependencies: - dependency-name: actions/cache dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major 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 | 4 ++-- .github/workflows/reusable-phpunit-test.yml | 4 ++-- .github/workflows/reusable-serviceless-phpunit-test.yml | 4 ++-- .github/workflows/test-coding-standards.yml | 2 +- .github/workflows/test-phpstan.yml | 4 ++-- .github/workflows/test-psalm.yml | 4 ++-- .github/workflows/test-random-execution.yml | 2 +- .github/workflows/test-rector.yml | 4 ++-- .github/workflows/test-structarmed.yml | 4 ++-- 9 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/reusable-coveralls.yml b/.github/workflows/reusable-coveralls.yml index 9acb9e374910..74c04243de03 100644 --- a/.github/workflows/reusable-coveralls.yml +++ b/.github/workflows/reusable-coveralls.yml @@ -43,7 +43,7 @@ jobs: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: ${{ steps.composer-cache.outputs.COMPOSER_CACHE_FILES_DIR }} key: ${{ github.job }}-php-${{ inputs.php-version }}-${{ hashFiles('**/composer.*') }} @@ -52,7 +52,7 @@ jobs: ${{ github.job }}- - name: Cache PHPUnit's static analysis cache - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: build/.phpunit.cache/code-coverage key: phpunit-code-coverage-${{ hashFiles('**/phpunit.*') }} diff --git a/.github/workflows/reusable-phpunit-test.yml b/.github/workflows/reusable-phpunit-test.yml index 0ff6e04dc7f9..e0c8b6c835a7 100644 --- a/.github/workflows/reusable-phpunit-test.yml +++ b/.github/workflows/reusable-phpunit-test.yml @@ -192,7 +192,7 @@ jobs: echo "ARTIFACT_NAME=${{ inputs.job-id || github.job }}-php-${{ inputs.php-version }}-db-${{ inputs.db-platform || 'none' }}${{ inputs.mysql-version || '' }}" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.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.*') }} @@ -203,7 +203,7 @@ jobs: - name: Cache PHPUnit's static analysis cache if: ${{ inputs.enable-artifact-upload }} - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: build/.phpunit.cache/code-coverage key: phpunit-code-coverage-${{ hashFiles('**/phpunit.*') }} diff --git a/.github/workflows/reusable-serviceless-phpunit-test.yml b/.github/workflows/reusable-serviceless-phpunit-test.yml index 7bb45e2e74db..81ed2561f659 100644 --- a/.github/workflows/reusable-serviceless-phpunit-test.yml +++ b/.github/workflows/reusable-serviceless-phpunit-test.yml @@ -89,7 +89,7 @@ jobs: echo "ARTIFACT_NAME=${{ inputs.job-id || github.job }}-php-${{ inputs.php-version }}" >> $GITHUB_OUTPUT - name: Cache Composer dependencies - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: ${{ steps.setup-env.outputs.COMPOSER_CACHE_FILES_DIR }} key: ${{ inputs.job-id || github.job }}-php-${{ inputs.php-version }}-${{ hashFiles('**/composer.*') }} @@ -99,7 +99,7 @@ jobs: - name: Cache PHPUnit's static analysis cache if: ${{ inputs.enable-artifact-upload }} - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: build/.phpunit.cache/code-coverage key: phpunit-code-coverage-${{ hashFiles('**/phpunit.*') }} diff --git a/.github/workflows/test-coding-standards.yml b/.github/workflows/test-coding-standards.yml index 9b91c83a809f..318643448765 100644 --- a/.github/workflows/test-coding-standards.yml +++ b/.github/workflows/test-coding-standards.yml @@ -49,7 +49,7 @@ jobs: run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: ${{ steps.composer-cache.outputs.COMPOSER_CACHE_FILES_DIR }} key: ${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }} diff --git a/.github/workflows/test-phpstan.yml b/.github/workflows/test-phpstan.yml index 318f29307add..3eebbfe010d8 100644 --- a/.github/workflows/test-phpstan.yml +++ b/.github/workflows/test-phpstan.yml @@ -66,7 +66,7 @@ jobs: run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: ${{ steps.composer-cache.outputs.COMPOSER_CACHE_FILES_DIR }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} @@ -76,7 +76,7 @@ jobs: run: mkdir -p build/phpstan - name: Cache PHPStan result cache directory - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: build/phpstan key: ${{ runner.os }}-phpstan-${{ github.sha }} diff --git a/.github/workflows/test-psalm.yml b/.github/workflows/test-psalm.yml index 01b6d1f701f8..c95ad3d9e697 100644 --- a/.github/workflows/test-psalm.yml +++ b/.github/workflows/test-psalm.yml @@ -54,7 +54,7 @@ jobs: run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache composer dependencies - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: ${{ steps.composer-cache.outputs.COMPOSER_CACHE_FILES_DIR }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}-${{ hashFiles('**/composer.lock') }} @@ -64,7 +64,7 @@ jobs: run: mkdir -p build/psalm - name: Cache Psalm results - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: build/psalm key: ${{ runner.os }}-psalm-${{ github.sha }} diff --git a/.github/workflows/test-random-execution.yml b/.github/workflows/test-random-execution.yml index 57841b4d19d1..a5d7a12ee093 100644 --- a/.github/workflows/test-random-execution.yml +++ b/.github/workflows/test-random-execution.yml @@ -186,7 +186,7 @@ jobs: run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache composer dependencies - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: ${{ steps.composer-cache.outputs.COMPOSER_CACHE_FILES_DIR }} key: PHP_${{ matrix.php-version }}-${{ hashFiles('**/composer.*') }} diff --git a/.github/workflows/test-rector.yml b/.github/workflows/test-rector.yml index d51d36fcc8ba..dd6cfe8c7c4a 100644 --- a/.github/workflows/test-rector.yml +++ b/.github/workflows/test-rector.yml @@ -72,7 +72,7 @@ jobs: run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: ${{ steps.composer-cache.outputs.COMPOSER_CACHE_FILES_DIR }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} @@ -82,7 +82,7 @@ jobs: run: composer update --ansi --no-interaction ${{ matrix.composer-option }} - name: Rector Cache - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: /tmp/rector key: ${{ runner.os }}-rector-${{ github.run_id }} diff --git a/.github/workflows/test-structarmed.yml b/.github/workflows/test-structarmed.yml index 24e39b73cb91..828552c4f306 100644 --- a/.github/workflows/test-structarmed.yml +++ b/.github/workflows/test-structarmed.yml @@ -68,7 +68,7 @@ jobs: run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: ${{ steps.composer-cache.outputs.COMPOSER_CACHE_FILES_DIR }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} @@ -78,7 +78,7 @@ jobs: run: composer update --ansi --no-interaction - name: Structarmed Cache - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: /tmp/structarmed key: ${{ runner.os }}-structarmed-${{ github.run_id }} From 19cae6e63fc04594171a9ce6926bb5f60c2c2efa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 23:30:49 +0800 Subject: [PATCH 39/65] chore(deps): bump actions/setup-python in / (#10338) Bumps [actions/setup-python](https://github.com/actions/setup-python) in `/` from 6.2.0 to 6.3.0. Updates `actions/setup-python` from 6.2.0 to 6.3.0 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/a309ff8b426b58ec0e2a45f0f869d46889d02405...ece7cb06caefa5fff74198d8649806c4678c61a1) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: 6.3.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 d74cf7e54d05..221383aafd57 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@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: '3.12' diff --git a/.github/workflows/deploy-userguide-latest.yml b/.github/workflows/deploy-userguide-latest.yml index a56d4b48ca94..9bbd7a5bb7ba 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@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: '3.12' diff --git a/.github/workflows/test-userguide.yml b/.github/workflows/test-userguide.yml index 211ac47ab481..1dd382cd069b 100644 --- a/.github/workflows/test-userguide.yml +++ b/.github/workflows/test-userguide.yml @@ -27,7 +27,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Setup Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: '3.12' From e91517c7f958b86bc54c95ea5b01a5bc955b46be Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Mon, 29 Jun 2026 08:23:52 +0200 Subject: [PATCH 40/65] fix: normalize SodiumHandler params and padding failures (#10321) * fix: SodiumHandler encrypt/decrypt key handling bugs * chore: clean up redundant comments * test: use Reflection to access protected property in SodiumHandlerTest * test: resolve ReflectionException * Update tests/system/Encryption/Handlers/SodiumHandlerTest.php Co-authored-by: Michal Sniatala * fix: address PR review comments on SodiumHandler * style: apply cs-fix to SodiumHandler * fix: import SodiumException to resolve PHPStan error * test: update expected exception for mismatched block size * style: remove unused SodiumException import in test * Update user_guide_src/source/changelogs/v4.7.4.rst Co-authored-by: Michal Sniatala --------- Co-authored-by: Michal Sniatala --- system/Encryption/Handlers/SodiumHandler.php | 70 +++++++++---------- .../Encryption/Handlers/SodiumHandlerTest.php | 53 ++++++++++++++ user_guide_src/source/changelogs/v4.7.4.rst | 1 + 3 files changed, 86 insertions(+), 38 deletions(-) diff --git a/system/Encryption/Handlers/SodiumHandler.php b/system/Encryption/Handlers/SodiumHandler.php index 248436c28799..55bf14893251 100644 --- a/system/Encryption/Handlers/SodiumHandler.php +++ b/system/Encryption/Handlers/SodiumHandler.php @@ -15,6 +15,7 @@ use CodeIgniter\Encryption\Exceptions\EncryptionException; use SensitiveParameter; +use SodiumException; /** * SodiumHandler uses libsodium in encryption. @@ -43,34 +44,31 @@ class SodiumHandler extends BaseHandler */ public function encrypt(#[SensitiveParameter] $data, #[SensitiveParameter] $params = null) { - // Allow key override - $key = $params !== null - ? (is_array($params) && isset($params['key']) ? $params['key'] : $params) - : $this->key; - - // Allow blockSize override - $blockSize = (is_array($params) && isset($params['blockSize'])) - ? $params['blockSize'] - : $this->blockSize; + $key = $this->key; + $blockSize = $this->blockSize; + + if ($params !== null) { + if (is_array($params)) { + $key = $params['key'] ?? $key; + $blockSize = $params['blockSize'] ?? $blockSize; + } else { + $key = $params; + } + } - if (empty($key)) { + if (empty($key) || strlen((string) $key) !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES) { throw EncryptionException::forNeedsStarterKey(); } - // create a nonce for this operation - $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); // 24 bytes - - // add padding before we encrypt the data if ($blockSize <= 0) { throw EncryptionException::forEncryptionFailed(); } - $data = sodium_pad($data, $blockSize); + $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); + $data = sodium_pad($data, $blockSize); - // encrypt message and combine with nonce $ciphertext = $nonce . sodium_crypto_secretbox($data, $nonce, $key); - // cleanup buffers sodium_memzero($data); sodium_memzero($key); @@ -82,45 +80,41 @@ public function encrypt(#[SensitiveParameter] $data, #[SensitiveParameter] $para */ public function decrypt($data, #[SensitiveParameter] $params = null) { - // Allow key override - $key = $params !== null - ? (is_array($params) && isset($params['key']) ? $params['key'] : $params) - : $this->key; - - // Allow blockSize override - $blockSize = (is_array($params) && isset($params['blockSize'])) - ? $params['blockSize'] - : $this->blockSize; + $key = $this->key; + $blockSize = $this->blockSize; + + if ($params !== null) { + if (is_array($params)) { + $key = $params['key'] ?? $key; + $blockSize = $params['blockSize'] ?? $blockSize; + } else { + $key = $params; + } + } - if (empty($key)) { + if (empty($key) || strlen((string) $key) !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES) { throw EncryptionException::forNeedsStarterKey(); } if (mb_strlen($data, '8bit') < (SODIUM_CRYPTO_SECRETBOX_NONCEBYTES + SODIUM_CRYPTO_SECRETBOX_MACBYTES)) { - // message was truncated throw EncryptionException::forAuthenticationFailed(); } - // Extract info from encrypted data $nonce = self::substr($data, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); $ciphertext = self::substr($data, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); - // decrypt data $data = sodium_crypto_secretbox_open($ciphertext, $nonce, $key); - if ($data === false) { - // message was tampered in transit - throw EncryptionException::forAuthenticationFailed(); // @codeCoverageIgnore + if ($data === false || $blockSize <= 0) { + throw EncryptionException::forAuthenticationFailed(); } - // remove extra padding during encryption - if ($blockSize <= 0) { + try { + $data = sodium_unpad($data, $blockSize); + } catch (SodiumException) { throw EncryptionException::forAuthenticationFailed(); } - $data = sodium_unpad($data, $blockSize); - - // cleanup buffers sodium_memzero($ciphertext); sodium_memzero($key); diff --git a/tests/system/Encryption/Handlers/SodiumHandlerTest.php b/tests/system/Encryption/Handlers/SodiumHandlerTest.php index 4e963762b997..09b1784e4a12 100644 --- a/tests/system/Encryption/Handlers/SodiumHandlerTest.php +++ b/tests/system/Encryption/Handlers/SodiumHandlerTest.php @@ -151,4 +151,57 @@ public function testInternalKeyNotModifiedByParams(): void $this->assertSame($message, $encrypter->decrypt($encoded, ['key' => $differentKey])); } + + public function testNullKeyOverrideFallsBackToInstanceKey(): void + { + /** @var SodiumHandler $encrypter */ + $encrypter = $this->encryption->initialize($this->config); + + $ciphertext = $encrypter->encrypt('message', ['key' => null]); + + $this->assertSame('message', $encrypter->decrypt($ciphertext, ['key' => null])); + } + + public function testInvalidKeyLengthThrowsEncryptionException(): void + { + $this->expectException(EncryptionException::class); + /** @var SodiumHandler $encrypter */ + $encrypter = $this->encryption->initialize($this->config); + + $encrypter->encrypt('message', str_repeat('a', 31)); + } + + public function testMismatchedBlockSizeThrowsEncryptionException(): void + { + $this->expectException(EncryptionException::class); + /** @var SodiumHandler $encrypter */ + $encrypter = $this->encryption->initialize($this->config); + + $ciphertext = $encrypter->encrypt('message', ['blockSize' => 16]); + + $encrypter->decrypt($ciphertext, ['blockSize' => 32]); + } + + public function testDecryptTamperedMessageThrowsException(): void + { + $this->expectException(EncryptionException::class); + $encrypter = $this->encryption->initialize($this->config); + + $ciphertext = $encrypter->encrypt('message'); + + $ciphertext[0] = $ciphertext[0] === 'a' ? 'b' : 'a'; + + $encrypter->decrypt($ciphertext); + } + + public function testOverrideKeyAsStringWorks(): void + { + $encrypter = $this->encryption->initialize($this->config); + $newKey = sodium_crypto_secretbox_keygen(); + + $ciphertext = $encrypter->encrypt('message', $newKey); + $decrypted = $encrypter->decrypt($ciphertext, $newKey); + + $this->assertSame('message', $decrypted); + } } diff --git a/user_guide_src/source/changelogs/v4.7.4.rst b/user_guide_src/source/changelogs/v4.7.4.rst index 1cf22169f347..f24a070e3177 100644 --- a/user_guide_src/source/changelogs/v4.7.4.rst +++ b/user_guide_src/source/changelogs/v4.7.4.rst @@ -37,6 +37,7 @@ Bugs Fixed - **Commands:** Fixed a bug where ``spark lang:find`` treated translation keys already provided by the framework or another namespace (such as ``Errors.*`` in ``system/Language``) as new, listing them under ``--show-new`` and writing untranslated placeholders into ``app/Language`` that overrode the existing translations. - **Config:** Fixed a bug where ``BaseService::injectMock`` did not apply ``strtolower`` consistently, causing inconsistent Service and Mock registration and resolution. - **Database:** Fixed a bug where ``updateBatch()`` could be called after Query Builder ``where()`` conditions, even though it's not supported. In this situation, now the ``DatabaseException`` is thrown. +- **Encryption:** Fixed bugs in ``SodiumHandler`` where runtime ``blockSize`` overrides without ``key`` were handled incorrectly, invalid key lengths could leak native Sodium errors, and mismatched decrypt ``blockSize`` values could throw ``SodiumException`` instead of ``EncryptionException``. - **Filters:** Fixed a bug in ``InvalidChars`` filter where invalid UTF-8 or control characters in array keys were not checked. - **HTTP:** Fixed a bug where the User Agent library reported Safari's WebKit version instead of the browser version from the ``Version`` token. - **Model:** Fixed a bug in ``Model::objectToRawArray()`` where the ``$recursive`` parameter was ignored. From e154a2a463e08329b9134093d6e706733f41e79b Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Mon, 29 Jun 2026 08:58:03 +0200 Subject: [PATCH 41/65] fix(Validation): correct required_without logic and prevent array key warnings (#10328) --- system/Validation/Rules.php | 15 +++++-- tests/system/Validation/ValidationTest.php | 45 +++++++++++++++++++++ user_guide_src/source/changelogs/v4.7.4.rst | 1 + 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/system/Validation/Rules.php b/system/Validation/Rules.php index e3e256b099b4..17d3ced44e44 100644 --- a/system/Validation/Rules.php +++ b/system/Validation/Rules.php @@ -429,15 +429,22 @@ public function required_without( $fieldData = dot_array_search($otherField, $data); $fieldSplitArray = explode('.', $field); - $fieldKey = $fieldSplitArray[1]; + $fieldKey = $fieldSplitArray[1] ?? null; if (is_array($fieldData)) { - return ! empty(dot_array_search($otherField, $data)[$fieldKey]); + if (empty($fieldData[$fieldKey])) { + return false; + } + + continue; } - $nowField = str_replace('*', $fieldKey, $otherField); + + $nowField = str_replace('*', (string) $fieldKey, $otherField); $nowFieldVaule = dot_array_search($nowField, $data); - return null !== $nowFieldVaule; + if ($nowFieldVaule === null) { + return false; + } } } diff --git a/tests/system/Validation/ValidationTest.php b/tests/system/Validation/ValidationTest.php index 7e0f4411f079..7cf70deaeb27 100644 --- a/tests/system/Validation/ValidationTest.php +++ b/tests/system/Validation/ValidationTest.php @@ -1854,6 +1854,51 @@ public function testRequireWithoutWithAsterisk(): void ); } + /** + * Test that `required_without` checks all fields in dot-notation when there are multiple fields. + */ + public function testRequireWithoutMultipleWithAsterisk(): void + { + $data = [ + 'a' => [ + ['b' => 1, 'c' => 2, 'd' => ''], + ['b' => 1, 'c' => '', 'd' => ''], + ], + ]; + + $this->validation->setRules([ + 'a.*.d' => 'required_without[a.*.b,a.*.c]', + ])->run($data); + + $this->assertSame( + 'The a.*.d field is required when a.*.b,a.*.c is not present.', + $this->validation->getError('a.1.d'), + ); + } + + /** + * Test that `required_without` handles a non-asterisk field checked against an asterisk field + * without throwing undefined array key warnings for `$fieldSplitArray[1]`. + */ + public function testRequireWithoutAsteriskOnNonAsteriskField(): void + { + $data = [ + 'foo' => '', + 'a' => [ + ['b' => ''], + ], + ]; + + $this->validation->setRules([ + 'foo' => 'required_without[a.*.b]', + ])->run($data); + + $this->assertSame( + 'The foo field is required when a.*.b is not present.', + $this->validation->getError('foo'), + ); + } + /** * @see https://github.com/codeigniter4/CodeIgniter4/issues/8128 */ diff --git a/user_guide_src/source/changelogs/v4.7.4.rst b/user_guide_src/source/changelogs/v4.7.4.rst index f24a070e3177..7aba967ec02a 100644 --- a/user_guide_src/source/changelogs/v4.7.4.rst +++ b/user_guide_src/source/changelogs/v4.7.4.rst @@ -43,6 +43,7 @@ Bugs Fixed - **Model:** Fixed a bug in ``Model::objectToRawArray()`` where the ``$recursive`` parameter was ignored. - **Session:** Fixed a bug in ``RedisHandler`` where the configured ``$lockMaxRetries`` and ``$lockRetryInterval`` values were not respected when acquiring session locks. - **Testing:** Fixed a bug where using ``MockInputOutput`` within a test that also uses ``StreamFilterTrait`` tore down the trait's stream filters, so CLI output produced after the ``MockInputOutput`` interaction (such as in ``tearDown()``) was no longer captured and leaked to the console. +- **Validation:** Fixed bugs in the ``required_without`` rule logic where using array dot notation caused early exits ignoring subsequent fields and triggered an ``Undefined array key`` warning for missing keys. See the repo's `CHANGELOG.md `_ From 042d365143d18cc912c6f17015d393abe4da8e51 Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Mon, 29 Jun 2026 08:58:31 +0200 Subject: [PATCH 42/65] docs: warn against user-controlled image paths (#10325) --- user_guide_src/source/libraries/images.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/user_guide_src/source/libraries/images.rst b/user_guide_src/source/libraries/images.rst index 4696b0d126d4..2bffd2b9a122 100644 --- a/user_guide_src/source/libraries/images.rst +++ b/user_guide_src/source/libraries/images.rst @@ -42,6 +42,14 @@ The available Handlers are as follows: On Windows, the ImageMagick handler requires **absolute file paths** when loading images (for example, using ``WRITEPATH`` or ``FCPATH``). +.. warning:: + Do not let user input directly decide the image source path, storage + directory, or filename. This includes values passed to methods like + ``save()``, ``copy()`` and others, and any path or filename used to store + processed images. Use directories controlled by your application, and + generate filenames yourself or sanitize them with + :php:func:`sanitize_filename`. + ******************* Processing an Image ******************* From fa890c2843b3da1a40f8e75b9f080d624f910d06 Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Mon, 29 Jun 2026 08:59:54 +0200 Subject: [PATCH 43/65] refactor(HTTP): optimize file path parsing in `download()` method (#10330) * refactor(HTTP): optimize download() file path processing and add extreme filename test * Update tests/system/HTTP/ResponseTest.php Co-authored-by: Michal Sniatala * Update tests/system/HTTP/ResponseTest.php Co-authored-by: Michal Sniatala * Update tests/system/HTTP/ResponseTest.php Co-authored-by: Michal Sniatala * chore(changelog): remove download optimization entry as it is a refactoring * fix: remove duplicated temporary directory creation in test --------- Co-authored-by: Michal Sniatala --- system/HTTP/ResponseTrait.php | 15 +++++-------- tests/system/HTTP/ResponseTest.php | 35 ++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/system/HTTP/ResponseTrait.php b/system/HTTP/ResponseTrait.php index 3063e44116fb..265a62fb0a1f 100644 --- a/system/HTTP/ResponseTrait.php +++ b/system/HTTP/ResponseTrait.php @@ -733,20 +733,15 @@ public function download(string $filename = '', $data = '', bool $setMime = fals return null; } - $filepath = ''; if ($data === null) { - $filepath = $filename; - $filename = explode('/', str_replace(DIRECTORY_SEPARATOR, '/', $filename)); - $filename = end($filename); + $response = new DownloadResponse(basename($filename), $setMime); + $response->setFilePath($filename); + + return $response; } $response = new DownloadResponse($filename, $setMime); - - if ($filepath !== '') { - $response->setFilePath($filepath); - } elseif ($data !== null) { - $response->setBinary($data); - } + $response->setBinary($data); return $response; } diff --git a/tests/system/HTTP/ResponseTest.php b/tests/system/HTTP/ResponseTest.php index 38c349117cbb..17d1b4191134 100644 --- a/tests/system/HTTP/ResponseTest.php +++ b/tests/system/HTTP/ResponseTest.php @@ -494,6 +494,41 @@ public function testGetDownloadResponseByFilePath(): void $this->assertSame(file_get_contents(__FILE__), $actualOutput); } + public function testGetDownloadResponseByExtremeFilePath(): void + { + $response = new Response(new App()); + + $tempDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'ci4_test_dir_' . bin2hex(random_bytes(8)); + $this->assertTrue(mkdir($tempDir)); + $extremeName = 'my_extreme_file_!@#$%.txt'; + $extremePath = $tempDir . DIRECTORY_SEPARATOR . $extremeName; + + try { + file_put_contents($extremePath, 'extreme data'); + + $actual = $response->download($extremePath, null); + + $this->assertInstanceOf(DownloadResponse::class, $actual); + $actual->buildHeaders(); + + $expectedFilename = $extremeName; + $this->assertSame( + 'attachment; filename="' . addslashes($expectedFilename) . '"; filename*=UTF-8\'\'' . rawurlencode($expectedFilename), + $actual->getHeaderLine('Content-Disposition'), + ); + + ob_start(); + $actual->sendBody(); + $actualOutput = ob_get_contents(); + ob_end_clean(); + + $this->assertSame('extreme data', $actualOutput); + } finally { + @unlink($extremePath); + @rmdir($tempDir); + } + } + public function testVagueDownload(): void { $response = new Response(new App()); From ad03579de8b7376d6cef5158ec23c141cc462fba Mon Sep 17 00:00:00 2001 From: Silva Dev BR <116388885+Will-thom@users.noreply.github.com> Date: Mon, 29 Jun 2026 04:44:49 -0300 Subject: [PATCH 44/65] fix: handle null ini values in phpini check (#10233) * Fix phpini check for null ini values * test: cover phpini null ini values * docs: add phpini null value changelog --- system/Security/CheckPhpIni.php | 4 +-- tests/system/Security/CheckPhpIniTest.php | 40 +++++++++++++++++++++ user_guide_src/source/changelogs/v4.7.4.rst | 1 + 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/system/Security/CheckPhpIni.php b/system/Security/CheckPhpIni.php index 949c1d10aa48..d1ab3dfccda0 100644 --- a/system/Security/CheckPhpIni.php +++ b/system/Security/CheckPhpIni.php @@ -182,8 +182,8 @@ private static function checkIni(?string $argument = null): array foreach ($items as $key => $values) { $hasKeyInIni = array_key_exists($key, $ini); $output[$key] = [ - 'global' => $hasKeyInIni ? $ini[$key]['global_value'] : 'disabled', - 'current' => $hasKeyInIni ? $ini[$key]['local_value'] : 'disabled', + 'global' => $hasKeyInIni ? (string) ($ini[$key]['global_value'] ?? '') : 'disabled', + 'current' => $hasKeyInIni ? (string) ($ini[$key]['local_value'] ?? '') : 'disabled', 'recommended' => $values['recommended'] ?? '', 'remark' => $values['remark'] ?? '', ]; diff --git a/tests/system/Security/CheckPhpIniTest.php b/tests/system/Security/CheckPhpIniTest.php index 939f69e4191a..272a06298240 100644 --- a/tests/system/Security/CheckPhpIniTest.php +++ b/tests/system/Security/CheckPhpIniTest.php @@ -17,6 +17,14 @@ use CodeIgniter\Test\StreamFilterTrait; use PHPUnit\Framework\Attributes\Group; +/** + * @return array>|false + */ +function ini_get_all(?string $extension = null, bool $details = true): array|false +{ + return CheckPhpIniTest::$iniGetAllReturn ?? \ini_get_all($extension, $details); +} + /** * @internal */ @@ -25,6 +33,18 @@ final class CheckPhpIniTest extends CIUnitTestCase { use StreamFilterTrait; + /** + * @var array>|null + */ + public static ?array $iniGetAllReturn = null; + + protected function tearDown(): void + { + parent::tearDown(); + + self::$iniGetAllReturn = null; + } + public function testCheckIni(): void { $output = self::getPrivateMethodInvoker(CheckPhpIni::class, 'checkIni')(); @@ -51,6 +71,26 @@ public function testCheckIniOpcache(): void $this->assertSame($expected, $output['opcache.save_comments']); } + public function testCheckIniCastsNullIniValuesToString(): void + { + self::$iniGetAllReturn = [ + 'default_charset' => [ + 'global_value' => null, + 'local_value' => null, + ], + ]; + + $output = self::getPrivateMethodInvoker(CheckPhpIni::class, 'checkIni')(); + + $expected = [ + 'global' => '', + 'current' => '', + 'recommended' => 'UTF-8', + 'remark' => '', + ]; + $this->assertSame($expected, $output['default_charset']); + } + public function testRunCli(): void { CheckPhpIni::run(true); diff --git a/user_guide_src/source/changelogs/v4.7.4.rst b/user_guide_src/source/changelogs/v4.7.4.rst index 7aba967ec02a..10dbc27cdeff 100644 --- a/user_guide_src/source/changelogs/v4.7.4.rst +++ b/user_guide_src/source/changelogs/v4.7.4.rst @@ -41,6 +41,7 @@ Bugs Fixed - **Filters:** Fixed a bug in ``InvalidChars`` filter where invalid UTF-8 or control characters in array keys were not checked. - **HTTP:** Fixed a bug where the User Agent library reported Safari's WebKit version instead of the browser version from the ``Version`` token. - **Model:** Fixed a bug in ``Model::objectToRawArray()`` where the ``$recursive`` parameter was ignored. +- **Security:** Fixed a bug where ``CheckPhpIni`` could raise a type error when ``ini_get_all()`` returned ``null`` for a configured directive value. - **Session:** Fixed a bug in ``RedisHandler`` where the configured ``$lockMaxRetries`` and ``$lockRetryInterval`` values were not respected when acquiring session locks. - **Testing:** Fixed a bug where using ``MockInputOutput`` within a test that also uses ``StreamFilterTrait`` tore down the trait's stream filters, so CLI output produced after the ``MockInputOutput`` interaction (such as in ``tearDown()``) was no longer captured and leaked to the console. - **Validation:** Fixed bugs in the ``required_without`` rule logic where using array dot notation caused early exits ignoring subsequent fields and triggered an ``Undefined array key`` warning for missing keys. From 4ba835d183f09099d57ff7ebd9b1e4697807121c Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Mon, 29 Jun 2026 15:27:21 +0200 Subject: [PATCH 45/65] refactor(database): optimize groupGetType by caching it inside BaseBuilder loops (#10340) --- system/Database/BaseBuilder.php | 20 +++- tests/system/Database/Builder/LikeTest.php | 111 +++++++++++++++++++ utils/phpstan-baseline/empty.notAllowed.neon | 2 +- 3 files changed, 128 insertions(+), 5 deletions(-) diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index 0a27b0b33abc..df2fa411ca36 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -764,14 +764,18 @@ protected function whereHaving(string $qbKey, $key, $value = null, string $type $keyValue = $key; } + if ($keyValue === []) { + return $this; + } + // If the escape value was not set will base it on the global setting if (! is_bool($escape)) { $escape = $this->db->protectIdentifiers; } - foreach ($keyValue as $k => $v) { - $prefix = empty($this->{$qbKey}) ? $this->groupGetType('') : $this->groupGetType($type); + $prefix = empty($this->{$qbKey}) ? $this->groupGetType('') : $this->groupGetType($type); + foreach ($keyValue as $k => $v) { if ($rawSqlOnly) { $k = ''; $op = ''; @@ -830,6 +834,8 @@ protected function whereHaving(string $qbKey, $key, $value = null, string $type 'escape' => $escape, ]; } + + $prefix = $type; } return $this; @@ -1152,13 +1158,17 @@ protected function _like($field, string $match = '', string $type = 'AND ', stri $keyValue = is_array($field) ? $field : [$field => $match]; + if ($keyValue === []) { + return $this; + } + + $prefix = $this->{$clause} === [] ? $this->groupGetType('') : $this->groupGetType($type); + foreach ($keyValue as $k => $v) { if ($insensitiveSearch) { $v = mb_strtolower($v, 'UTF-8'); } - $prefix = empty($this->{$clause}) ? $this->groupGetType('') : $this->groupGetType($type); - if ($side === 'none') { $bind = $this->setBind($k, $v, $escape); } elseif ($side === 'before') { @@ -1180,6 +1190,8 @@ protected function _like($field, string $match = '', string $type = 'AND ', stri 'condition' => $likeStatement, 'escape' => $escape, ]; + + $prefix = $type; } return $this; diff --git a/tests/system/Database/Builder/LikeTest.php b/tests/system/Database/Builder/LikeTest.php index d2a534b5bf27..f9d7cd91f65e 100644 --- a/tests/system/Database/Builder/LikeTest.php +++ b/tests/system/Database/Builder/LikeTest.php @@ -17,6 +17,7 @@ use CodeIgniter\Database\RawSql; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Mock\MockConnection; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; /** @@ -231,4 +232,114 @@ public function testDBPrefixAndCoulmnWithTablename(): void $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); $this->assertSame($expectedBinds, $builder->getBinds()); } + + public function testLikeMultipleFields(): void + { + $builder = new BaseBuilder('job', $this->db); + + $builder->like([ + 'name' => 'veloper', + 'title' => 'dev', + ]); + + $expectedSQL = "SELECT * FROM \"job\" WHERE \"name\" LIKE '%veloper%' ESCAPE '!' AND \"title\" LIKE '%dev%' ESCAPE '!'"; + $expectedBinds = [ + 'name' => [ + '%veloper%', + true, + ], + 'title' => [ + '%dev%', + true, + ], + ]; + + $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); + $this->assertSame($expectedBinds, $builder->getBinds()); + } + + public function testLikeMultipleCallsWithRawSqlAndString(): void + { + $builder = new BaseBuilder('users', $this->db); + + $sql = "concat(users.name, ' ', users.surname)"; + $rawSql = new RawSql($sql); + + $builder->like($rawSql, 'value')->like('name', 'veloper'); + + $expectedSQL = "SELECT * FROM \"users\" WHERE {$sql} LIKE '%value%' ESCAPE '!' AND \"name\" LIKE '%veloper%' ESCAPE '!'"; + $expectedBinds = [ + $rawSql->getBindingKey() => [ + '%value%', + true, + ], + 'name' => [ + '%veloper%', + true, + ], + ]; + + $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); + $this->assertSame($expectedBinds, $builder->getBinds()); + } + + public function testOrLikeMultipleFields(): void + { + $builder = new BaseBuilder('job', $this->db); + + $builder->orLike([ + 'name' => 'veloper', + 'title' => 'dev', + ]); + + $expectedSQL = "SELECT * FROM \"job\" WHERE \"name\" LIKE '%veloper%' ESCAPE '!' OR \"title\" LIKE '%dev%' ESCAPE '!'"; + $expectedBinds = [ + 'name' => [ + '%veloper%', + true, + ], + 'title' => [ + '%dev%', + true, + ], + ]; + + $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); + $this->assertSame($expectedBinds, $builder->getBinds()); + } + + #[DataProvider('provideLikeMethodsWithEmptyArray')] + public function testLikeMethodsWithEmptyArray(string $method): void + { + $builder = new BaseBuilder('job', $this->db); + + $builder->groupStart() + ->{$method}([]) + ->where('id', 1) + ->groupEnd(); + + $expectedSQL = 'SELECT * FROM "job" WHERE ( "id" = 1 )'; + $expectedBinds = [ + 'id' => [ + 1, + true, + ], + ]; + + $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); + $this->assertSame($expectedBinds, $builder->getBinds()); + } + + /** + * @return array + */ + public static function provideLikeMethodsWithEmptyArray(): iterable + { + return [ + 'like' => ['like'], + 'orLike' => ['orLike'], + 'notLike' => ['notLike'], + 'orNotLike' => ['orNotLike'], + ]; + } } diff --git a/utils/phpstan-baseline/empty.notAllowed.neon b/utils/phpstan-baseline/empty.notAllowed.neon index 0f92e89f912e..e526a441d344 100644 --- a/utils/phpstan-baseline/empty.notAllowed.neon +++ b/utils/phpstan-baseline/empty.notAllowed.neon @@ -34,7 +34,7 @@ parameters: - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 28 + count: 27 path: ../../system/Database/BaseBuilder.php - From e3e2218d069568078d57b8746650ee7836d9b4f8 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Mon, 29 Jun 2026 22:17:12 +0800 Subject: [PATCH 46/65] refactor: bump to phpstan-codeigniter v2.1 (#10312) --- .gitattributes | 2 +- .github/workflows/test-phpstan.yml | 4 +- phpstan-bootstrap.php | 4 + phpstan.neon.dist => phpstan.dist.neon | 2 +- rector.php | 2 +- system/BaseModel.php | 2 +- system/Boot.php | 6 +- system/CLI/CLI.php | 6 +- system/Commands/Encryption/GenerateKey.php | 3 +- system/Common.php | 4 +- system/Config/AutoloadConfig.php | 3 +- system/Config/BaseConfig.php | 8 +- system/Session/Session.php | 11 +- system/Test/FeatureTestTrait.php | 6 +- system/Test/Mock/MockSecurity.php | 2 +- system/util_bootstrap.php | 11 +- tests/system/CommonFunctionsSendTest.php | 3 +- tests/system/CommonFunctionsTest.php | 3 +- tests/system/Config/BaseConfigTest.php | 5 +- tests/system/Config/DotEnvTest.php | 16 +- tests/system/Config/ServicesTest.php | 2 +- tests/system/ControllerTest.php | 4 +- tests/system/Debug/ExceptionsTest.php | 4 +- tests/system/Filters/FiltersTest.php | 2 +- tests/system/HTTP/IncomingRequestTest.php | 8 +- .../SiteURIFactoryDetectRoutePathTest.php | 4 +- tests/system/Helpers/CookieHelperTest.php | 2 +- tests/system/Helpers/FormHelperTest.php | 2 +- .../Helpers/URLHelper/SiteUrlCliTest.php | 2 +- .../system/Helpers/URLHelper/SiteUrlTest.php | 2 +- .../system/Models/DataConverterModelTest.php | 7 +- tests/system/Models/FindModelTest.php | 30 ++-- tests/system/Models/InsertModelTest.php | 30 ++-- .../system/Models/MiscellaneousModelTest.php | 7 +- tests/system/Models/SaveModelTest.php | 6 +- tests/system/Models/TimestampModelTest.php | 11 +- tests/system/Models/UpdateModelTest.php | 18 +-- tests/system/Models/WhenWhenNotModelTest.php | 20 ++- tests/system/SuperglobalsTest.php | 20 +-- .../source/installation/upgrade_412.rst | 3 - utils/phpstan-baseline/argument.type.neon | 7 +- .../codeigniter.superglobalsGlobalAssign.neon | 83 ---------- .../codeigniter.superglobalsOffsetAccess.neon | 148 ------------------ .../codeigniter.superglobalsOffsetAssign.neon | 28 ---- .../codeigniter.superglobalsOffsetUnset.neon | 33 ---- utils/phpstan-baseline/loader.neon | 6 +- .../phpstan-baseline/property.nonObject.neon | 57 +------ 47 files changed, 150 insertions(+), 499 deletions(-) rename phpstan.neon.dist => phpstan.dist.neon (95%) delete mode 100644 utils/phpstan-baseline/codeigniter.superglobalsGlobalAssign.neon delete mode 100644 utils/phpstan-baseline/codeigniter.superglobalsOffsetAccess.neon delete mode 100644 utils/phpstan-baseline/codeigniter.superglobalsOffsetAssign.neon delete mode 100644 utils/phpstan-baseline/codeigniter.superglobalsOffsetUnset.neon diff --git a/.gitattributes b/.gitattributes index b1d0822c10d5..05fc1f38cde9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -26,7 +26,7 @@ structarmed.php export-ignore phpmetrics.json export-ignore phpstan-baseline.php export-ignore phpstan-bootstrap.php export-ignore -phpstan.neon.dist export-ignore +phpstan.dist.neon export-ignore phpunit.dist.xml export-ignore psalm-baseline.xml export-ignore psalm.xml export-ignore diff --git a/.github/workflows/test-phpstan.yml b/.github/workflows/test-phpstan.yml index 3eebbfe010d8..02c844fba724 100644 --- a/.github/workflows/test-phpstan.yml +++ b/.github/workflows/test-phpstan.yml @@ -13,7 +13,7 @@ on: - 'tests/**.php' - 'utils/**.php' - composer.json - - phpstan.neon.dist + - phpstan.dist.neon - phpstan-baseline.php - '.github/workflows/test-phpstan.yml' @@ -27,7 +27,7 @@ on: - 'tests/**.php' - 'utils/**.php' - composer.json - - phpstan.neon.dist + - phpstan.dist.neon - phpstan-baseline.php - '.github/workflows/test-phpstan.yml' diff --git a/phpstan-bootstrap.php b/phpstan-bootstrap.php index 0f45bf0cb559..ef64b6c23a52 100644 --- a/phpstan-bootstrap.php +++ b/phpstan-bootstrap.php @@ -1,5 +1,9 @@ withPHPStanConfigs([ + __DIR__ . '/phpstan.dist.neon', __DIR__ . '/vendor/codeigniter/phpstan-codeigniter/extension.neon', __DIR__ . '/vendor/phpstan/phpstan-strict-rules/rules.neon', __DIR__ . '/vendor/shipmonk/phpstan-baseline-per-identifier/extension.neon', - __DIR__ . '/phpstan.neon.dist', ]) // is there a file you need to skip? ->withSkip([ diff --git a/system/BaseModel.php b/system/BaseModel.php index d40a662724d9..6f52c02a48c6 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -49,7 +49,7 @@ * - process various callbacks * - allow intermingling calls to the db connection * - * @phpstan-type row_array array + * @phpstan-type row_array array * @phpstan-type event_data_beforeinsert array{data: row_array} * @phpstan-type event_data_afterinsert array{id: int|string, data: row_array, result: bool} * @phpstan-type event_data_beforefind array{id?: int|string, method: string, singleton: bool, limit?: int, offset?: int} diff --git a/system/Boot.php b/system/Boot.php index d3b25895093b..a1b8c9695004 100644 --- a/system/Boot.php +++ b/system/Boot.php @@ -426,9 +426,11 @@ protected static function initializeConsole(): Console { $console = new Console(); + $args = $_SERVER['argv'] ?? []; // @phpstan-ignore codeigniter.superglobalsOffsetAccess (reads live $_SERVER, not the snapshot service) + // Show basic information before we do anything else. - if (is_int($suppress = array_search('--no-header', $_SERVER['argv'], true))) { - unset($_SERVER['argv'][$suppress]); + if (is_int($suppress = array_search('--no-header', $args, true))) { + unset($args[$suppress]); $suppress = true; } diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index d26681213914..3996f31ee349 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -707,7 +707,7 @@ public static function streamSupports(string $function, $resource): bool public static function hasColorSupport($resource): bool { // Follow https://no-color.org/ - if (isset($_SERVER['NO_COLOR']) || getenv('NO_COLOR') !== false) { + if (isset($_SERVER['NO_COLOR']) || getenv('NO_COLOR') !== false) { // @phpstan-ignore codeigniter.superglobalsOffsetAccess (reads live $_SERVER, not the snapshot service) return false; } @@ -718,7 +718,7 @@ public static function hasColorSupport($resource): bool if (is_windows()) { // @codeCoverageIgnoreStart return static::streamSupports('sapi_windows_vt100_support', $resource) - || isset($_SERVER['ANSICON']) + || isset($_SERVER['ANSICON']) // @phpstan-ignore codeigniter.superglobalsOffsetAccess (reads live $_SERVER, not the snapshot service) || getenv('ANSICON') !== false || getenv('ConEmuANSI') === 'ON' || getenv('TERM') === 'xterm'; @@ -887,7 +887,7 @@ public static function wrap(?string $string = null, int $max = 0, int $padLeft = */ protected static function parseCommandLine() { - $args = $_SERVER['argv'] ?? []; + $args = $_SERVER['argv'] ?? []; // @phpstan-ignore codeigniter.superglobalsOffsetAccess (reads live $_SERVER, not the snapshot service) array_shift($args); // scrap invoking program $optionValue = false; diff --git a/system/Commands/Encryption/GenerateKey.php b/system/Commands/Encryption/GenerateKey.php index 3726360fa8ad..148fd264dc33 100644 --- a/system/Commands/Encryption/GenerateKey.php +++ b/system/Commands/Encryption/GenerateKey.php @@ -101,7 +101,8 @@ public function run(array $params) // force DotEnv to reload the new env vars putenv('encryption.key'); - unset($_ENV['encryption.key'], $_SERVER['encryption.key']); + unset($_ENV['encryption.key']); + service('superglobals')->unsetServer('encryption.key'); $dotenv = new DotEnv((new Paths())->envDirectory ?? ROOTPATH); // @phpstan-ignore nullCoalesce.property $dotenv->load(); diff --git a/system/Common.php b/system/Common.php index f3f20a7a0db5..c0bab0fd3acd 100644 --- a/system/Common.php +++ b/system/Common.php @@ -409,7 +409,7 @@ function db_connect($db = null, bool $getShared = true) */ function env(string $key, $default = null) { - $value = $_ENV[$key] ?? $_SERVER[$key] ?? getenv($key); + $value = $_ENV[$key] ?? $_SERVER[$key] ?? getenv($key); // @phpstan-ignore codeigniter.superglobalsOffsetAccess (reads live $_SERVER, not the snapshot service) // Not found? Return the default value if ($value === false) { @@ -697,7 +697,7 @@ function is_cli(): bool // PHP_SAPI could be 'cgi-fcgi', 'fpm-fcgi'. // See https://github.com/codeigniter4/CodeIgniter4/pull/5393 - return ! isset($_SERVER['REMOTE_ADDR']) && ! isset($_SERVER['REQUEST_METHOD']); + return ! isset($_SERVER['REMOTE_ADDR']) && ! isset($_SERVER['REQUEST_METHOD']); // @phpstan-ignore codeigniter.superglobalsOffsetAccess (reads live $_SERVER, not the snapshot service), codeigniter.superglobalsOffsetAccess (reads live $_SERVER, not the snapshot service) } } diff --git a/system/Config/AutoloadConfig.php b/system/Config/AutoloadConfig.php index b6d7f72bdd54..f35df4ebe740 100644 --- a/system/Config/AutoloadConfig.php +++ b/system/Config/AutoloadConfig.php @@ -142,7 +142,8 @@ class AutoloadConfig */ public function __construct() { - if (isset($_SERVER['CI_ENVIRONMENT']) && $_SERVER['CI_ENVIRONMENT'] === 'testing') { + // @phpstan-ignore codeigniter.superglobalsOffsetAccess (service() is not yet available) + if (($_SERVER['CI_ENVIRONMENT'] ?? null) === 'testing') { $this->psr4['Tests\Support'] = SUPPORTPATH; $this->classmap['CodeIgniter\Log\TestLogger'] = SYSTEMPATH . 'Test/TestLogger.php'; $this->classmap['CIDatabaseTestCase'] = SYSTEMPATH . 'Test/CIDatabaseTestCase.php'; diff --git a/system/Config/BaseConfig.php b/system/Config/BaseConfig.php index 42da45cdb52f..24c1ce019c08 100644 --- a/system/Config/BaseConfig.php +++ b/system/Config/BaseConfig.php @@ -221,10 +221,10 @@ protected function getEnvValue(string $property, string $prefix, string $shortPr return $_ENV["{$shortPrefix}_{$underscoreProperty}"]; case array_key_exists("{$shortPrefix}.{$property}", $_SERVER): - return $_SERVER["{$shortPrefix}.{$property}"]; + return $_SERVER["{$shortPrefix}.{$property}"]; // @phpstan-ignore codeigniter.superglobalsOffsetAccess (reads live $_SERVER, not the snapshot service) case array_key_exists("{$shortPrefix}_{$underscoreProperty}", $_SERVER): - return $_SERVER["{$shortPrefix}_{$underscoreProperty}"]; + return $_SERVER["{$shortPrefix}_{$underscoreProperty}"]; // @phpstan-ignore codeigniter.superglobalsOffsetAccess (reads live $_SERVER, not the snapshot service) case array_key_exists("{$prefix}.{$property}", $_ENV): return $_ENV["{$prefix}.{$property}"]; @@ -233,10 +233,10 @@ protected function getEnvValue(string $property, string $prefix, string $shortPr return $_ENV["{$prefix}_{$underscoreProperty}"]; case array_key_exists("{$prefix}.{$property}", $_SERVER): - return $_SERVER["{$prefix}.{$property}"]; + return $_SERVER["{$prefix}.{$property}"]; // @phpstan-ignore codeigniter.superglobalsOffsetAccess (reads live $_SERVER, not the snapshot service) case array_key_exists("{$prefix}_{$underscoreProperty}", $_SERVER): - return $_SERVER["{$prefix}_{$underscoreProperty}"]; + return $_SERVER["{$prefix}_{$underscoreProperty}"]; // @phpstan-ignore codeigniter.superglobalsOffsetAccess (reads live $_SERVER, not the snapshot service) default: $value = getenv("{$shortPrefix}.{$property}"); diff --git a/system/Session/Session.php b/system/Session/Session.php index 1c3824928775..1998a0ea8596 100644 --- a/system/Session/Session.php +++ b/system/Session/Session.php @@ -110,11 +110,10 @@ public function start() $this->setSaveHandler(); // Sanitize the cookie, because apparently PHP doesn't do that for userspace handlers - if ( - isset($_COOKIE[$this->config->cookieName]) - && (! is_string($_COOKIE[$this->config->cookieName]) || preg_match('#\A' . $this->sidRegexp . '\z#', $_COOKIE[$this->config->cookieName]) !== 1) - ) { - unset($_COOKIE[$this->config->cookieName]); + $cookie = service('superglobals')->cookie($this->config->cookieName); + + if (isset($cookie) && (! is_string($cookie) || preg_match('#\A' . $this->sidRegexp . '\z#', $cookie) !== 1)) { + service('superglobals')->unsetCookie($this->config->cookieName); } $this->startSession(); @@ -132,7 +131,7 @@ public function start() } // Another work-around ... PHP doesn't seem to send the session cookie // unless it is being currently created or regenerated - elseif (isset($_COOKIE[$this->config->cookieName]) && $_COOKIE[$this->config->cookieName] === session_id()) { + elseif (isset($cookie) && $cookie === session_id()) { $this->setCookie(); } diff --git a/system/Test/FeatureTestTrait.php b/system/Test/FeatureTestTrait.php index a271191a2271..087fc0a59d90 100644 --- a/system/Test/FeatureTestTrait.php +++ b/system/Test/FeatureTestTrait.php @@ -350,9 +350,9 @@ protected function setupRequest(string $method, ?string $path = null): IncomingR $request->setProtocolVersion('1.1'); if ($config->forceGlobalSecureRequests) { - $_SERVER['HTTPS'] = 'test'; - $server = $request->getServer(); - $server['HTTPS'] = 'test'; + service('superglobals')->setServer('HTTPS', 'test'); + $server = $request->getServer(); + $server['HTTPS'] = 'test'; $request->setGlobal('server', $server); } diff --git a/system/Test/Mock/MockSecurity.php b/system/Test/Mock/MockSecurity.php index 89d1eab0a701..072dc548dbe6 100644 --- a/system/Test/Mock/MockSecurity.php +++ b/system/Test/Mock/MockSecurity.php @@ -19,7 +19,7 @@ class MockSecurity extends Security { protected function doSendCookie(): void { - $_COOKIE['csrf_cookie_name'] = $this->hash; + service('superglobals')->setCookie('csrf_cookie_name', $this->hash); } protected function randomize(string $hash): string diff --git a/system/util_bootstrap.php b/system/util_bootstrap.php index 5423954deb59..135208a78ea9 100644 --- a/system/util_bootstrap.php +++ b/system/util_bootstrap.php @@ -23,14 +23,13 @@ * DEFINE ENVIRONMENT * --------------------------------------------------------------- * - * As this bootstrap file is primarily used by internal scripts - * across the framework and other CodeIgniter projects, we need - * to make sure it recognizes that we're in development. + * The environment defaults to development. A caller may set + * $_SERVER['CI_ENVIRONMENT'] before including this file to override it. */ -$_SERVER['CI_ENVIRONMENT'] = 'development'; -define('ENVIRONMENT', 'development'); -defined('CI_DEBUG') || define('CI_DEBUG', true); +$_SERVER['CI_ENVIRONMENT'] ??= 'development'; +defined('ENVIRONMENT') || define('ENVIRONMENT', $_SERVER['CI_ENVIRONMENT']); +defined('CI_DEBUG') || define('CI_DEBUG', true); /* * --------------------------------------------------------------- diff --git a/tests/system/CommonFunctionsSendTest.php b/tests/system/CommonFunctionsSendTest.php index e1f3f9dff961..66e285d26f84 100644 --- a/tests/system/CommonFunctionsSendTest.php +++ b/tests/system/CommonFunctionsSendTest.php @@ -30,7 +30,8 @@ protected function setUp(): void { parent::setUp(); - unset($_ENV['foo'], $_SERVER['foo']); + unset($_ENV['foo']); + service('superglobals')->unsetServer('foo'); } /** diff --git a/tests/system/CommonFunctionsTest.php b/tests/system/CommonFunctionsTest.php index 30f99d79a515..83750c220bc4 100644 --- a/tests/system/CommonFunctionsTest.php +++ b/tests/system/CommonFunctionsTest.php @@ -65,7 +65,8 @@ final class CommonFunctionsTest extends CIUnitTestCase protected function setUp(): void { - unset($_ENV['foo'], $_SERVER['foo']); + unset($_ENV['foo']); + service('superglobals')->unsetServer('foo'); $this->resetServices(); parent::setUp(); diff --git a/tests/system/Config/BaseConfigTest.php b/tests/system/Config/BaseConfigTest.php index 1d34ce30535a..24d259feef26 100644 --- a/tests/system/Config/BaseConfigTest.php +++ b/tests/system/Config/BaseConfigTest.php @@ -125,10 +125,7 @@ public function testUseDefaultValueTypeStringValue(): void #[RunInSeparateProcess] public function testServerValues(): void { - $_SERVER = [ - 'simpleconfig.shortie' => 123, - 'SimpleConfig.longie' => 456, - ]; + service('superglobals')->setServerArray(['simpleconfig.shortie' => 123, 'SimpleConfig.longie' => 456]); $dotenv = new DotEnv($this->fixturesFolder, '.env'); $dotenv->load(); $config = new SimpleConfig(); diff --git a/tests/system/Config/DotEnvTest.php b/tests/system/Config/DotEnvTest.php index 67a0e05fd18b..c789169b1b87 100644 --- a/tests/system/Config/DotEnvTest.php +++ b/tests/system/Config/DotEnvTest.php @@ -93,7 +93,8 @@ public static function provideLoadsVars(): iterable public function testLoadsHex2Bin(): void { putenv('encryption.key'); - unset($_ENV['encryption.key'], $_SERVER['encryption.key']); + unset($_ENV['encryption.key']); + service('superglobals')->unsetServer('encryption.key'); $dotenv = new DotEnv($this->fixturesFolder, 'encryption.env'); $dotenv->load(); @@ -108,7 +109,8 @@ public function testLoadsHex2Bin(): void public function testLoadsBase64(): void { putenv('encryption.key'); - unset($_ENV['encryption.key'], $_SERVER['encryption.key']); + unset($_ENV['encryption.key']); + service('superglobals')->unsetServer('encryption.key'); $dotenv = new DotEnv($this->fixturesFolder, 'base64encryption.env'); $dotenv->load(); @@ -167,10 +169,10 @@ public function testLoadsServerGlobals(): void $dotenv = new DotEnv($this->fixturesFolder, '.env'); $dotenv->load(); - $this->assertSame('bar', $_SERVER['FOO']); - $this->assertSame('baz', $_SERVER['BAR']); - $this->assertSame('with spaces', $_SERVER['SPACED']); - $this->assertSame('', $_SERVER['NULL']); + $this->assertSame('bar', $_SERVER['FOO']); // @phpstan-ignore codeigniter.superglobalsOffsetAccess (checks the live superglobal, not the snapshot service) + $this->assertSame('baz', $_SERVER['BAR']); // @phpstan-ignore codeigniter.superglobalsOffsetAccess (checks the live superglobal, not the snapshot service) + $this->assertSame('with spaces', $_SERVER['SPACED']); // @phpstan-ignore codeigniter.superglobalsOffsetAccess (checks the live superglobal, not the snapshot service) + $this->assertSame('', $_SERVER['NULL']); // @phpstan-ignore codeigniter.superglobalsOffsetAccess (checks the live superglobal, not the snapshot service) } public function testNamespacedVariables(): void @@ -178,7 +180,7 @@ public function testNamespacedVariables(): void $dotenv = new DotEnv($this->fixturesFolder, '.env'); $dotenv->load(); - $this->assertSame('complex', $_SERVER['SimpleConfig_simple_name']); + $this->assertSame('complex', $_SERVER['SimpleConfig_simple_name']); // @phpstan-ignore codeigniter.superglobalsOffsetAccess (checks the live superglobal, not the snapshot service) } public function testLoadsGetServerVar(): void diff --git a/tests/system/Config/ServicesTest.php b/tests/system/Config/ServicesTest.php index 7eef9818a727..86228dfadd3f 100644 --- a/tests/system/Config/ServicesTest.php +++ b/tests/system/Config/ServicesTest.php @@ -78,7 +78,7 @@ protected function setUp(): void protected function tearDown(): void { - $_SERVER = $this->original; + service('superglobals')->setServerArray($this->original); $this->resetServices(); } diff --git a/tests/system/ControllerTest.php b/tests/system/ControllerTest.php index 113b02c14deb..2ca26f9aa15c 100644 --- a/tests/system/ControllerTest.php +++ b/tests/system/ControllerTest.php @@ -74,7 +74,7 @@ public function testConstructor(): void public function testConstructorHTTPS(): void { $original = $_SERVER; - $_SERVER = ['HTTPS' => 'on']; + service('superglobals')->setServerArray(['HTTPS' => 'on']); // make sure we can instantiate one try { @@ -86,7 +86,7 @@ public function testConstructorHTTPS(): void } $this->assertInstanceOf(Controller::class, $this->controller); - $_SERVER = $original; // restore so code coverage doesn't break + service('superglobals')->setServerArray($original); // restore so code coverage doesn't break } public function testCachePageSetsTtl(): void diff --git a/tests/system/Debug/ExceptionsTest.php b/tests/system/Debug/ExceptionsTest.php index 415b6772c509..919c1ea85180 100644 --- a/tests/system/Debug/ExceptionsTest.php +++ b/tests/system/Debug/ExceptionsTest.php @@ -37,14 +37,14 @@ public static function setUpBeforeClass(): void { parent::setUpBeforeClass(); - unset($_SERVER['CODEIGNITER_SCREAM_DEPRECATIONS']); + service('superglobals')->unsetServer('CODEIGNITER_SCREAM_DEPRECATIONS'); } public static function tearDownAfterClass(): void { parent::tearDownAfterClass(); - $_SERVER['CODEIGNITER_SCREAM_DEPRECATIONS'] = '1'; + service('superglobals')->setServer('CODEIGNITER_SCREAM_DEPRECATIONS', '1'); } protected function setUp(): void diff --git a/tests/system/Filters/FiltersTest.php b/tests/system/Filters/FiltersTest.php index cdf57c8b9211..b632df217043 100644 --- a/tests/system/Filters/FiltersTest.php +++ b/tests/system/Filters/FiltersTest.php @@ -68,7 +68,7 @@ protected function setUp(): void ]; service('autoloader')->addNamespace($defaults); - $_SERVER = []; + service('superglobals')->setServerArray([]); Services::injectMock('superglobals', new Superglobals()); $this->response = service('response'); diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index 509cf69643d2..5a7d5f29dfe2 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -45,7 +45,13 @@ protected function setUp(): void { parent::setUp(); - $_POST = $_GET = $_SERVER = $_REQUEST = $_ENV = $_COOKIE = $_SESSION = []; + $_ENV = $_SESSION = []; + service('superglobals') + ->setPostArray([]) + ->setGetArray([]) + ->setServerArray([]) + ->setRequestArray([]) + ->setCookieArray([]); Services::injectMock('superglobals', new Superglobals()); $config = new App(); diff --git a/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php b/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php index 2a0e485775d4..b864e493bb5f 100644 --- a/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php +++ b/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php @@ -32,7 +32,7 @@ protected function setUp(): void { parent::setUp(); - $_GET = $_SERVER = []; + service('superglobals')->setGetArray([])->setServerArray([]); Services::injectMock('superglobals', new Superglobals()); } @@ -251,7 +251,7 @@ public function testQueryStringWithQueryString(): void $expected = 'ci/woot'; $this->assertSame($expected, $factory->detectRoutePath('QUERY_STRING')); - $this->assertSame('code=good', $_SERVER['QUERY_STRING']); + $this->assertSame('code=good', $_SERVER['QUERY_STRING']); // @phpstan-ignore codeigniter.superglobalsOffsetAccess (checks the live superglobal, not the snapshot service) $this->assertSame(['code' => 'good'], $_GET); } diff --git a/tests/system/Helpers/CookieHelperTest.php b/tests/system/Helpers/CookieHelperTest.php index abbc291ceb38..9b79e8d61dbc 100644 --- a/tests/system/Helpers/CookieHelperTest.php +++ b/tests/system/Helpers/CookieHelperTest.php @@ -41,7 +41,7 @@ final class CookieHelperTest extends CIUnitTestCase protected function setUp(): void { - $_COOKIE = []; + service('superglobals')->setCookieArray([]); parent::setUp(); diff --git a/tests/system/Helpers/FormHelperTest.php b/tests/system/Helpers/FormHelperTest.php index 04c35458cd01..73cbe2430a5f 100644 --- a/tests/system/Helpers/FormHelperTest.php +++ b/tests/system/Helpers/FormHelperTest.php @@ -41,7 +41,7 @@ protected function setUp(): void parent::setUp(); - $_POST = $_GET = []; + service('superglobals')->setPostArray([])->setGetArray([]); CodeIgniterServices::injectMock('superglobals', new Superglobals()); diff --git a/tests/system/Helpers/URLHelper/SiteUrlCliTest.php b/tests/system/Helpers/URLHelper/SiteUrlCliTest.php index e24f55f44605..ad99f46a8b2a 100644 --- a/tests/system/Helpers/URLHelper/SiteUrlCliTest.php +++ b/tests/system/Helpers/URLHelper/SiteUrlCliTest.php @@ -48,7 +48,7 @@ protected function tearDown(): void { parent::tearDown(); - $_SERVER = []; + service('superglobals')->setServerArray([]); } private function createRequest(?App $config = null): void diff --git a/tests/system/Helpers/URLHelper/SiteUrlTest.php b/tests/system/Helpers/URLHelper/SiteUrlTest.php index 791515a09fab..c8a07758e3bd 100644 --- a/tests/system/Helpers/URLHelper/SiteUrlTest.php +++ b/tests/system/Helpers/URLHelper/SiteUrlTest.php @@ -52,7 +52,7 @@ protected function tearDown(): void { parent::tearDown(); - $_SERVER = []; + service('superglobals')->setServerArray([]); } private function createRequest(?App $config = null, $body = null, ?string $path = null): void diff --git a/tests/system/Models/DataConverterModelTest.php b/tests/system/Models/DataConverterModelTest.php index eb9a36c4668d..f26f03d999b9 100644 --- a/tests/system/Models/DataConverterModelTest.php +++ b/tests/system/Models/DataConverterModelTest.php @@ -20,6 +20,7 @@ use Tests\Support\Models\UserCastsTimestampModel; /** + * @property-read UserCastsTimestampModel $model * @internal */ #[Group('DatabaseLive')] @@ -335,8 +336,7 @@ public function testUpdateCustomObject(): void public function testUpdateEntity(): void { - $id = $this->prepareOneRecord(); - /** @var User $user */ + $id = $this->prepareOneRecord(); $user = $this->model->asObject(User::class)->find($id); $email = $user->email; @@ -403,8 +403,7 @@ public function testSaveCustomObject(): void public function testSaveEntity(): void { - $id = $this->prepareOneRecord(); - /** @var User $user */ + $id = $this->prepareOneRecord(); $user = $this->model->asObject(User::class)->find($id); $email = $user->email; diff --git a/tests/system/Models/FindModelTest.php b/tests/system/Models/FindModelTest.php index 71a085dc3950..b5efc8a64f64 100644 --- a/tests/system/Models/FindModelTest.php +++ b/tests/system/Models/FindModelTest.php @@ -226,27 +226,27 @@ public function testFirstRespectsSoftDeletes($aggregate, $groupBy): void ->where('id', 1) ->update(['deleted_at' => date('Y-m-d H:i:s')]); - $this->createModel(UserModel::class); + $model = $this->createModel(UserModel::class); if ($aggregate) { $ANSISQLDriverNames = ['OCI8']; if (in_array($this->db->DBDriver, $ANSISQLDriverNames, true)) { - $this->model->select('SUM("id") as "id"'); + $model->select('SUM("id") as "id"'); } else { - $this->model->select('SUM(id) as id'); + $model->select('SUM(id) as id'); } } if ($groupBy) { if (! $aggregate) { - $this->model->select('id'); + $model->select('id'); } - $this->model->groupBy('id'); + $model->groupBy('id'); } - $user = $this->model->first(); + $user = $model->first(); if (! $aggregate || $groupBy) { $count = is_object($user) ? 1 : 0; @@ -256,7 +256,7 @@ public function testFirstRespectsSoftDeletes($aggregate, $groupBy): void $this->assertSame(9, (int) $user->id); } - $user = $this->model->withDeleted()->select('id')->first(); + $user = $model->withDeleted()->select('id')->first(); $this->assertSame(1, (int) $user->id); } @@ -267,29 +267,29 @@ public function testFirstRespectsSoftDeletes($aggregate, $groupBy): void #[DataProvider('provideAggregateAndGroupBy')] public function testFirstRecoverTempUseSoftDeletes($aggregate, $groupBy): void { - $this->createModel(UserModel::class); - $this->model->delete(1); + $model = $this->createModel(UserModel::class); + $model->delete(1); if ($aggregate) { $ANSISQLDriverNames = ['OCI8']; if (in_array($this->db->DBDriver, $ANSISQLDriverNames, true)) { - $this->model->select('SUM("id") as "id"'); + $model->select('SUM("id") as "id"'); } else { - $this->model->select('SUM(id) as id'); + $model->select('SUM(id) as id'); } } else { - $this->model->select('id'); + $model->select('id'); } if ($groupBy) { - $this->model->groupBy('id'); + $model->groupBy('id'); } - $user = $this->model->withDeleted()->first(); + $user = $model->withDeleted()->first(); $this->assertSame(1, (int) $user->id); - $user2 = $this->model->select('id')->first(); + $user2 = $model->select('id')->first(); $this->assertSame(2, (int) $user2->id); } diff --git a/tests/system/Models/InsertModelTest.php b/tests/system/Models/InsertModelTest.php index 74a2ffaa89fa..ccaa8e9cf4ac 100644 --- a/tests/system/Models/InsertModelTest.php +++ b/tests/system/Models/InsertModelTest.php @@ -119,12 +119,12 @@ public function testInsertBatchSetsIntTimestamps(): void ], ]; - $this->createModel(JobModel::class); + $model = $this->createModel(JobModel::class); - $this->setPrivateProperty($this->model, 'useTimestamps', true); - $this->assertSame(2, $this->model->insertBatch($jobData)); + $this->setPrivateProperty($model, 'useTimestamps', true); + $this->assertSame(2, $model->insertBatch($jobData)); - $result = $this->model->where('name', 'Philosopher')->first(); + $result = $model->where('name', 'Philosopher')->first(); $this->assertCloseEnough(time(), (int) $result->created_at); } @@ -143,12 +143,12 @@ public function testInsertBatchSetsDatetimeTimestamps(): void ], ]; - $this->createModel(UserModel::class); + $model = $this->createModel(UserModel::class); - $this->setPrivateProperty($this->model, 'useTimestamps', true); - $this->assertSame(2, $this->model->insertBatch($userData)); + $this->setPrivateProperty($model, 'useTimestamps', true); + $this->assertSame(2, $model->insertBatch($userData)); - $result = $this->model->where('name', 'Lou')->first(); + $result = $model->where('name', 'Lou')->first(); $this->assertCloseEnough(time(), strtotime($result->created_at)); } @@ -335,15 +335,13 @@ public function testInsertWithSetAndEscape(): void 'name' => 'Scott', ]; - $this->createModel(UserModel::class); - - $this->setPrivateProperty($this->model, 'useTimestamps', true); - - $this->model->set('country', '1+1', false)->set('email', '2+2')->insert($userData); + $model = $this->createModel(UserModel::class); + $this->setPrivateProperty($model, 'useTimestamps', true); - $this->assertGreaterThan(0, $this->model->getInsertID()); - $result = $this->model->where('name', 'Scott')->where('country', '2')->where('email', '2+2')->first(); + $model->set('country', '1+1', false)->set('email', '2+2')->insert($userData); + $this->assertGreaterThan(0, $model->getInsertID()); + $result = $model->where('name', 'Scott')->where('country', '2')->where('email', '2+2')->first(); $this->assertNotNull($result->created_at); } @@ -389,7 +387,7 @@ public function testInsertBatchWithCasts(): void ]; $this->createModel(UserCastsTimestampModel::class); - $numRows = $this->model->insertBatch($userData); // @phpstan-ignore argument.type + $numRows = $this->model->insertBatch($userData); $this->assertSame(2, $numRows); diff --git a/tests/system/Models/MiscellaneousModelTest.php b/tests/system/Models/MiscellaneousModelTest.php index e95a709e61bd..ae6d98621819 100644 --- a/tests/system/Models/MiscellaneousModelTest.php +++ b/tests/system/Models/MiscellaneousModelTest.php @@ -99,7 +99,8 @@ public function testChunkEmptyTable(): void public function testCanCreateAndSaveEntityClasses(): void { - $entity = $this->createModel(EntityModel::class)->where('name', 'Developer')->first(); + $model = $this->createModel(EntityModel::class); + $entity = $model->where('name', 'Developer')->first(); $this->assertInstanceOf(SimpleEntity::class, $entity); $this->assertSame('Developer', $entity->name); @@ -110,9 +111,9 @@ public function testCanCreateAndSaveEntityClasses(): void $entity->name = 'Senior Developer'; $entity->created_at = $time; - $this->assertTrue($this->model->save($entity)); + $this->assertTrue($model->save($entity)); - $result = $this->model->where('name', 'Senior Developer')->first(); + $result = $model->where('name', 'Senior Developer')->first(); $this->assertSame( Time::createFromTimestamp($time)->toDateTimeString(), $result->created_at->toDateTimeString(), diff --git a/tests/system/Models/SaveModelTest.php b/tests/system/Models/SaveModelTest.php index be77d958ff7f..246bd8e6142c 100644 --- a/tests/system/Models/SaveModelTest.php +++ b/tests/system/Models/SaveModelTest.php @@ -167,9 +167,9 @@ public function testSelectAndEntitiesSaveOnlyChangedValues(): void 'created_at' => time(), ]); - $this->createModel(EntityModel::class); + $model = $this->createModel(EntityModel::class); - $job = $this->model->select('id, name')->where('name', 'Rocket Scientist')->first(); + $job = $model->select('id, name')->where('name', 'Rocket Scientist')->first(); $this->assertNull($job->description); $this->assertSame('Rocket Scientist', $job->name); @@ -181,7 +181,7 @@ public function testSelectAndEntitiesSaveOnlyChangedValues(): void 'name' => 'Rocket Scientist', ]); - $job = $this->model->select('id, name, description')->where('name', 'Rocket Scientist')->first(); + $job = $model->select('id, name, description')->where('name', 'Rocket Scientist')->first(); $this->assertSame('Some guitar description', $job->description); } diff --git a/tests/system/Models/TimestampModelTest.php b/tests/system/Models/TimestampModelTest.php index e2b5feb2da14..52989cff754a 100644 --- a/tests/system/Models/TimestampModelTest.php +++ b/tests/system/Models/TimestampModelTest.php @@ -19,6 +19,7 @@ use Tests\Support\Models\UserTimestampModel; /** + * @property-read UserTimestampModel $model * @internal */ #[Group('DatabaseLive')] @@ -170,14 +171,13 @@ public function testDoNotAllowDatesUpdateEntityUpdatesUpdatedAt(): void ]; $id = $this->doNotAllowDatesPrepareOneRecord($data); $this->setPrivateProperty($this->model, 'returnType', User::class); - $this->setPrivateProperty($this->model, 'tempReturnType', User::class); - $user = $this->model->find($id); + $user = $this->model->asObject(User::class)->find($id); $user->country = 'CA'; $this->model->update($user->id, $user); - $user = $this->model->find($id); + $user = $this->model->asObject(User::class)->find($id); $this->assertSame('2023-11-25 12:00:00', (string) $user->created_at); $this->assertSame('2023-11-25 12:00:00', (string) $user->updated_at); @@ -292,14 +292,13 @@ public function testAllowDatesUpdateEntityUpdatesUpdatedAt(): void ]; $id = $this->allowDatesPrepareOneRecord($data); $this->setPrivateProperty($this->model, 'returnType', User::class); - $this->setPrivateProperty($this->model, 'tempReturnType', User::class); - $user = $this->model->find($id); + $user = $this->model->asObject(User::class)->find($id); $user->country = 'CA'; $this->model->update($user->id, $user); - $user = $this->model->find($id); + $user = $this->model->asObject(User::class)->find($id); $this->assertSame('2000-01-01 12:00:00', (string) $user->created_at); // The Entity has `updated_at` value, but it will be discarded because of onlyChanged. diff --git a/tests/system/Models/UpdateModelTest.php b/tests/system/Models/UpdateModelTest.php index ed2ebf7a9a93..23279dc5b968 100644 --- a/tests/system/Models/UpdateModelTest.php +++ b/tests/system/Models/UpdateModelTest.php @@ -422,7 +422,6 @@ public function testUpdateSetObject(): void /** @var int|string $id */ $id = $this->model->insert($object); - /** @var stdClass $object */ $object = $this->model->find($id); $object->name = 'John Smith'; @@ -457,30 +456,27 @@ public function testUpdateSetEntity(): void public function testUpdateEntityWithPrimaryKeyCast(): void { - if ( - in_array($this->db->DBDriver, ['OCI8', 'Postgre', 'SQLSRV', 'SQLite3'], true) - ) { + if (in_array($this->db->DBDriver, ['OCI8', 'Postgre', 'SQLSRV', 'SQLite3'], true)) { $this->markTestSkipped($this->db->DBDriver . ' does not work with binary data as string data.'); } $this->createUuidTable(); - $this->createModel(UUIDPkeyModel::class); + $model = $this->createModel(UUIDPkeyModel::class); $entity = new UUID(); $entity->id = '550e8400-e29b-41d4-a716-446655440000'; $entity->value = 'test1'; - $id = $this->model->insert($entity); - $entity = $this->model->find($id); + $id = $model->insert($entity); + $entity = $model->find($id); $entity->value = 'id'; - $result = $this->model->save($entity); + $result = $model->save($entity); $this->assertTrue($result); - $entity = $this->model->find($id); - + $entity = $model->find($id); $this->assertSame('id', $entity->value); } @@ -716,7 +712,7 @@ public function testUpdateBatchWithCasts(): void ], ]; - $numRows = $this->model->updateBatch($updateData, 'id'); // @phpstan-ignore argument.type + $numRows = $this->model->updateBatch($updateData, 'id'); $this->assertSame(2, $numRows); $this->seeInDatabase('user', ['email' => json_encode($updateData[0]['email'])]); diff --git a/tests/system/Models/WhenWhenNotModelTest.php b/tests/system/Models/WhenWhenNotModelTest.php index 31015aa88bb1..aa9edffe9efe 100644 --- a/tests/system/Models/WhenWhenNotModelTest.php +++ b/tests/system/Models/WhenWhenNotModelTest.php @@ -40,9 +40,10 @@ public function testWhenWithTrueCondition(): void ]; $filter = 'foobar'; - $this->createModel(SecondaryModel::class)->insertBatch($secondaryData); + $model = $this->createModel(SecondaryModel::class); + $model->insertBatch($secondaryData); - $result = $this->model->when($filter, static function ($query, $filter): void { + $result = $model->when($filter, static function ($query, $filter): void { $query->where('value', $filter); })->find(); @@ -69,9 +70,10 @@ public function testWhenWithFalseConditionAndDefaultCallback(): void ]; $filter = ''; - $this->createModel(SecondaryModel::class)->insertBatch($secondaryData); + $model = $this->createModel(SecondaryModel::class); + $model->insertBatch($secondaryData); - $result = $this->model->when($filter, static function ($query, $filter): void { + $result = $model->when($filter, static function ($query, $filter): void { $query->where('value', $filter); }, static function ($query): void { $query->where('value', 'foobar'); @@ -100,9 +102,10 @@ public function testWhenNotWithFalseCondition(): void ]; $filter = ''; - $this->createModel(SecondaryModel::class)->insertBatch($secondaryData); + $model = $this->createModel(SecondaryModel::class); + $model->insertBatch($secondaryData); - $result = $this->model->whenNot($filter, static function ($query, $filter): void { + $result = $model->whenNot($filter, static function ($query, $filter): void { $query->where('value !=', 'foobar'); })->find(); @@ -129,9 +132,10 @@ public function testWhenNotWithTrueConditionAndDefaultCallback(): void ]; $filter = 'foobar'; - $this->createModel(SecondaryModel::class)->insertBatch($secondaryData); + $model = $this->createModel(SecondaryModel::class); + $model->insertBatch($secondaryData); - $result = $this->model->whenNot($filter, static function ($query, $filter): void { + $result = $model->whenNot($filter, static function ($query, $filter): void { $query->where('value !=', 'foobar'); }, static function ($query): void { $query->where('value', 'foobar'); diff --git a/tests/system/SuperglobalsTest.php b/tests/system/SuperglobalsTest.php index b7a2af0edcd7..de1fed832f3a 100644 --- a/tests/system/SuperglobalsTest.php +++ b/tests/system/SuperglobalsTest.php @@ -40,7 +40,7 @@ public function testServerGetSet(): void $this->superglobals->setServer('TEST_KEY', 'test_value'); $this->assertSame('test_value', $this->superglobals->server('TEST_KEY')); - $this->assertSame('test_value', $_SERVER['TEST_KEY']); + $this->assertSame('test_value', $_SERVER['TEST_KEY']); // @phpstan-ignore codeigniter.superglobalsOffsetAccess (checks the live superglobal, not the snapshot service) } public function testServerGetReturnsNullForNonExistent(): void @@ -108,7 +108,7 @@ public function testGetGetSet(): void $this->superglobals->setGet('test', 'value1'); $this->assertSame('value1', $this->superglobals->get('test')); - $this->assertSame('value1', $_GET['test']); + $this->assertSame('value1', $_GET['test']); // @phpstan-ignore codeigniter.superglobalsOffsetAccess (checks the live superglobal, not the snapshot service) } public function testGetReturnsNullForNonExistent(): void @@ -158,7 +158,7 @@ public function testPostGetSet(): void $this->superglobals->setPost('test', 'value1'); $this->assertSame('value1', $this->superglobals->post('test')); - $this->assertSame('value1', $_POST['test']); + $this->assertSame('value1', $_POST['test']); // @phpstan-ignore codeigniter.superglobalsOffsetAccess (checks the live superglobal, not the snapshot service) } public function testPostReturnsNullForNonExistent(): void @@ -208,7 +208,7 @@ public function testCookieGetSet(): void $this->superglobals->setCookie('session', 'abc123'); $this->assertSame('abc123', $this->superglobals->cookie('session')); - $this->assertSame('abc123', $_COOKIE['session']); + $this->assertSame('abc123', $_COOKIE['session']); // @phpstan-ignore codeigniter.superglobalsOffsetAccess (checks the live superglobal, not the snapshot service) } public function testCookieReturnsNullForNonExistent(): void @@ -258,7 +258,7 @@ public function testRequestGetSet(): void $this->superglobals->setRequest('test', 'value1'); $this->assertSame('value1', $this->superglobals->request('test')); - $this->assertSame('value1', $_REQUEST['test']); + $this->assertSame('value1', $_REQUEST['test']); // @phpstan-ignore codeigniter.superglobalsOffsetAccess (checks the live superglobal, not the snapshot service) } public function testRequestReturnsNullForNonExistent(): void @@ -475,11 +475,11 @@ public function testConstructorSynchronizesWithPhpSuperglobals(): void new Superglobals($server, $get, $post, $cookie, $files, $request); // Verify PHP superglobals are synchronized - $this->assertSame('server_val', $_SERVER['CUSTOM_SERVER']); - $this->assertSame('get_val', $_GET['custom_get']); - $this->assertSame('post_val', $_POST['custom_post']); - $this->assertSame('cookie_val', $_COOKIE['custom_cookie']); - $this->assertSame('request_val', $_REQUEST['custom_request']); + $this->assertSame('server_val', $_SERVER['CUSTOM_SERVER']); // @phpstan-ignore codeigniter.superglobalsOffsetAccess (checks the live superglobal, not the snapshot service) + $this->assertSame('get_val', $_GET['custom_get']); // @phpstan-ignore codeigniter.superglobalsOffsetAccess (checks the live superglobal, not the snapshot service) + $this->assertSame('post_val', $_POST['custom_post']); // @phpstan-ignore codeigniter.superglobalsOffsetAccess (checks the live superglobal, not the snapshot service) + $this->assertSame('cookie_val', $_COOKIE['custom_cookie']); // @phpstan-ignore codeigniter.superglobalsOffsetAccess (checks the live superglobal, not the snapshot service) + $this->assertSame('request_val', $_REQUEST['custom_request']); // @phpstan-ignore codeigniter.superglobalsOffsetAccess (checks the live superglobal, not the snapshot service) $this->assertSame($files, $_FILES); } diff --git a/user_guide_src/source/installation/upgrade_412.rst b/user_guide_src/source/installation/upgrade_412.rst index 1fec935f7ec6..3d3f0aa4051a 100644 --- a/user_guide_src/source/installation/upgrade_412.rst +++ b/user_guide_src/source/installation/upgrade_412.rst @@ -143,9 +143,6 @@ many will be simple comments or formatting that have no effect on the runtime: * ``composer.json`` * ``contributing/guidelines.rst`` * ``env`` -* ``phpstan.neon.dist`` -* ``phpunit.xml.dist`` * ``public/.htaccess`` * ``public/index.php`` -* ``rector.php`` * ``spark`` diff --git a/utils/phpstan-baseline/argument.type.neon b/utils/phpstan-baseline/argument.type.neon index 65d03b5b278d..bbd0685fcf76 100644 --- a/utils/phpstan-baseline/argument.type.neon +++ b/utils/phpstan-baseline/argument.type.neon @@ -1,4 +1,4 @@ -# total 76 errors +# total 73 errors parameters: ignoreErrors: @@ -177,11 +177,6 @@ parameters: count: 1 path: ../../tests/system/Log/Handlers/ChromeLoggerHandlerTest.php - - - message: '#^Parameter \#1 \$row of method CodeIgniter\\Model\:\:insert\(\) expects array\\|object\|null, array\\|string\> given\.$#' - count: 3 - path: ../../tests/system/Models/DataConverterModelTest.php - - message: '#^Parameter \#1 \$format of method CodeIgniter\\RESTful\\ResourceController\:\:setFormat\(\) expects ''json''\|''xml'', ''Nonsense'' given\.$#' count: 1 diff --git a/utils/phpstan-baseline/codeigniter.superglobalsGlobalAssign.neon b/utils/phpstan-baseline/codeigniter.superglobalsGlobalAssign.neon deleted file mode 100644 index e34453095d8e..000000000000 --- a/utils/phpstan-baseline/codeigniter.superglobalsGlobalAssign.neon +++ /dev/null @@ -1,83 +0,0 @@ -# total 17 errors - -parameters: - ignoreErrors: - - - message: '#^Direct global assignment to \$_SERVER is not allowed\.$#' - count: 1 - path: ../../tests/system/Config/BaseConfigTest.php - - - - message: '#^Direct global assignment to \$_SERVER is not allowed\.$#' - count: 1 - path: ../../tests/system/Config/ServicesTest.php - - - - message: '#^Direct global assignment to \$_SERVER is not allowed\.$#' - count: 2 - path: ../../tests/system/ControllerTest.php - - - - message: '#^Direct global assignment to \$_SERVER is not allowed\.$#' - count: 1 - path: ../../tests/system/Filters/FiltersTest.php - - - - message: '#^Direct global assignment to \$_COOKIE is not allowed\.$#' - count: 1 - path: ../../tests/system/HTTP/IncomingRequestTest.php - - - - message: '#^Direct global assignment to \$_GET is not allowed\.$#' - count: 1 - path: ../../tests/system/HTTP/IncomingRequestTest.php - - - - message: '#^Direct global assignment to \$_POST is not allowed\.$#' - count: 1 - path: ../../tests/system/HTTP/IncomingRequestTest.php - - - - message: '#^Direct global assignment to \$_REQUEST is not allowed\.$#' - count: 1 - path: ../../tests/system/HTTP/IncomingRequestTest.php - - - - message: '#^Direct global assignment to \$_SERVER is not allowed\.$#' - count: 1 - path: ../../tests/system/HTTP/IncomingRequestTest.php - - - - message: '#^Direct global assignment to \$_GET is not allowed\.$#' - count: 1 - path: ../../tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php - - - - message: '#^Direct global assignment to \$_SERVER is not allowed\.$#' - count: 1 - path: ../../tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php - - - - message: '#^Direct global assignment to \$_COOKIE is not allowed\.$#' - count: 1 - path: ../../tests/system/Helpers/CookieHelperTest.php - - - - message: '#^Direct global assignment to \$_GET is not allowed\.$#' - count: 1 - path: ../../tests/system/Helpers/FormHelperTest.php - - - - message: '#^Direct global assignment to \$_POST is not allowed\.$#' - count: 1 - path: ../../tests/system/Helpers/FormHelperTest.php - - - - message: '#^Direct global assignment to \$_SERVER is not allowed\.$#' - count: 1 - path: ../../tests/system/Helpers/URLHelper/SiteUrlCliTest.php - - - - message: '#^Direct global assignment to \$_SERVER is not allowed\.$#' - count: 1 - path: ../../tests/system/Helpers/URLHelper/SiteUrlTest.php diff --git a/utils/phpstan-baseline/codeigniter.superglobalsOffsetAccess.neon b/utils/phpstan-baseline/codeigniter.superglobalsOffsetAccess.neon deleted file mode 100644 index 81cec98356ff..000000000000 --- a/utils/phpstan-baseline/codeigniter.superglobalsOffsetAccess.neon +++ /dev/null @@ -1,148 +0,0 @@ -# total 35 errors - -parameters: - ignoreErrors: - - - message: '#^Direct access to \$_SERVER\[''argv''\] is not allowed\.$#' - count: 2 - path: ../../system/Boot.php - - - - message: '#^Direct access to \$_SERVER\[''ANSICON''\] is not allowed\.$#' - count: 1 - path: ../../system/CLI/CLI.php - - - - message: '#^Direct access to \$_SERVER\[''NO_COLOR''\] is not allowed\.$#' - count: 1 - path: ../../system/CLI/CLI.php - - - - message: '#^Direct access to \$_SERVER\[''argv''\] is not allowed\.$#' - count: 1 - path: ../../system/CLI/CLI.php - - - - message: '#^Direct access to \$_SERVER\[''REMOTE_ADDR''\] is not allowed\.$#' - count: 1 - path: ../../system/Common.php - - - - message: '#^Direct access to \$_SERVER\[''REQUEST_METHOD''\] is not allowed\.$#' - count: 1 - path: ../../system/Common.php - - - - message: '#^Direct access to \$_SERVER\[\$key\] is not allowed\.$#' - count: 1 - path: ../../system/Common.php - - - - message: '#^Direct access to \$_SERVER\[''CI_ENVIRONMENT''\] is not allowed\.$#' - count: 2 - path: ../../system/Config/AutoloadConfig.php - - - - message: '#^Direct access to \$_SERVER\["\{\$prefix\}\.\{\$property\}"\] is not allowed\.$#' - count: 1 - path: ../../system/Config/BaseConfig.php - - - - message: '#^Direct access to \$_SERVER\["\{\$prefix\}_\{\$underscoreProperty\}"\] is not allowed\.$#' - count: 1 - path: ../../system/Config/BaseConfig.php - - - - message: '#^Direct access to \$_SERVER\["\{\$shortPrefix\}\.\{\$property\}"\] is not allowed\.$#' - count: 1 - path: ../../system/Config/BaseConfig.php - - - - message: '#^Direct access to \$_SERVER\["\{\$shortPrefix\}_\{\$underscoreProperty\}"\] is not allowed\.$#' - count: 1 - path: ../../system/Config/BaseConfig.php - - - - message: '#^Direct access to \$_COOKIE\[\$this\-\>config\-\>cookieName\] is not allowed\.$#' - count: 5 - path: ../../system/Session/Session.php - - - - message: '#^Direct access to \$_SERVER\[''BAR''\] is not allowed\.$#' - count: 1 - path: ../../tests/system/Config/DotEnvTest.php - - - - message: '#^Direct access to \$_SERVER\[''FOO''\] is not allowed\.$#' - count: 1 - path: ../../tests/system/Config/DotEnvTest.php - - - - message: '#^Direct access to \$_SERVER\[''NULL''\] is not allowed\.$#' - count: 1 - path: ../../tests/system/Config/DotEnvTest.php - - - - message: '#^Direct access to \$_SERVER\[''SPACED''\] is not allowed\.$#' - count: 1 - path: ../../tests/system/Config/DotEnvTest.php - - - - message: '#^Direct access to \$_SERVER\[''SimpleConfig_simple_name''\] is not allowed\.$#' - count: 1 - path: ../../tests/system/Config/DotEnvTest.php - - - - message: '#^Direct access to \$_SERVER\[''QUERY_STRING''\] is not allowed\.$#' - count: 1 - path: ../../tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php - - - - message: '#^Direct access to \$_COOKIE\[''custom_cookie''\] is not allowed\.$#' - count: 1 - path: ../../tests/system/SuperglobalsTest.php - - - - message: '#^Direct access to \$_COOKIE\[''session''\] is not allowed\.$#' - count: 1 - path: ../../tests/system/SuperglobalsTest.php - - - - message: '#^Direct access to \$_GET\[''custom_get''\] is not allowed\.$#' - count: 1 - path: ../../tests/system/SuperglobalsTest.php - - - - message: '#^Direct access to \$_GET\[''test''\] is not allowed\.$#' - count: 1 - path: ../../tests/system/SuperglobalsTest.php - - - - message: '#^Direct access to \$_POST\[''custom_post''\] is not allowed\.$#' - count: 1 - path: ../../tests/system/SuperglobalsTest.php - - - - message: '#^Direct access to \$_POST\[''test''\] is not allowed\.$#' - count: 1 - path: ../../tests/system/SuperglobalsTest.php - - - - message: '#^Direct access to \$_REQUEST\[''custom_request''\] is not allowed\.$#' - count: 1 - path: ../../tests/system/SuperglobalsTest.php - - - - message: '#^Direct access to \$_REQUEST\[''test''\] is not allowed\.$#' - count: 1 - path: ../../tests/system/SuperglobalsTest.php - - - - message: '#^Direct access to \$_SERVER\[''CUSTOM_SERVER''\] is not allowed\.$#' - count: 1 - path: ../../tests/system/SuperglobalsTest.php - - - - message: '#^Direct access to \$_SERVER\[''TEST_KEY''\] is not allowed\.$#' - count: 1 - path: ../../tests/system/SuperglobalsTest.php diff --git a/utils/phpstan-baseline/codeigniter.superglobalsOffsetAssign.neon b/utils/phpstan-baseline/codeigniter.superglobalsOffsetAssign.neon deleted file mode 100644 index 9012f8e762f8..000000000000 --- a/utils/phpstan-baseline/codeigniter.superglobalsOffsetAssign.neon +++ /dev/null @@ -1,28 +0,0 @@ -# total 5 errors - -parameters: - ignoreErrors: - - - message: '#^Direct assignment of string\|null to \$_COOKIE\[''csrf_cookie_name''\] is not allowed\.$#' - count: 1 - path: ../../system/Test/Mock/MockSecurity.php - - - - message: '#^Direct assignment of ''1'' to \$_SERVER\[''CODEIGNITER_SCREAM_DEPRECATIONS''\] is not allowed\.$#' - count: 1 - path: ../../tests/system/Debug/ExceptionsTest.php - - - - message: '#^Direct assignment of ''test'' to \$_SERVER\[''HTTPS''\] is not allowed\.$#' - count: 1 - path: ../../tests/system/HomeTest.php - - - - message: '#^Direct assignment of ''test'' to \$_SERVER\[''HTTPS''\] is not allowed\.$#' - count: 1 - path: ../../tests/system/Test/FeatureTestAutoRoutingImprovedTest.php - - - - message: '#^Direct assignment of ''test'' to \$_SERVER\[''HTTPS''\] is not allowed\.$#' - count: 1 - path: ../../tests/system/Test/FeatureTestTraitTest.php diff --git a/utils/phpstan-baseline/codeigniter.superglobalsOffsetUnset.neon b/utils/phpstan-baseline/codeigniter.superglobalsOffsetUnset.neon deleted file mode 100644 index bbdc3228eb79..000000000000 --- a/utils/phpstan-baseline/codeigniter.superglobalsOffsetUnset.neon +++ /dev/null @@ -1,33 +0,0 @@ -# total 7 errors - -parameters: - ignoreErrors: - - - message: '#^Direct unset of \$_SERVER\[''encryption\.key''\] is not allowed\.$#' - count: 1 - path: ../../system/Commands/Encryption/GenerateKey.php - - - - message: '#^Direct unset of \$_COOKIE\[\$this\-\>config\-\>cookieName\] is not allowed\.$#' - count: 1 - path: ../../system/Session/Session.php - - - - message: '#^Direct unset of \$_SERVER\[''foo''\] is not allowed\.$#' - count: 1 - path: ../../tests/system/CommonFunctionsSendTest.php - - - - message: '#^Direct unset of \$_SERVER\[''foo''\] is not allowed\.$#' - count: 1 - path: ../../tests/system/CommonFunctionsTest.php - - - - message: '#^Direct unset of \$_SERVER\[''encryption\.key''\] is not allowed\.$#' - count: 2 - path: ../../tests/system/Config/DotEnvTest.php - - - - message: '#^Direct unset of \$_SERVER\[''CODEIGNITER_SCREAM_DEPRECATIONS''\] is not allowed\.$#' - count: 1 - path: ../../tests/system/Debug/ExceptionsTest.php diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index d464ba204c47..672ff3a32e43 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,14 +1,10 @@ -# total 2035 errors +# total 1944 errors includes: - argument.type.neon - arguments.count.neon - assign.propertyType.neon - codeigniter.modelArgumentType.neon - - codeigniter.superglobalsGlobalAssign.neon - - codeigniter.superglobalsOffsetAccess.neon - - codeigniter.superglobalsOffsetAssign.neon - - codeigniter.superglobalsOffsetUnset.neon - deadCode.unreachable.neon - empty.notAllowed.neon - function.resultUnused.neon diff --git a/utils/phpstan-baseline/property.nonObject.neon b/utils/phpstan-baseline/property.nonObject.neon index 26e874602857..7268e376d119 100644 --- a/utils/phpstan-baseline/property.nonObject.neon +++ b/utils/phpstan-baseline/property.nonObject.neon @@ -1,4 +1,4 @@ -# total 36 errors +# total 12 errors parameters: ignoreErrors: @@ -27,61 +27,6 @@ parameters: count: 1 path: ../../tests/system/Database/Live/GetTest.php - - - message: '#^Cannot access property \$id on array\\.$#' - count: 2 - path: ../../tests/system/Models/FindModelTest.php - - - - message: '#^Cannot access property \$created_at on array\\.$#' - count: 3 - path: ../../tests/system/Models/InsertModelTest.php - - - - message: '#^Cannot access property \$created_at on array\\.$#' - count: 1 - path: ../../tests/system/Models/MiscellaneousModelTest.php - - - - message: '#^Cannot access property \$description on array\\.$#' - count: 1 - path: ../../tests/system/Models/SaveModelTest.php - - - - message: '#^Cannot access property \$country on array\\.$#' - count: 2 - path: ../../tests/system/Models/TimestampModelTest.php - - - - message: '#^Cannot access property \$created_at on array\\.$#' - count: 2 - path: ../../tests/system/Models/TimestampModelTest.php - - - - message: '#^Cannot access property \$id on array\\.$#' - count: 2 - path: ../../tests/system/Models/TimestampModelTest.php - - - - message: '#^Cannot access property \$updated_at on array\\.$#' - count: 2 - path: ../../tests/system/Models/TimestampModelTest.php - - - - message: '#^Cannot access property \$value on array\\|string, mixed\>\.$#' - count: 1 - path: ../../tests/system/Models/UpdateModelTest.php - - - - message: '#^Cannot access property \$key on array\\.$#' - count: 7 - path: ../../tests/system/Models/WhenWhenNotModelTest.php - - - - message: '#^Cannot access property \$value on array\\.$#' - count: 1 - path: ../../tests/system/Models/WhenWhenNotModelTest.php - - message: '#^Cannot access property \$created_at on array\|object\.$#' count: 1 From 606ada9cbd7280e98bf357e16a6718af32ac4a69 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 02:03:56 +0700 Subject: [PATCH 47/65] chore(deps-dev): update boundwize/structarmed requirement (#10346) Updates the requirements on [boundwize/structarmed](https://github.com/boundwize/structarmed) to permit the latest version. Updates `boundwize/structarmed` to 0.14.6 - [Release notes](https://github.com/boundwize/structarmed/releases) - [Commits](https://github.com/boundwize/structarmed/compare/0.13.8...0.14.6) --- updated-dependencies: - dependency-name: boundwize/structarmed dependency-version: 0.14.6 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 160daedc0d8e..bb24bbdd48f4 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "psr/log": "^3.0" }, "require-dev": { - "boundwize/structarmed": "0.13.8", + "boundwize/structarmed": "0.14.6", "codeigniter/phpstan-codeigniter": "^2.1", "fakerphp/faker": "^1.24", "kint-php/kint": "^6.1", From fa140b5f9719481e2e681c325522fe086981b0b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 22:11:59 +0200 Subject: [PATCH 48/65] chore(deps): bump actions/cache in / (#10347) Bumps [actions/cache](https://github.com/actions/cache) in `/` from 6.0.0 to 6.1.0. Updates `actions/cache` from 6.0.0 to 6.1.0 - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/2c8a9bd7457de244a408f35966fab2fb45fda9c8...55cc8345863c7cc4c66a329aec7e433d2d1c52a9) --- updated-dependencies: - dependency-name: actions/cache dependency-version: 6.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/reusable-coveralls.yml | 4 ++-- .github/workflows/reusable-phpunit-test.yml | 4 ++-- .github/workflows/reusable-serviceless-phpunit-test.yml | 4 ++-- .github/workflows/test-coding-standards.yml | 2 +- .github/workflows/test-phpstan.yml | 4 ++-- .github/workflows/test-psalm.yml | 4 ++-- .github/workflows/test-random-execution.yml | 2 +- .github/workflows/test-rector.yml | 4 ++-- .github/workflows/test-structarmed.yml | 4 ++-- 9 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/reusable-coveralls.yml b/.github/workflows/reusable-coveralls.yml index 74c04243de03..e47cceb1771d 100644 --- a/.github/workflows/reusable-coveralls.yml +++ b/.github/workflows/reusable-coveralls.yml @@ -43,7 +43,7 @@ jobs: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 + 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.*') }} @@ -52,7 +52,7 @@ jobs: ${{ github.job }}- - name: Cache PHPUnit's static analysis cache - uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: build/.phpunit.cache/code-coverage key: phpunit-code-coverage-${{ hashFiles('**/phpunit.*') }} diff --git a/.github/workflows/reusable-phpunit-test.yml b/.github/workflows/reusable-phpunit-test.yml index e0c8b6c835a7..43d520458bf0 100644 --- a/.github/workflows/reusable-phpunit-test.yml +++ b/.github/workflows/reusable-phpunit-test.yml @@ -192,7 +192,7 @@ jobs: echo "ARTIFACT_NAME=${{ inputs.job-id || github.job }}-php-${{ inputs.php-version }}-db-${{ inputs.db-platform || 'none' }}${{ inputs.mysql-version || '' }}" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 + 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.*') }} @@ -203,7 +203,7 @@ jobs: - name: Cache PHPUnit's static analysis cache if: ${{ inputs.enable-artifact-upload }} - uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: build/.phpunit.cache/code-coverage key: phpunit-code-coverage-${{ hashFiles('**/phpunit.*') }} diff --git a/.github/workflows/reusable-serviceless-phpunit-test.yml b/.github/workflows/reusable-serviceless-phpunit-test.yml index 81ed2561f659..7858913cf531 100644 --- a/.github/workflows/reusable-serviceless-phpunit-test.yml +++ b/.github/workflows/reusable-serviceless-phpunit-test.yml @@ -89,7 +89,7 @@ jobs: echo "ARTIFACT_NAME=${{ inputs.job-id || github.job }}-php-${{ inputs.php-version }}" >> $GITHUB_OUTPUT - name: Cache Composer dependencies - uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 + 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.*') }} @@ -99,7 +99,7 @@ jobs: - name: Cache PHPUnit's static analysis cache if: ${{ inputs.enable-artifact-upload }} - uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: build/.phpunit.cache/code-coverage key: phpunit-code-coverage-${{ hashFiles('**/phpunit.*') }} diff --git a/.github/workflows/test-coding-standards.yml b/.github/workflows/test-coding-standards.yml index 318643448765..125dbd5b3311 100644 --- a/.github/workflows/test-coding-standards.yml +++ b/.github/workflows/test-coding-standards.yml @@ -49,7 +49,7 @@ jobs: run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 + 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') }} diff --git a/.github/workflows/test-phpstan.yml b/.github/workflows/test-phpstan.yml index 02c844fba724..586314917200 100644 --- a/.github/workflows/test-phpstan.yml +++ b/.github/workflows/test-phpstan.yml @@ -66,7 +66,7 @@ jobs: run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ steps.composer-cache.outputs.COMPOSER_CACHE_FILES_DIR }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} @@ -76,7 +76,7 @@ jobs: run: mkdir -p build/phpstan - name: Cache PHPStan result cache directory - uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: build/phpstan key: ${{ runner.os }}-phpstan-${{ github.sha }} diff --git a/.github/workflows/test-psalm.yml b/.github/workflows/test-psalm.yml index c95ad3d9e697..43931032b9e4 100644 --- a/.github/workflows/test-psalm.yml +++ b/.github/workflows/test-psalm.yml @@ -54,7 +54,7 @@ jobs: run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache composer dependencies - uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 + 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') }} @@ -64,7 +64,7 @@ jobs: run: mkdir -p build/psalm - name: Cache Psalm results - uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: build/psalm key: ${{ runner.os }}-psalm-${{ github.sha }} diff --git a/.github/workflows/test-random-execution.yml b/.github/workflows/test-random-execution.yml index a5d7a12ee093..4f29559b2e93 100644 --- a/.github/workflows/test-random-execution.yml +++ b/.github/workflows/test-random-execution.yml @@ -186,7 +186,7 @@ jobs: run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache composer dependencies - uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ steps.composer-cache.outputs.COMPOSER_CACHE_FILES_DIR }} key: PHP_${{ matrix.php-version }}-${{ hashFiles('**/composer.*') }} diff --git a/.github/workflows/test-rector.yml b/.github/workflows/test-rector.yml index dd6cfe8c7c4a..4f52dfe53681 100644 --- a/.github/workflows/test-rector.yml +++ b/.github/workflows/test-rector.yml @@ -72,7 +72,7 @@ jobs: run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ steps.composer-cache.outputs.COMPOSER_CACHE_FILES_DIR }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} @@ -82,7 +82,7 @@ jobs: run: composer update --ansi --no-interaction ${{ matrix.composer-option }} - name: Rector Cache - uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: /tmp/rector key: ${{ runner.os }}-rector-${{ github.run_id }} diff --git a/.github/workflows/test-structarmed.yml b/.github/workflows/test-structarmed.yml index 828552c4f306..296b4d22870a 100644 --- a/.github/workflows/test-structarmed.yml +++ b/.github/workflows/test-structarmed.yml @@ -68,7 +68,7 @@ jobs: run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ steps.composer-cache.outputs.COMPOSER_CACHE_FILES_DIR }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} @@ -78,7 +78,7 @@ jobs: run: composer update --ansi --no-interaction - name: Structarmed Cache - uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: /tmp/structarmed key: ${{ runner.os }}-structarmed-${{ github.run_id }} From 72476b3aaad3ccd72921dcab67b0db6484741d0a Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Tue, 30 Jun 2026 11:47:09 +0800 Subject: [PATCH 49/65] refactor: replace type `mixed` with more specific types (#10345) * refactor: replace narrowable mixed PHPDocs with specific types * refactor: collapse long form `mixed` to just `mixed` --- system/Common.php | 8 +- system/Config/BaseConfig.php | 2 +- system/Config/BaseService.php | 2 +- system/Database/BaseBuilder.php | 4 +- system/Database/BaseConnection.php | 6 +- system/Database/BaseUtils.php | 4 +- system/Database/ConnectionInterface.php | 6 +- system/Database/OCI8/Builder.php | 2 +- system/Database/Postgre/Builder.php | 2 +- system/Database/Postgre/Connection.php | 2 +- system/Database/SQLSRV/Builder.php | 4 +- system/Database/SQLSRV/Forge.php | 2 +- system/Debug/Toolbar.php | 2 +- system/Encryption/Handlers/BaseHandler.php | 2 +- system/Encryption/KeyRotationDecorator.php | 2 +- system/Entity/Cast/CastInterface.php | 12 +- system/Entity/Entity.php | 8 +- system/HTTP/CURLRequest.php | 2 +- system/HTTP/IncomingRequest.php | 18 +- system/HTTP/RequestTrait.php | 2 +- system/Helpers/Array/ArrayHelper.php | 4 +- system/Helpers/array_helper.php | 12 +- system/I18n/TimeTrait.php | 2 +- system/Model.php | 4 +- system/Test/Mock/MockConnection.php | 2 +- system/Traits/ConditionalTrait.php | 2 - system/Validation/Rules.php | 10 +- .../StrictRules/CreditCardRules.php | 2 +- system/Validation/StrictRules/FormatRules.php | 50 +- system/Validation/StrictRules/Rules.php | 56 +- system/Validation/Validation.php | 18 +- system/Validation/ValidationInterface.php | 8 +- system/View/Parser.php | 2 +- utils/phpstan-baseline/empty.notAllowed.neon | 2 +- utils/phpstan-baseline/loader.neon | 2 +- .../method.childParameterType.neon | 15 +- .../missingType.iterableValue.neon | 522 +----------------- utils/phpstan-baseline/return.type.neon | 7 +- 38 files changed, 146 insertions(+), 666 deletions(-) diff --git a/system/Common.php b/system/Common.php index c0bab0fd3acd..d74fe403c302 100644 --- a/system/Common.php +++ b/system/Common.php @@ -403,9 +403,9 @@ function db_connect($db = null, bool $getShared = true) * retrieving values set from the .env file for * use in config files. * - * @param array|bool|float|int|object|string|null $default + * @param mixed $default * - * @return array|bool|float|int|object|string|null + * @return mixed */ function env(string $key, $default = null) { @@ -1080,7 +1080,7 @@ function session(?string $val = null) * - $timer = service('timer') * - $timer = \CodeIgniter\Config\Services::timer(); * - * @param array|bool|float|int|object|string|null ...$params + * @param mixed ...$params */ function service(string $name, ...$params): ?object { @@ -1096,7 +1096,7 @@ function service(string $name, ...$params): ?object /** * Always returns a new instance of the class. * - * @param array|bool|float|int|object|string|null ...$params + * @param mixed ...$params */ function single_service(string $name, ...$params): ?object { diff --git a/system/Config/BaseConfig.php b/system/Config/BaseConfig.php index 24c1ce019c08..40782a243b96 100644 --- a/system/Config/BaseConfig.php +++ b/system/Config/BaseConfig.php @@ -166,7 +166,7 @@ protected function parseEncryptionKey(string $key): string /** * Initialization an environment-specific configuration setting * - * @param array|bool|float|int|string|null $property + * @param array|bool|float|int|string|null $property * * @return void */ diff --git a/system/Config/BaseService.php b/system/Config/BaseService.php index 579879e9c768..64b5266da041 100644 --- a/system/Config/BaseService.php +++ b/system/Config/BaseService.php @@ -244,7 +244,7 @@ public static function override(string $key, object $value): void * * $key must be a name matching a service. * - * @param array|bool|float|int|object|string|null ...$params + * @param mixed ...$params * * @return object */ diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index df2fa411ca36..4900d7cd3db7 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -2817,7 +2817,7 @@ public function getCompiledDelete(bool $reset = true): string /** * Compiles a delete string and runs the query * - * @param array|RawSql|string $where + * @param array|RawSql|string $where * * @return bool|string Returns a SQL string if in test mode. * @@ -3075,7 +3075,7 @@ protected function trackAliases($table) * Generates a query string based on which functions were used. * Should not be called directly. * - * @param mixed $selectOverride + * @param false|string $selectOverride */ protected function compileSelect($selectOverride = false): string { diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index 273e4c60c4d5..c0cb7174dd52 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -781,7 +781,7 @@ abstract protected function execute(string $sql); * Should automatically handle different connections for read/write * queries if needed. * - * @param array|string|null $binds + * @param array|string|null $binds * * @return BaseResult|bool|Query * @@ -1533,7 +1533,7 @@ abstract public function affectedRows(): int; * Escapes data based on type. * Sets boolean and null types * - * @param array|bool|float|int|object|string|null $str + * @param mixed $str * * @return ($str is array ? array : float|int|string) */ @@ -2059,7 +2059,7 @@ protected function _enableForeignKeyChecks() /** * Accessor for properties if they exist. * - * @return array|bool|float|int|object|resource|string|null + * @return mixed */ public function __get(string $key) { diff --git a/system/Database/BaseUtils.php b/system/Database/BaseUtils.php index a9310e1d4e32..0286e177a060 100644 --- a/system/Database/BaseUtils.php +++ b/system/Database/BaseUtils.php @@ -125,7 +125,7 @@ public function optimizeTable(string $tableName) /** * Optimize Database * - * @return mixed + * @return array|bool * * @throws DatabaseException */ @@ -170,7 +170,7 @@ public function optimizeDatabase() /** * Repair Table * - * @return mixed + * @return array|bool * * @throws DatabaseException */ diff --git a/system/Database/ConnectionInterface.php b/system/Database/ConnectionInterface.php index 97c8fa0bbcba..15fd63e1c7aa 100644 --- a/system/Database/ConnectionInterface.php +++ b/system/Database/ConnectionInterface.php @@ -100,7 +100,7 @@ public function getVersion(): string; * Should automatically handle different connections for read/write * queries if needed. * - * @param array|string|null $binds + * @param array|string|null $binds * * @return BaseResult|bool|Query */ @@ -137,7 +137,7 @@ public function getLastQuery(); * Escapes data based on type. * Sets boolean and null types. * - * @param array|bool|float|int|object|string|null $str + * @param mixed $str * * @return ($str is array ? array : float|int|string) */ @@ -149,7 +149,7 @@ public function escape($str); * * @param array ...$params * - * @return array|bool|float|int|object|resource|string|null + * @return mixed */ public function callFunction(string $functionName, ...$params); diff --git a/system/Database/OCI8/Builder.php b/system/Database/OCI8/Builder.php index 0bbf4283d056..25af517dc959 100644 --- a/system/Database/OCI8/Builder.php +++ b/system/Database/OCI8/Builder.php @@ -150,7 +150,7 @@ protected function _truncate(string $table): string /** * Compiles a delete string and runs the query * - * @param mixed $where + * @param array|RawSql|string $where * * @return bool|string * diff --git a/system/Database/Postgre/Builder.php b/system/Database/Postgre/Builder.php index 502a5278482f..4cf0bde40060 100644 --- a/system/Database/Postgre/Builder.php +++ b/system/Database/Postgre/Builder.php @@ -225,7 +225,7 @@ protected function _insertBatch(string $table, array $keys, array $values): stri /** * Compiles a delete string and runs the query * - * @param mixed $where + * @param array|RawSql|string $where * * @return bool|string * diff --git a/system/Database/Postgre/Connection.php b/system/Database/Postgre/Connection.php index 37c1d678fcc0..4c3358a4b470 100644 --- a/system/Database/Postgre/Connection.php +++ b/system/Database/Postgre/Connection.php @@ -241,7 +241,7 @@ public function affectedRows(): int * * Escapes data based on type * - * @param array|bool|float|int|object|string|null $str + * @param mixed $str * * @return ($str is array ? array : float|int|string) */ diff --git a/system/Database/SQLSRV/Builder.php b/system/Database/SQLSRV/Builder.php index 67f2f26c33f9..098b08599808 100644 --- a/system/Database/SQLSRV/Builder.php +++ b/system/Database/SQLSRV/Builder.php @@ -538,7 +538,7 @@ protected function _delete(string $table): string /** * Compiles a delete string and runs the query * - * @param mixed $where + * @param array|RawSql|string $where * * @return bool|string * @@ -578,7 +578,7 @@ public function delete($where = '', ?int $limit = null, bool $resetData = true) * * Generates a query string based on which functions were used. * - * @param bool $selectOverride + * @param false|string $selectOverride */ protected function compileSelect($selectOverride = false): string { diff --git a/system/Database/SQLSRV/Forge.php b/system/Database/SQLSRV/Forge.php index 0622de5e8e81..64a4ffaf6d35 100644 --- a/system/Database/SQLSRV/Forge.php +++ b/system/Database/SQLSRV/Forge.php @@ -321,7 +321,7 @@ protected function _alterTable(string $alterType, string $table, $processedField /** * Drop index for table * - * @return mixed + * @return false|resource */ protected function _dropIndex(string $table, object $indexData) { diff --git a/system/Debug/Toolbar.php b/system/Debug/Toolbar.php index fdd0b0f3f54c..f5885aa0555c 100644 --- a/system/Debug/Toolbar.php +++ b/system/Debug/Toolbar.php @@ -479,7 +479,7 @@ public function respond(): void // Otherwise, if it includes ?debugbar_time, then // we should return the entire debugbar. - if ($request->getGet('debugbar_time')) { + if ($request->getGet('debugbar_time') !== null) { helper('security'); // Negotiate the content-type to format the output diff --git a/system/Encryption/Handlers/BaseHandler.php b/system/Encryption/Handlers/BaseHandler.php index 365ed961d323..c324a465dbb8 100644 --- a/system/Encryption/Handlers/BaseHandler.php +++ b/system/Encryption/Handlers/BaseHandler.php @@ -55,7 +55,7 @@ protected static function substr($str, $start, $length = null) * * @param string $key Property name * - * @return array|bool|int|string|null + * @return array|bool|int|string|null */ public function __get($key) { diff --git a/system/Encryption/KeyRotationDecorator.php b/system/Encryption/KeyRotationDecorator.php index 8778b7ce800b..9e4b6ef0002b 100644 --- a/system/Encryption/KeyRotationDecorator.php +++ b/system/Encryption/KeyRotationDecorator.php @@ -86,7 +86,7 @@ public function decrypt($data, #[SensitiveParameter] $params = null) /** * Delegate property access to the inner handler. * - * @return array|bool|int|string|null + * @return array|bool|int|string|null */ public function __get(string $key) { diff --git a/system/Entity/Cast/CastInterface.php b/system/Entity/Cast/CastInterface.php index 8b727fb0117f..9f5552826859 100644 --- a/system/Entity/Cast/CastInterface.php +++ b/system/Entity/Cast/CastInterface.php @@ -23,20 +23,20 @@ interface CastInterface /** * Takes a raw value from Entity, returns its value for PHP. * - * @param array|bool|float|int|object|string|null $value Data - * @param array $params Additional param + * @param mixed $value Data + * @param array $params Additional param * - * @return array|bool|float|int|object|string|null + * @return mixed */ public static function get($value, array $params = []); /** * Takes a PHP value, returns its raw value for Entity. * - * @param array|bool|float|int|object|string|null $value Data - * @param array $params Additional param + * @param mixed $value Data + * @param array $params Additional param * - * @return array|bool|float|int|object|string|null + * @return mixed */ public static function set($value, array $params = []); } diff --git a/system/Entity/Entity.php b/system/Entity/Entity.php index e3733bd0fc02..95261f607458 100644 --- a/system/Entity/Entity.php +++ b/system/Entity/Entity.php @@ -157,7 +157,7 @@ public function __construct(?array $data = null) * properties, using any `setCamelCasedProperty()` methods * that may or may not exist. * - * @param array|bool|float|int|object|string|null> $data + * @param array $data * * @return $this */ @@ -556,7 +556,7 @@ protected function mutateDate($value) * @param string $attribute Attribute name * @param string $method Allowed to "get" and "set" * - * @return array|bool|float|int|object|string|null + * @return mixed * * @throws CastException */ @@ -631,7 +631,7 @@ public function cast(?bool $cast = null) * $this->my_property = $p; * $this->setMyProperty() = $p; * - * @param array|bool|float|int|object|string|null $value + * @param mixed $value * * @return void * @@ -682,7 +682,7 @@ public function __set(string $key, $value = null) * $p = $this->my_property * $p = $this->getMyProperty() * - * @return array|bool|float|int|object|string|null + * @return mixed * * @throws Exception */ diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php index 02aadfa8f97b..ac05f9ae903b 100644 --- a/system/HTTP/CURLRequest.php +++ b/system/HTTP/CURLRequest.php @@ -289,7 +289,7 @@ public function setForm(array $params, bool $multipart = false) /** * Set JSON data to be sent. * - * @param array|bool|float|int|object|string|null $data + * @param mixed $data * * @return $this */ diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index f9a57a4098ba..a4a4c357ac74 100644 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -363,7 +363,7 @@ public function getDefaultLocale(): string * @param int|null $filter Filter constant * @param array|int|null $flags * - * @return array|bool|float|int|stdClass|string|null + * @return array|bool|float|int|stdClass|string|null */ public function getVar($index = null, $filter = null, $flags = null) { @@ -390,7 +390,7 @@ public function getVar($index = null, $filter = null, $flags = null) * * @see http://php.net/manual/en/function.json-decode.php * - * @return array|bool|float|int|stdClass|null + * @return array|bool|float|int|stdClass|null * * @throws HTTPException When the body is invalid as JSON. */ @@ -417,7 +417,7 @@ public function getJSON(bool $assoc = false, int $depth = 512, int $options = 0) * @param int|null $filter Filter Constant * @param array|int|null $flags Option * - * @return array|bool|float|int|stdClass|string|null + * @return array|bool|float|int|stdClass|string|null */ public function getJsonVar($index = null, bool $assoc = false, ?int $filter = null, $flags = null) { @@ -508,7 +508,7 @@ public function getRawInput() * @param int|null $filter Filter Constant * @param array|int|null $flags Option * - * @return array|bool|float|int|object|string|null + * @return mixed */ public function getRawInputVar($index = null, ?int $filter = null, $flags = null) { @@ -562,7 +562,7 @@ public function getRawInputVar($index = null, ?int $filter = null, $flags = null * @param int|null $filter A filter name to apply. * @param array|int|null $flags * - * @return array|bool|float|int|object|string|null + * @return mixed */ public function getGet($index = null, $filter = null, $flags = null) { @@ -576,7 +576,7 @@ public function getGet($index = null, $filter = null, $flags = null) * @param int|null $filter A filter name to apply * @param array|int|null $flags * - * @return array|bool|float|int|object|string|null + * @return mixed */ public function getPost($index = null, $filter = null, $flags = null) { @@ -590,7 +590,7 @@ public function getPost($index = null, $filter = null, $flags = null) * @param int|null $filter A filter name to apply * @param array|int|null $flags * - * @return array|bool|float|int|object|string|null + * @return mixed */ public function getPostGet($index = null, $filter = null, $flags = null) { @@ -613,7 +613,7 @@ public function getPostGet($index = null, $filter = null, $flags = null) * @param int|null $filter A filter name to apply * @param array|int|null $flags * - * @return array|bool|float|int|object|string|null + * @return mixed */ public function getGetPost($index = null, $filter = null, $flags = null) { @@ -636,7 +636,7 @@ public function getGetPost($index = null, $filter = null, $flags = null) * @param int|null $filter A filter name to be applied * @param array|int|null $flags * - * @return array|bool|float|int|object|string|null + * @return mixed */ public function getCookie($index = null, $filter = null, $flags = null) { diff --git a/system/HTTP/RequestTrait.php b/system/HTTP/RequestTrait.php index 6adfe3794935..67c5382adc1c 100644 --- a/system/HTTP/RequestTrait.php +++ b/system/HTTP/RequestTrait.php @@ -257,7 +257,7 @@ public function setGlobal(string $name, $value) * @param int|null $filter Filter constant * @param array|int|null $flags Options * - * @return array|bool|float|int|object|string|null + * @return mixed */ public function fetchGlobal(string $name, $index = null, ?int $filter = null, $flags = null) { diff --git a/system/Helpers/Array/ArrayHelper.php b/system/Helpers/Array/ArrayHelper.php index 6741dc4bbb0e..1e7d6904796c 100644 --- a/system/Helpers/Array/ArrayHelper.php +++ b/system/Helpers/Array/ArrayHelper.php @@ -35,7 +35,7 @@ final class ArrayHelper * * @param string $index The index as dot array syntax. * - * @return array|bool|int|object|string|null + * @return mixed */ public static function dotSearch(string $index, array $array) { @@ -68,7 +68,7 @@ private static function convertToArray(string $index): array * * @used-by dotSearch() * - * @return array|bool|float|int|object|string|null + * @return mixed */ private static function arraySearchDot(array $indexes, array $array) { diff --git a/system/Helpers/array_helper.php b/system/Helpers/array_helper.php index 837a612e4cef..4d5d12488d78 100644 --- a/system/Helpers/array_helper.php +++ b/system/Helpers/array_helper.php @@ -20,7 +20,7 @@ * Searches an array through dot syntax. Supports * wildcard searches, like foo.*.bar * - * @return array|bool|int|object|string|null + * @return mixed */ function dot_array_search(string $index, array $array) { @@ -34,7 +34,7 @@ function dot_array_search(string $index, array $array) * * @param int|string $key * - * @return array|bool|float|int|object|string|null + * @return mixed */ function array_deep_search($key, array $array) { @@ -43,8 +43,12 @@ function array_deep_search($key, array $array) } foreach ($array as $value) { - if (is_array($value) && ($result = array_deep_search($key, $value))) { - return $result; + if (is_array($value)) { + $result = array_deep_search($key, $value); + + if ($result !== null) { + return $result; + } } } diff --git a/system/I18n/TimeTrait.php b/system/I18n/TimeTrait.php index 9daa1f9b7848..04499668f5ed 100644 --- a/system/I18n/TimeTrait.php +++ b/system/I18n/TimeTrait.php @@ -1211,7 +1211,7 @@ public function __toString(): string * * @param string $name * - * @return array|bool|DateTimeInterface|DateTimeZone|int|IntlCalendar|self|string|null + * @return array|bool|DateTimeInterface|DateTimeZone|int|IntlCalendar|self|string|null */ public function __get($name) { diff --git a/system/Model.php b/system/Model.php index 3599ba963176..0d3c55ee0568 100644 --- a/system/Model.php +++ b/system/Model.php @@ -714,7 +714,7 @@ public function update($id = null, $row = null): bool /** * Provides/instantiates the builder/db connection and model's table/primary key names and return type. * - * @return array|BaseBuilder|bool|float|int|object|string|null + * @return mixed */ public function __get(string $name) { @@ -741,7 +741,7 @@ public function __isset(string $name): bool * Provides direct access to method in the builder (if available) * and the database connection. * - * @return $this|array|BaseBuilder|bool|float|int|object|string|null + * @return mixed */ public function __call(string $name, array $params) { diff --git a/system/Test/Mock/MockConnection.php b/system/Test/Mock/MockConnection.php index d14160595333..7f6aa5240e27 100644 --- a/system/Test/Mock/MockConnection.php +++ b/system/Test/Mock/MockConnection.php @@ -70,7 +70,7 @@ public function shouldReturn(string $method, $return) * Should automatically handle different connections for read/write * queries if needed. * - * @param mixed $binds + * @param array|string|null $binds * * @return BaseResult|bool|Query * diff --git a/system/Traits/ConditionalTrait.php b/system/Traits/ConditionalTrait.php index 6835de6c3a4f..b24fbdcd1b0c 100644 --- a/system/Traits/ConditionalTrait.php +++ b/system/Traits/ConditionalTrait.php @@ -23,7 +23,6 @@ trait ConditionalTrait * @param TWhen $condition * @param callable(self, TWhen): mixed $callback * @param (callable(self): mixed)|null $defaultCallback - * @param mixed $condition * * @return $this */ @@ -46,7 +45,6 @@ public function when($condition, callable $callback, ?callable $defaultCallback * @param TWhenNot $condition * @param callable(self, TWhenNot): mixed $callback * @param (callable(self): mixed)|null $defaultCallback - * @param mixed $condition * * @return $this */ diff --git a/system/Validation/Rules.php b/system/Validation/Rules.php index 17d3ced44e44..7617b283f099 100644 --- a/system/Validation/Rules.php +++ b/system/Validation/Rules.php @@ -316,7 +316,7 @@ public function not_in_list($value, string $list): bool } /** - * @param array|bool|float|int|object|string|null $str + * @param mixed $str */ public function required($str = null): bool { @@ -454,10 +454,10 @@ public function required_without( /** * The field exists in $data. * - * @param array|bool|float|int|object|string|null $value The field value. - * @param string|null $param The rule's parameter. - * @param array $data The data to be validated. - * @param string|null $field The field name. + * @param mixed $value The field value. + * @param string|null $param The rule's parameter. + * @param array $data The data to be validated. + * @param string|null $field The field name. */ public function field_exists( $value = null, diff --git a/system/Validation/StrictRules/CreditCardRules.php b/system/Validation/StrictRules/CreditCardRules.php index 6512568317ed..fd28e4454095 100644 --- a/system/Validation/StrictRules/CreditCardRules.php +++ b/system/Validation/StrictRules/CreditCardRules.php @@ -42,7 +42,7 @@ public function __construct() * 'cc_num' => 'valid_cc_number[visa]' * ]; * - * @param array|bool|float|int|object|string|null $ccNumber + * @param mixed $ccNumber */ public function valid_cc_number($ccNumber, string $type): bool { diff --git a/system/Validation/StrictRules/FormatRules.php b/system/Validation/StrictRules/FormatRules.php index 0363f4be4afc..8557a6de070d 100644 --- a/system/Validation/StrictRules/FormatRules.php +++ b/system/Validation/StrictRules/FormatRules.php @@ -32,7 +32,7 @@ public function __construct() /** * Alpha * - * @param array|bool|float|int|object|string|null $str + * @param mixed $str */ public function alpha($str = null): bool { @@ -46,7 +46,7 @@ public function alpha($str = null): bool /** * Alpha with spaces. * - * @param array|bool|float|int|object|string|null $value Value. + * @param mixed $value Value. * * @return bool True if alpha with spaces, else false. */ @@ -62,7 +62,7 @@ public function alpha_space($value = null): bool /** * Alphanumeric with underscores and dashes * - * @param array|bool|float|int|object|string|null $str + * @param mixed $str */ public function alpha_dash($str = null): bool { @@ -84,7 +84,7 @@ public function alpha_dash($str = null): bool * _ underscore, + plus, = equals, | vertical bar, : colon, . period * ~ ! # $ % & * - _ + = | : . * - * @param array|bool|float|int|object|string|null $str + * @param mixed $str * * @return bool */ @@ -104,7 +104,7 @@ public function alpha_numeric_punct($str) /** * Alphanumeric * - * @param array|bool|float|int|object|string|null $str + * @param mixed $str */ public function alpha_numeric($str = null): bool { @@ -122,7 +122,7 @@ public function alpha_numeric($str = null): bool /** * Alphanumeric w/ spaces * - * @param array|bool|float|int|object|string|null $str + * @param mixed $str */ public function alpha_numeric_space($str = null): bool { @@ -140,7 +140,7 @@ public function alpha_numeric_space($str = null): bool /** * Any type of string * - * @param array|bool|float|int|object|string|null $str + * @param mixed $str */ public function string($str = null): bool { @@ -150,7 +150,7 @@ public function string($str = null): bool /** * Decimal number * - * @param array|bool|float|int|object|string|null $str + * @param mixed $str */ public function decimal($str = null): bool { @@ -168,7 +168,7 @@ public function decimal($str = null): bool /** * String of hexidecimal characters * - * @param array|bool|float|int|object|string|null $str + * @param mixed $str */ public function hex($str = null): bool { @@ -186,7 +186,7 @@ public function hex($str = null): bool /** * Integer * - * @param array|bool|float|int|object|string|null $str + * @param mixed $str */ public function integer($str = null): bool { @@ -204,7 +204,7 @@ public function integer($str = null): bool /** * Is a Natural number (0,1,2,3, etc.) * - * @param array|bool|float|int|object|string|null $str + * @param mixed $str */ public function is_natural($str = null): bool { @@ -222,7 +222,7 @@ public function is_natural($str = null): bool /** * Is a Natural number, but not a zero (1,2,3, etc.) * - * @param array|bool|float|int|object|string|null $str + * @param mixed $str */ public function is_natural_no_zero($str = null): bool { @@ -240,7 +240,7 @@ public function is_natural_no_zero($str = null): bool /** * Numeric * - * @param array|bool|float|int|object|string|null $str + * @param mixed $str */ public function numeric($str = null): bool { @@ -258,7 +258,7 @@ public function numeric($str = null): bool /** * Compares value against a regular expression pattern. * - * @param array|bool|float|int|object|string|null $str + * @param mixed $str */ public function regex_match($str, string $pattern): bool { @@ -275,7 +275,7 @@ public function regex_match($str, string $pattern): bool * * @see http://php.net/manual/en/datetimezone.listidentifiers.php * - * @param array|bool|float|int|object|string|null $str + * @param mixed $str */ public function timezone($str = null): bool { @@ -292,7 +292,7 @@ public function timezone($str = null): bool * Tests a string for characters outside of the Base64 alphabet * as defined by RFC 2045 http://www.faqs.org/rfcs/rfc2045 * - * @param array|bool|float|int|object|string|null $str + * @param mixed $str */ public function valid_base64($str = null): bool { @@ -306,7 +306,7 @@ public function valid_base64($str = null): bool /** * Valid JSON * - * @param array|bool|float|int|object|string|null $str + * @param mixed $str */ public function valid_json($str = null): bool { @@ -320,7 +320,7 @@ public function valid_json($str = null): bool /** * Checks for a correctly formatted email address * - * @param array|bool|float|int|object|string|null $str + * @param mixed $str */ public function valid_email($str = null): bool { @@ -337,7 +337,7 @@ public function valid_email($str = null): bool * Example: * valid_emails[one@example.com,two@example.com] * - * @param array|bool|float|int|object|string|null $str + * @param mixed $str */ public function valid_emails($str = null): bool { @@ -351,8 +351,8 @@ public function valid_emails($str = null): bool /** * Validate an IP address (human readable format or binary string - inet_pton) * - * @param array|bool|float|int|object|string|null $ip - * @param string|null $which IP protocol: 'ipv4' or 'ipv6' + * @param mixed $ip + * @param string|null $which IP protocol: 'ipv4' or 'ipv6' */ public function valid_ip($ip = null, ?string $which = null): bool { @@ -369,7 +369,7 @@ public function valid_ip($ip = null, ?string $which = null): bool * Warning: this rule will pass basic strings like * "banana"; use valid_url_strict for a stricter rule. * - * @param array|bool|float|int|object|string|null $str + * @param mixed $str */ public function valid_url($str = null): bool { @@ -383,8 +383,8 @@ public function valid_url($str = null): bool /** * Checks a URL to ensure it's formed correctly. * - * @param array|bool|float|int|object|string|null $str - * @param string|null $validSchemes comma separated list of allowed schemes + * @param mixed $str + * @param string|null $validSchemes comma separated list of allowed schemes */ public function valid_url_strict($str = null, ?string $validSchemes = null): bool { @@ -398,7 +398,7 @@ public function valid_url_strict($str = null, ?string $validSchemes = null): boo /** * Checks for a valid date and matches a given date format * - * @param array|bool|float|int|object|string|null $str + * @param mixed $str */ public function valid_date($str = null, ?string $format = null): bool { diff --git a/system/Validation/StrictRules/Rules.php b/system/Validation/StrictRules/Rules.php index 0a03e6461dba..6d8dce14588b 100644 --- a/system/Validation/StrictRules/Rules.php +++ b/system/Validation/StrictRules/Rules.php @@ -33,8 +33,8 @@ public function __construct() /** * The value does not match another field in $data. * - * @param array|bool|float|int|object|string|null $str - * @param array $data Other field/value pairs + * @param mixed $str + * @param array $data Other field/value pairs */ public function differs( $str, @@ -65,7 +65,7 @@ public function differs( /** * Equals the static value provided. * - * @param array|bool|float|int|object|string|null $str + * @param mixed $str */ public function equals($str, string $val): bool { @@ -76,7 +76,7 @@ public function equals($str, string $val): bool * Returns true if $str is $val characters long. * $val = "5" (one) | "5,8,12" (multiple values) * - * @param array|bool|float|int|object|string|null $str + * @param mixed $str */ public function exact_length($str, string $val): bool { @@ -94,7 +94,7 @@ public function exact_length($str, string $val): bool /** * Greater than * - * @param array|bool|float|int|object|string|null $str expects int|string + * @param mixed $str expects int|string */ public function greater_than($str, string $min): bool { @@ -112,7 +112,7 @@ public function greater_than($str, string $min): bool /** * Equal to or Greater than * - * @param array|bool|float|int|object|string|null $str expects int|string + * @param mixed $str expects int|string */ public function greater_than_equal_to($str, string $min): bool { @@ -137,7 +137,7 @@ public function greater_than_equal_to($str, string $min): bool * is_not_unique[table.field,where_field,where_value] * is_not_unique[menu.id,active,1] * - * @param array|bool|float|int|object|string|null $str + * @param mixed $str */ public function is_not_unique($str, string $field, array $data): bool { @@ -151,7 +151,7 @@ public function is_not_unique($str, string $field, array $data): bool /** * Value should be within an array of values * - * @param array|bool|float|int|object|string|null $value + * @param mixed $value */ public function in_list($value, string $list): bool { @@ -176,7 +176,7 @@ public function in_list($value, string $list): bool * is_unique[table.field,ignore_field,ignore_value] * is_unique[users.email,id,5] * - * @param array|bool|float|int|object|string|null $str + * @param mixed $str */ public function is_unique($str, string $field, array $data): bool { @@ -190,7 +190,7 @@ public function is_unique($str, string $field, array $data): bool /** * Less than * - * @param array|bool|float|int|object|string|null $str expects int|string + * @param mixed $str expects int|string */ public function less_than($str, string $max): bool { @@ -208,7 +208,7 @@ public function less_than($str, string $max): bool /** * Equal to or Less than * - * @param array|bool|float|int|object|string|null $str expects int|string + * @param mixed $str expects int|string */ public function less_than_equal_to($str, string $max): bool { @@ -226,8 +226,8 @@ public function less_than_equal_to($str, string $max): bool /** * Matches the value of another field in $data. * - * @param array|bool|float|int|object|string|null $str - * @param array $data Other field/value pairs + * @param mixed $str + * @param array $data Other field/value pairs */ public function matches( $str, @@ -258,7 +258,7 @@ public function matches( /** * Returns true if $str is $val or fewer characters in length. * - * @param array|bool|float|int|object|string|null $str + * @param mixed $str */ public function max_length($str, string $val): bool { @@ -276,7 +276,7 @@ public function max_length($str, string $val): bool /** * Returns true if $str is at least $val length. * - * @param array|bool|float|int|object|string|null $str + * @param mixed $str */ public function min_length($str, string $val): bool { @@ -294,7 +294,7 @@ public function min_length($str, string $val): bool /** * Does not equal the static value provided. * - * @param array|bool|float|int|object|string|null $str + * @param mixed $str */ public function not_equals($str, string $val): bool { @@ -304,7 +304,7 @@ public function not_equals($str, string $val): bool /** * Value should not be within an array of values. * - * @param array|bool|float|int|object|string|null $value + * @param mixed $value */ public function not_in_list($value, string $list): bool { @@ -324,7 +324,7 @@ public function not_in_list($value, string $list): bool } /** - * @param array|bool|float|int|object|string|null $str + * @param mixed $str */ public function required($str = null): bool { @@ -339,9 +339,9 @@ public function required($str = null): bool * * required_with[password] * - * @param array|bool|float|int|object|string|null $str - * @param string|null $fields List of fields that we should check if present - * @param array $data Complete list of fields from the form + * @param mixed $str + * @param string|null $fields List of fields that we should check if present + * @param array $data Complete list of fields from the form */ public function required_with($str = null, ?string $fields = null, array $data = []): bool { @@ -356,9 +356,9 @@ public function required_with($str = null, ?string $fields = null, array $data = * * required_without[id,email] * - * @param array|bool|float|int|object|string|null $str - * @param string|null $otherFields The param fields of required_without[]. - * @param string|null $field This rule param fields aren't present, this field is required. + * @param mixed $str + * @param string|null $otherFields The param fields of required_without[]. + * @param string|null $field This rule param fields aren't present, this field is required. */ public function required_without( $str = null, @@ -373,10 +373,10 @@ public function required_without( /** * The field exists in $data. * - * @param array|bool|float|int|object|string|null $value The field value. - * @param string|null $param The rule's parameter. - * @param array $data The data to be validated. - * @param string|null $field The field name. + * @param mixed $value The field value. + * @param string|null $param The rule's parameter. + * @param array $data The data to be validated. + * @param string|null $field The field name. */ public function field_exists( $value = null, diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php index 6ca3233f746f..38210fbdb705 100644 --- a/system/Validation/Validation.php +++ b/system/Validation/Validation.php @@ -242,10 +242,10 @@ private static function getRegex(string $field): string * Runs the validation process, returning true or false determining whether * validation was successful or not. * - * @param array|bool|float|int|object|string|null $value The data to validate. - * @param array|string $rules The validation rules. - * @param list $errors The custom error message. - * @param string|null $dbGroup The database group to use. + * @param mixed $value The data to validate. + * @param array|string $rules The validation rules. + * @param list $errors The custom error message. + * @param string|null $dbGroup The database group to use. */ public function check($value, $rules, array $errors = [], $dbGroup = null): bool { @@ -1000,8 +1000,7 @@ protected function splitRules(string $rules): array * Entry point: allocates a single accumulator and delegates to the * recursive collector, so no intermediate arrays are built or unpacked. * - * @param list $segments - * @param array|mixed $current + * @param list $segments * * @return list */ @@ -1019,10 +1018,9 @@ private function walkForAllPossiblePaths(array $segments, mixed $current, string * paths where the key is genuinely absent are recorded - intermediate * missing segments are silently skipped so `*` never appears in a result. * - * @param list $segments - * @param int<0, max> $segmentCount - * @param array|mixed $current - * @param list $result + * @param list $segments + * @param int<0, max> $segmentCount + * @param list $result */ private function collectMissingPaths( array $segments, diff --git a/system/Validation/ValidationInterface.php b/system/Validation/ValidationInterface.php index 997bfb2bc0a4..836516d561b8 100644 --- a/system/Validation/ValidationInterface.php +++ b/system/Validation/ValidationInterface.php @@ -35,10 +35,10 @@ public function run(?array $data = null, ?string $group = null, $dbGroup = null) * Check; runs the validation process, returning true or false * determining whether or not validation was successful. * - * @param array|bool|float|int|object|string|null $value Value to validate. - * @param array|string $rules - * @param list $errors - * @param string|null $dbGroup The database group to use. + * @param mixed $value Value to validate. + * @param array|string $rules + * @param list $errors + * @param string|null $dbGroup The database group to use. * * @return bool True if valid, else false. */ diff --git a/system/View/Parser.php b/system/View/Parser.php index 9a2d7d9d83b1..5de115f8236b 100644 --- a/system/View/Parser.php +++ b/system/View/Parser.php @@ -748,7 +748,7 @@ public function removePlugin(string $alias) * Converts an object to an array, respecting any * toArray() methods on an object. * - * @param array|bool|float|int|object|string|null $value + * @param mixed $value * * @return array|bool|float|int|string|null */ diff --git a/utils/phpstan-baseline/empty.notAllowed.neon b/utils/phpstan-baseline/empty.notAllowed.neon index e526a441d344..a302b0433f39 100644 --- a/utils/phpstan-baseline/empty.notAllowed.neon +++ b/utils/phpstan-baseline/empty.notAllowed.neon @@ -1,4 +1,4 @@ -# total 214 errors +# total 213 errors parameters: ignoreErrors: diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 672ff3a32e43..8c8e2bbe900e 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 1944 errors +# total 1839 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/method.childParameterType.neon b/utils/phpstan-baseline/method.childParameterType.neon index d5b6014b15a6..21752412ee34 100644 --- a/utils/phpstan-baseline/method.childParameterType.neon +++ b/utils/phpstan-baseline/method.childParameterType.neon @@ -1,4 +1,4 @@ -# total 12 errors +# total 11 errors parameters: ignoreErrors: @@ -18,27 +18,22 @@ parameters: path: ../../system/Database/BaseResult.php - - message: '#^Parameter \#1 \$selectOverride \(bool\) of method CodeIgniter\\Database\\SQLSRV\\Builder\:\:compileSelect\(\) should be contravariant with parameter \$selectOverride \(mixed\) of method CodeIgniter\\Database\\BaseBuilder\:\:compileSelect\(\)$#' - count: 1 - path: ../../system/Database/SQLSRV/Builder.php - - - - message: '#^Parameter \#1 \$value \(bool\|int\|string\) of method CodeIgniter\\Entity\\Cast\\IntBoolCast\:\:set\(\) should be contravariant with parameter \$value \(array\|bool\|float\|int\|object\|string\|null\) of method CodeIgniter\\Entity\\Cast\\BaseCast\:\:set\(\)$#' + message: '#^Parameter \#1 \$value \(bool\|int\|string\) of method CodeIgniter\\Entity\\Cast\\IntBoolCast\:\:set\(\) should be contravariant with parameter \$value \(mixed\) of method CodeIgniter\\Entity\\Cast\\BaseCast\:\:set\(\)$#' count: 1 path: ../../system/Entity/Cast/IntBoolCast.php - - message: '#^Parameter \#1 \$value \(bool\|int\|string\) of method CodeIgniter\\Entity\\Cast\\IntBoolCast\:\:set\(\) should be contravariant with parameter \$value \(array\|bool\|float\|int\|object\|string\|null\) of method CodeIgniter\\Entity\\Cast\\CastInterface\:\:set\(\)$#' + message: '#^Parameter \#1 \$value \(bool\|int\|string\) of method CodeIgniter\\Entity\\Cast\\IntBoolCast\:\:set\(\) should be contravariant with parameter \$value \(mixed\) of method CodeIgniter\\Entity\\Cast\\CastInterface\:\:set\(\)$#' count: 1 path: ../../system/Entity/Cast/IntBoolCast.php - - message: '#^Parameter \#1 \$value \(int\) of method CodeIgniter\\Entity\\Cast\\IntBoolCast\:\:get\(\) should be contravariant with parameter \$value \(array\|bool\|float\|int\|object\|string\|null\) of method CodeIgniter\\Entity\\Cast\\BaseCast\:\:get\(\)$#' + message: '#^Parameter \#1 \$value \(int\) of method CodeIgniter\\Entity\\Cast\\IntBoolCast\:\:get\(\) should be contravariant with parameter \$value \(mixed\) of method CodeIgniter\\Entity\\Cast\\BaseCast\:\:get\(\)$#' count: 1 path: ../../system/Entity/Cast/IntBoolCast.php - - message: '#^Parameter \#1 \$value \(int\) of method CodeIgniter\\Entity\\Cast\\IntBoolCast\:\:get\(\) should be contravariant with parameter \$value \(array\|bool\|float\|int\|object\|string\|null\) of method CodeIgniter\\Entity\\Cast\\CastInterface\:\:get\(\)$#' + message: '#^Parameter \#1 \$value \(int\) of method CodeIgniter\\Entity\\Cast\\IntBoolCast\:\:get\(\) should be contravariant with parameter \$value \(mixed\) of method CodeIgniter\\Entity\\Cast\\CastInterface\:\:get\(\)$#' count: 1 path: ../../system/Entity/Cast/IntBoolCast.php diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index 571912cb7ad4..80d45074b96e 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 1257 errors +# total 1153 errors parameters: ignoreErrors: @@ -172,16 +172,6 @@ parameters: count: 1 path: ../../system/Common.php - - - message: '#^Function service\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Common.php - - - - message: '#^Function single_service\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Common.php - - message: '#^Function stringify_attributes\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' count: 1 @@ -207,11 +197,6 @@ parameters: count: 1 path: ../../system/Config/BaseConfig.php - - - message: '#^Method CodeIgniter\\Config\\BaseConfig\:\:initEnvValue\(\) has parameter \$property with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Config/BaseConfig.php - - message: '#^Property CodeIgniter\\Config\\BaseConfig\:\:\$registrars type has no value type specified in iterable type array\.$#' count: 1 @@ -232,11 +217,6 @@ parameters: count: 1 path: ../../system/Config/BaseService.php - - - message: '#^Method CodeIgniter\\Config\\BaseService\:\:getSharedInstance\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Config/BaseService.php - - message: '#^Property CodeIgniter\\Config\\BaseService\:\:\$services type has no value type specified in iterable type array\.$#' count: 1 @@ -447,11 +427,6 @@ parameters: count: 1 path: ../../system/Database/BaseBuilder.php - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:delete\(\) has parameter \$where with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:fieldsFromQuery\(\) return type has no value type specified in iterable type array\.$#' count: 1 @@ -787,11 +762,6 @@ parameters: count: 1 path: ../../system/Database/BaseConnection.php - - - message: '#^Method CodeIgniter\\Database\\BaseConnection\:\:__get\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseConnection.php - - message: '#^Method CodeIgniter\\Database\\BaseConnection\:\:callFunction\(\) has parameter \$params with no value type specified in iterable type array\.$#' count: 1 @@ -807,11 +777,6 @@ parameters: count: 1 path: ../../system/Database/BaseConnection.php - - - message: '#^Method CodeIgniter\\Database\\BaseConnection\:\:escape\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseConnection.php - - message: '#^Method CodeIgniter\\Database\\BaseConnection\:\:escape\(\) return type has no value type specified in iterable type array\.$#' count: 1 @@ -837,11 +802,6 @@ parameters: count: 1 path: ../../system/Database/BaseConnection.php - - - message: '#^Method CodeIgniter\\Database\\BaseConnection\:\:query\(\) has parameter \$binds with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseConnection.php - - message: '#^Method CodeIgniter\\Database\\BaseConnection\:\:setAliasedTables\(\) has parameter \$aliases with no value type specified in iterable type array\.$#' count: 1 @@ -1032,26 +992,11 @@ parameters: count: 1 path: ../../system/Database/ConnectionInterface.php - - - message: '#^Method CodeIgniter\\Database\\ConnectionInterface\:\:callFunction\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/ConnectionInterface.php - - - - message: '#^Method CodeIgniter\\Database\\ConnectionInterface\:\:escape\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/ConnectionInterface.php - - message: '#^Method CodeIgniter\\Database\\ConnectionInterface\:\:escape\(\) return type has no value type specified in iterable type array\.$#' count: 1 path: ../../system/Database/ConnectionInterface.php - - - message: '#^Method CodeIgniter\\Database\\ConnectionInterface\:\:query\(\) has parameter \$binds with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/ConnectionInterface.php - - message: '#^Method CodeIgniter\\Database\\ConnectionInterface\:\:table\(\) has parameter \$tableName with no value type specified in iterable type array\.$#' count: 1 @@ -1417,11 +1362,6 @@ parameters: count: 1 path: ../../system/Database/Postgre/Builder.php - - - message: '#^Method CodeIgniter\\Database\\Postgre\\Connection\:\:escape\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Postgre/Connection.php - - message: '#^Method CodeIgniter\\Database\\Postgre\\Connection\:\:escape\(\) return type has no value type specified in iterable type array\.$#' count: 1 @@ -2132,11 +2072,6 @@ parameters: count: 1 path: ../../system/Encryption/Encryption.php - - - message: '#^Method CodeIgniter\\Encryption\\Handlers\\BaseHandler\:\:__get\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Encryption/Handlers/BaseHandler.php - - message: '#^Method CodeIgniter\\Encryption\\Handlers\\OpenSSLHandler\:\:decrypt\(\) has parameter \$params with no value type specified in iterable type array\.$#' count: 1 @@ -2167,11 +2102,6 @@ parameters: count: 1 path: ../../system/Encryption/Handlers/SodiumHandler.php - - - message: '#^Method CodeIgniter\\Encryption\\KeyRotationDecorator\:\:__get\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Encryption/KeyRotationDecorator.php - - message: '#^Method CodeIgniter\\Encryption\\KeyRotationDecorator\:\:decrypt\(\) has parameter \$params with no value type specified in iterable type array\.$#' count: 1 @@ -2182,146 +2112,16 @@ parameters: count: 1 path: ../../system/Encryption/KeyRotationDecorator.php - - - message: '#^Method CodeIgniter\\Entity\\Cast\\ArrayCast\:\:get\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/ArrayCast.php - - message: '#^Method CodeIgniter\\Entity\\Cast\\ArrayCast\:\:get\(\) return type has no value type specified in iterable type array\.$#' count: 1 path: ../../system/Entity/Cast/ArrayCast.php - - - message: '#^Method CodeIgniter\\Entity\\Cast\\ArrayCast\:\:set\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/ArrayCast.php - - - - message: '#^Method CodeIgniter\\Entity\\Cast\\BaseCast\:\:get\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/BaseCast.php - - - - message: '#^Method CodeIgniter\\Entity\\Cast\\BaseCast\:\:get\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/BaseCast.php - - - - message: '#^Method CodeIgniter\\Entity\\Cast\\BaseCast\:\:set\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/BaseCast.php - - - - message: '#^Method CodeIgniter\\Entity\\Cast\\BaseCast\:\:set\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/BaseCast.php - - - - message: '#^Method CodeIgniter\\Entity\\Cast\\BooleanCast\:\:get\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/BooleanCast.php - - - - message: '#^Method CodeIgniter\\Entity\\Cast\\CSVCast\:\:get\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/CSVCast.php - - message: '#^Method CodeIgniter\\Entity\\Cast\\CSVCast\:\:get\(\) return type has no value type specified in iterable type array\.$#' count: 1 path: ../../system/Entity/Cast/CSVCast.php - - - message: '#^Method CodeIgniter\\Entity\\Cast\\CSVCast\:\:set\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/CSVCast.php - - - - message: '#^Method CodeIgniter\\Entity\\Cast\\CastInterface\:\:get\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/CastInterface.php - - - - message: '#^Method CodeIgniter\\Entity\\Cast\\CastInterface\:\:get\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/CastInterface.php - - - - message: '#^Method CodeIgniter\\Entity\\Cast\\CastInterface\:\:set\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/CastInterface.php - - - - message: '#^Method CodeIgniter\\Entity\\Cast\\CastInterface\:\:set\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/CastInterface.php - - - - message: '#^Method CodeIgniter\\Entity\\Cast\\DatetimeCast\:\:get\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/DatetimeCast.php - - - - message: '#^Method CodeIgniter\\Entity\\Cast\\EnumCast\:\:get\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/EnumCast.php - - - - message: '#^Method CodeIgniter\\Entity\\Cast\\EnumCast\:\:set\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/EnumCast.php - - - - message: '#^Method CodeIgniter\\Entity\\Cast\\FloatCast\:\:get\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/FloatCast.php - - - - message: '#^Method CodeIgniter\\Entity\\Cast\\IntegerCast\:\:get\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/IntegerCast.php - - - - message: '#^Method CodeIgniter\\Entity\\Cast\\JsonCast\:\:get\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/JsonCast.php - - - - message: '#^Method CodeIgniter\\Entity\\Cast\\JsonCast\:\:get\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/JsonCast.php - - - - message: '#^Method CodeIgniter\\Entity\\Cast\\JsonCast\:\:set\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/JsonCast.php - - - - message: '#^Method CodeIgniter\\Entity\\Cast\\ObjectCast\:\:get\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/ObjectCast.php - - - - message: '#^Method CodeIgniter\\Entity\\Cast\\StringCast\:\:get\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/StringCast.php - - - - message: '#^Method CodeIgniter\\Entity\\Cast\\TimestampCast\:\:get\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/TimestampCast.php - - - - message: '#^Method CodeIgniter\\Entity\\Cast\\TimestampCast\:\:get\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/TimestampCast.php - - - - message: '#^Method CodeIgniter\\Entity\\Cast\\URICast\:\:get\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/URICast.php - - message: '#^Method CodeIgniter\\Exceptions\\PageNotFoundException\:\:lang\(\) has parameter \$args with no value type specified in iterable type array\.$#' count: 1 @@ -2607,11 +2407,6 @@ parameters: count: 1 path: ../../system/HTTP/CURLRequest.php - - - message: '#^Method CodeIgniter\\HTTP\\CURLRequest\:\:setJSON\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CURLRequest.php - - message: '#^Method CodeIgniter\\HTTP\\CURLRequest\:\:setResponseHeaders\(\) has parameter \$headers with no value type specified in iterable type array\.$#' count: 1 @@ -2682,11 +2477,6 @@ parameters: count: 1 path: ../../system/HTTP/IncomingRequest.php - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getCookie\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getFileMultiple\(\) return type has no value type specified in iterable type array\.$#' count: 1 @@ -2707,11 +2497,6 @@ parameters: count: 1 path: ../../system/HTTP/IncomingRequest.php - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getGetPost\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getGet\(\) has parameter \$flags with no value type specified in iterable type array\.$#' count: 1 @@ -2722,16 +2507,6 @@ parameters: count: 1 path: ../../system/HTTP/IncomingRequest.php - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getGet\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getJSON\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getJsonVar\(\) has parameter \$flags with no value type specified in iterable type array\.$#' count: 1 @@ -2742,11 +2517,6 @@ parameters: count: 1 path: ../../system/HTTP/IncomingRequest.php - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getJsonVar\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getOldInput\(\) return type has no value type specified in iterable type array\.$#' count: 1 @@ -2762,11 +2532,6 @@ parameters: count: 1 path: ../../system/HTTP/IncomingRequest.php - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getPostGet\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getPost\(\) has parameter \$flags with no value type specified in iterable type array\.$#' count: 1 @@ -2777,11 +2542,6 @@ parameters: count: 1 path: ../../system/HTTP/IncomingRequest.php - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getPost\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getRawInputVar\(\) has parameter \$flags with no value type specified in iterable type array\.$#' count: 1 @@ -2792,11 +2552,6 @@ parameters: count: 1 path: ../../system/HTTP/IncomingRequest.php - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getRawInputVar\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getRawInput\(\) return type has no value type specified in iterable type array\.$#' count: 1 @@ -2812,11 +2567,6 @@ parameters: count: 1 path: ../../system/HTTP/IncomingRequest.php - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getVar\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:negotiate\(\) has parameter \$supported with no value type specified in iterable type array\.$#' count: 1 @@ -2952,11 +2702,6 @@ parameters: count: 1 path: ../../system/HTTP/Request.php - - - message: '#^Method CodeIgniter\\HTTP\\Request\:\:fetchGlobal\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Request.php - - message: '#^Method CodeIgniter\\HTTP\\Request\:\:getEnv\(\) has parameter \$flags with no value type specified in iterable type array\.$#' count: 1 @@ -3112,11 +2857,6 @@ parameters: count: 1 path: ../../system/Helpers/Array/ArrayHelper.php - - - message: '#^Method CodeIgniter\\Helpers\\Array\\ArrayHelper\:\:arraySearchDot\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/Array/ArrayHelper.php - - message: '#^Method CodeIgniter\\Helpers\\Array\\ArrayHelper\:\:dotKeyExists\(\) has parameter \$array with no value type specified in iterable type array\.$#' count: 1 @@ -3127,11 +2867,6 @@ parameters: count: 1 path: ../../system/Helpers/Array/ArrayHelper.php - - - message: '#^Method CodeIgniter\\Helpers\\Array\\ArrayHelper\:\:dotSearch\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/Array/ArrayHelper.php - - message: '#^Method CodeIgniter\\Helpers\\Array\\ArrayHelper\:\:groupBy\(\) has parameter \$array with no value type specified in iterable type array\.$#' count: 1 @@ -3172,11 +2907,6 @@ parameters: count: 1 path: ../../system/Helpers/array_helper.php - - - message: '#^Function array_deep_search\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/array_helper.php - - message: '#^Function array_flatten_with_dots\(\) has parameter \$array with no value type specified in iterable type iterable\.$#' count: 1 @@ -3217,11 +2947,6 @@ parameters: count: 1 path: ../../system/Helpers/array_helper.php - - - message: '#^Function dot_array_search\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/array_helper.php - - message: '#^Function directory_map\(\) return type has no value type specified in iterable type array\.$#' count: 1 @@ -3587,16 +3312,6 @@ parameters: count: 1 path: ../../system/HotReloader/IteratorFilter.php - - - message: '#^Method CodeIgniter\\I18n\\Time\:\:__get\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/I18n/Time.php - - - - message: '#^Method CodeIgniter\\I18n\\TimeLegacy\:\:__get\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/I18n/TimeLegacy.php - - message: '#^Method CodeIgniter\\Images\\Handlers\\BaseHandler\:\:__call\(\) has parameter \$args with no value type specified in iterable type array\.$#' count: 1 @@ -4167,11 +3882,6 @@ parameters: count: 1 path: ../../system/Validation/Rules.php - - - message: '#^Method CodeIgniter\\Validation\\Rules\:\:field_exists\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Rules.php - - message: '#^Method CodeIgniter\\Validation\\Rules\:\:is_not_unique\(\) has parameter \$data with no value type specified in iterable type array\.$#' count: 1 @@ -4187,11 +3897,6 @@ parameters: count: 1 path: ../../system/Validation/Rules.php - - - message: '#^Method CodeIgniter\\Validation\\Rules\:\:required\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Rules.php - - message: '#^Method CodeIgniter\\Validation\\Rules\:\:required_with\(\) has parameter \$data with no value type specified in iterable type array\.$#' count: 1 @@ -4202,266 +3907,46 @@ parameters: count: 1 path: ../../system/Validation/Rules.php - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\CreditCardRules\:\:valid_cc_number\(\) has parameter \$ccNumber with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/CreditCardRules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\FormatRules\:\:alpha\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/FormatRules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\FormatRules\:\:alpha_dash\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/FormatRules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\FormatRules\:\:alpha_numeric\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/FormatRules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\FormatRules\:\:alpha_numeric_punct\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/FormatRules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\FormatRules\:\:alpha_numeric_space\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/FormatRules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\FormatRules\:\:alpha_space\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/FormatRules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\FormatRules\:\:decimal\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/FormatRules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\FormatRules\:\:hex\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/FormatRules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\FormatRules\:\:integer\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/FormatRules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\FormatRules\:\:is_natural\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/FormatRules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\FormatRules\:\:is_natural_no_zero\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/FormatRules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\FormatRules\:\:numeric\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/FormatRules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\FormatRules\:\:regex_match\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/FormatRules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\FormatRules\:\:string\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/FormatRules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\FormatRules\:\:timezone\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/FormatRules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\FormatRules\:\:valid_base64\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/FormatRules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\FormatRules\:\:valid_date\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/FormatRules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\FormatRules\:\:valid_email\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/FormatRules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\FormatRules\:\:valid_emails\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/FormatRules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\FormatRules\:\:valid_ip\(\) has parameter \$ip with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/FormatRules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\FormatRules\:\:valid_json\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/FormatRules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\FormatRules\:\:valid_url\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/FormatRules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\FormatRules\:\:valid_url_strict\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/FormatRules.php - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:differs\(\) has parameter \$data with no value type specified in iterable type array\.$#' count: 1 path: ../../system/Validation/StrictRules/Rules.php - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:differs\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/Rules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:equals\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/Rules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:exact_length\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/Rules.php - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:field_exists\(\) has parameter \$data with no value type specified in iterable type array\.$#' count: 1 path: ../../system/Validation/StrictRules/Rules.php - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:field_exists\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/Rules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:greater_than\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/Rules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:greater_than_equal_to\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/Rules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:in_list\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/Rules.php - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:is_not_unique\(\) has parameter \$data with no value type specified in iterable type array\.$#' count: 1 path: ../../system/Validation/StrictRules/Rules.php - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:is_not_unique\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/Rules.php - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:is_unique\(\) has parameter \$data with no value type specified in iterable type array\.$#' count: 1 path: ../../system/Validation/StrictRules/Rules.php - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:is_unique\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/Rules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:less_than\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/Rules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:less_than_equal_to\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/Rules.php - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:matches\(\) has parameter \$data with no value type specified in iterable type array\.$#' count: 1 path: ../../system/Validation/StrictRules/Rules.php - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:matches\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/Rules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:max_length\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/Rules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:min_length\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/Rules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:not_equals\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/Rules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:not_in_list\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/Rules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:required\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/Rules.php - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:required_with\(\) has parameter \$data with no value type specified in iterable type array\.$#' count: 1 path: ../../system/Validation/StrictRules/Rules.php - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:required_with\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/Rules.php - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:required_without\(\) has parameter \$data with no value type specified in iterable type array\.$#' count: 1 path: ../../system/Validation/StrictRules/Rules.php - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:required_without\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/Rules.php - - message: '#^Method CodeIgniter\\Validation\\Validation\:\:check\(\) has parameter \$rules with no value type specified in iterable type array\.$#' count: 1 path: ../../system/Validation/Validation.php - - - message: '#^Method CodeIgniter\\Validation\\Validation\:\:check\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - message: '#^Method CodeIgniter\\Validation\\Validation\:\:fillPlaceholders\(\) has parameter \$data with no value type specified in iterable type array\.$#' count: 1 @@ -4627,11 +4112,6 @@ parameters: count: 1 path: ../../system/Validation/ValidationInterface.php - - - message: '#^Method CodeIgniter\\Validation\\ValidationInterface\:\:check\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/ValidationInterface.php - - message: '#^Method CodeIgniter\\Validation\\ValidationInterface\:\:getRules\(\) return type has no value type specified in iterable type array\.$#' count: 1 diff --git a/utils/phpstan-baseline/return.type.neon b/utils/phpstan-baseline/return.type.neon index b181bcabebd9..cd3da83ccce5 100644 --- a/utils/phpstan-baseline/return.type.neon +++ b/utils/phpstan-baseline/return.type.neon @@ -1,4 +1,4 @@ -# total 2 errors +# total 3 errors parameters: ignoreErrors: @@ -7,6 +7,11 @@ parameters: count: 1 path: ../../system/Database/BaseBuilder.php + - + message: '#^Method CodeIgniter\\Entity\\Cast\\DatetimeCast\:\:get\(\) should return CodeIgniter\\I18n\\Time but returns mixed\.$#' + count: 1 + path: ../../system/Entity/Cast/DatetimeCast.php + - message: '#^Method CodeIgniter\\Router\\Router\:\:getRouteAttributes\(\) should return array\{class\: list\, method\: list\\} but returns array\{class\: list\, method\: list\\}\.$#' count: 1 From ecbf044666bed41d23f07518096d9843fe6c08b0 Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Tue, 30 Jun 2026 12:49:32 +0200 Subject: [PATCH 50/65] Merge commit from fork * fix: validate forwarded HTTPS headers against trusted proxies * apply note about dual-stack servers * apply method visibility change --- system/HTTP/IncomingRequest.php | 4 + system/HTTP/RequestTrait.php | 148 ++++++++------ tests/system/HTTP/IncomingRequestTest.php | 84 +++++++- tests/system/HTTP/RequestTest.php | 186 ++++++++++-------- user_guide_src/source/changelogs/v4.7.4.rst | 11 ++ .../source/incoming/incomingrequest.rst | 15 ++ .../source/installation/upgrade_474.rst | 27 +++ utils/phpstan-baseline/loader.neon | 2 +- utils/phpstan-baseline/method.notFound.neon | 4 +- 9 files changed, 333 insertions(+), 148 deletions(-) diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index a4a4c357ac74..457d4da0fa84 100644 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -272,6 +272,10 @@ public function isSecure(): bool return true; } + if (! $this->isFromTrustedProxy()) { + return false; + } + if ($this->hasHeader('X-Forwarded-Proto') && $this->header('X-Forwarded-Proto')->getValue() === 'https') { return true; } diff --git a/system/HTTP/RequestTrait.php b/system/HTTP/RequestTrait.php index 67c5382adc1c..9011f4148eba 100644 --- a/system/HTTP/RequestTrait.php +++ b/system/HTTP/RequestTrait.php @@ -87,67 +87,7 @@ public function getIPAddress(): string // @TODO Extract all this IP address logic to another class. foreach ($proxyIPs as $proxyIP => $header) { - // Check if we have an IP address or a subnet - if (! str_contains($proxyIP, '/')) { - // An IP address (and not a subnet) is specified. - // We can compare right away. - if ($proxyIP === $this->ipAddress) { - $spoof = $this->getClientIP($header); - - if ($spoof !== null) { - $this->ipAddress = $spoof; - break; - } - } - - continue; - } - - // We have a subnet ... now the heavy lifting begins - if (! isset($separator)) { - $separator = $ipValidator($this->ipAddress, 'ipv6') ? ':' : '.'; - } - - // If the proxy entry doesn't match the IP protocol - skip it - if (! str_contains($proxyIP, $separator)) { - continue; - } - - // Convert the REMOTE_ADDR IP address to binary, if needed - if (! isset($ip, $sprintf)) { - if ($separator === ':') { - // Make sure we're having the "full" IPv6 format - $ip = explode(':', str_replace('::', str_repeat(':', 9 - substr_count($this->ipAddress, ':')), $this->ipAddress)); - - for ($j = 0; $j < 8; $j++) { - $ip[$j] = intval($ip[$j], 16); - } - - $sprintf = '%016b%016b%016b%016b%016b%016b%016b%016b'; - } else { - $ip = explode('.', $this->ipAddress); - $sprintf = '%08b%08b%08b%08b'; - } - - $ip = vsprintf($sprintf, $ip); - } - - // Split the netmask length off the network address - sscanf($proxyIP, '%[^/]/%d', $netaddr, $masklen); - - // Again, an IPv6 address is most likely in a compressed form - if ($separator === ':') { - $netaddr = explode(':', str_replace('::', str_repeat(':', 9 - substr_count($netaddr, ':')), $netaddr)); - - for ($i = 0; $i < 8; $i++) { - $netaddr[$i] = intval($netaddr[$i], 16); - } - } else { - $netaddr = explode('.', $netaddr); - } - - // Convert to binary and finally compare - if (strncmp($ip, vsprintf($sprintf, $netaddr), $masklen) === 0) { + if ($this->checkIPAgainstProxy($this->ipAddress, (string) $proxyIP)) { $spoof = $this->getClientIP($header); if ($spoof !== null) { @@ -192,6 +132,92 @@ private function getClientIP(string $header): ?string return $spoof; } + /** + * Checks if the request comes from one of the trusted proxies + * configured in Config\App::$proxyIPs. + */ + protected function isFromTrustedProxy(): bool + { + $proxyIPs = $this->config->proxyIPs; + + if (! is_array($proxyIPs) || $proxyIPs === []) { + return false; + } + + $remoteAddr = $this->getServer('REMOTE_ADDR'); + + if (! is_string($remoteAddr) || $remoteAddr === '') { + return false; + } + + foreach (array_keys($proxyIPs) as $proxyIP) { + if ($this->checkIPAgainstProxy($remoteAddr, (string) $proxyIP)) { + return true; + } + } + + return false; + } + + /** + * Checks if the given IP address matches the trusted proxy entry, + * which may be a single IP address or a subnet in CIDR notation. + * Supports both IPv4 and IPv6. + */ + private function checkIPAgainstProxy(string $ip, string $proxyIP): bool + { + $maskLength = null; + + if (str_contains($proxyIP, '/')) { + [$proxyIP, $mask] = explode('/', $proxyIP, 2); + + if ($mask === '' || ! ctype_digit($mask)) { + return false; + } + + $maskLength = (int) $mask; + } + + $binaryIP = inet_pton($ip); + $binaryProxy = inet_pton($proxyIP); + + if ($binaryIP === false || $binaryProxy === false) { + return false; + } + + // If the proxy entry doesn't match the IP protocol - no match + if (strlen($binaryIP) !== strlen($binaryProxy)) { + return false; + } + + if ($maskLength === null) { + return $binaryIP === $binaryProxy; + } + + if ($maskLength > strlen($binaryIP) * 8) { + return false; + } + + if ($maskLength === 0) { + return true; + } + + $fullBytes = intdiv($maskLength, 8); + $remainingBits = $maskLength % 8; + + if ($fullBytes > 0 && strncmp($binaryIP, $binaryProxy, $fullBytes) !== 0) { + return false; + } + + if ($remainingBits > 0) { + $bitmask = 0xFF & (0xFF << (8 - $remainingBits)); + + return (ord($binaryIP[$fullBytes]) & $bitmask) === (ord($binaryProxy[$fullBytes]) & $bitmask); + } + + return true; + } + /** * Fetch an item from the $_SERVER array. * diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index 5a7d5f29dfe2..d4a15da40537 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -762,16 +762,86 @@ public function testIsSecure(): void $this->assertTrue($this->request->isSecure()); } - public function testIsSecureFrontEnd(): void - { - $this->request->appendHeader('Front-End-Https', 'on'); - $this->assertTrue($this->request->isSecure()); + /** + * @param array $proxyIPs + */ + #[DataProvider('provideIsSecureWithForwardedHeaders')] + public function testIsSecureWithForwardedHeaders( + string $header, + string $value, + string $remoteAddr, + array $proxyIPs, + bool $expected, + ): void { + service('superglobals')->setServer('REMOTE_ADDR', $remoteAddr); + + $config = new App(); + $config->proxyIPs = $proxyIPs; + + $request = $this->createRequest($config); + $request->appendHeader($header, $value); + + $this->assertSame($expected, $request->isSecure()); } - public function testIsSecureForwarded(): void + /** + * @return iterable, bool}> + */ + public static function provideIsSecureWithForwardedHeaders(): iterable { - $this->request->appendHeader('X-Forwarded-Proto', 'https'); - $this->assertTrue($this->request->isSecure()); + yield from [ + '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 no trusted proxies' => [ + 'X-Forwarded-Proto', 'https', '10.0.1.200', [], false, + ], + 'X-Forwarded-Proto untrusted proxy IP' => [ + 'X-Forwarded-Proto', 'https', '10.0.1.201', ['10.0.1.200' => 'X-Forwarded-For'], false, + ], + 'X-Forwarded-Proto trusted subnet' => [ + 'X-Forwarded-Proto', 'https', '192.168.5.21', ['192.168.5.0/24' => 'X-Forwarded-For'], true, + ], + 'X-Forwarded-Proto out of trusted subnet' => [ + 'X-Forwarded-Proto', 'https', '192.168.6.21', ['192.168.5.0/24' => 'X-Forwarded-For'], false, + ], + 'X-Forwarded-Proto trusted IPv6 subnet' => [ + 'X-Forwarded-Proto', 'https', '2001:db8::5', ['2001:db8::/32' => 'X-Forwarded-For'], true, + ], + 'X-Forwarded-Proto out of trusted IPv6 subnet' => [ + 'X-Forwarded-Proto', 'https', '2001:db9::5', ['2001:db8::/32' => 'X-Forwarded-For'], false, + ], + 'X-Forwarded-Proto trusted proxy but http' => [ + 'X-Forwarded-Proto', 'http', '10.0.1.200', ['10.0.1.200' => 'X-Forwarded-For'], false, + ], + 'Front-End-Https trusted proxy IP' => [ + 'Front-End-Https', 'on', '10.0.1.200', ['10.0.1.200' => 'X-Forwarded-For'], true, + ], + 'Front-End-Https no trusted proxies' => [ + 'Front-End-Https', 'on', '10.0.1.200', [], false, + ], + 'Front-End-Https trusted proxy but off' => [ + 'Front-End-Https', 'off', '10.0.1.200', ['10.0.1.200' => 'X-Forwarded-For'], false, + ], + 'invalid proxy IP string' => [ + 'X-Forwarded-Proto', 'https', '10.0.1.200', ['not an ip' => 'X-Forwarded-For'], false, + ], + 'invalid proxy CIDR mask' => [ + 'X-Forwarded-Proto', 'https', '192.168.5.21', ['192.168.5.0/foo' => 'X-Forwarded-For'], false, + ], + 'empty proxy CIDR mask' => [ + 'X-Forwarded-Proto', 'https', '192.168.5.21', ['192.168.5.0/' => 'X-Forwarded-For'], false, + ], + 'negative proxy CIDR mask' => [ + 'X-Forwarded-Proto', 'https', '192.168.5.21', ['192.168.5.0/-1' => 'X-Forwarded-For'], false, + ], + 'out of range IPv4 proxy CIDR mask' => [ + 'X-Forwarded-Proto', 'https', '192.168.5.21', ['192.168.5.0/33' => 'X-Forwarded-For'], false, + ], + 'out of range IPv6 proxy CIDR mask' => [ + 'X-Forwarded-Proto', 'https', '2001:db8::5', ['2001:db8::/129' => 'X-Forwarded-For'], false, + ], + ]; } public function testUserAgent(): void diff --git a/tests/system/HTTP/RequestTest.php b/tests/system/HTTP/RequestTest.php index 0ccaf8083f4f..eb15506e6fdf 100644 --- a/tests/system/HTTP/RequestTest.php +++ b/tests/system/HTTP/RequestTest.php @@ -19,6 +19,7 @@ use CodeIgniter\Test\CIUnitTestCase; use Config\App; use PHPUnit\Framework\Attributes\BackupGlobals; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; /** @@ -567,95 +568,126 @@ public function testGetIPAddressNormal(): void $this->assertSame($expected, $this->request->getIPAddress()); } - public function testGetIPAddressThruProxy(): void - { - $expected = '123.123.123.123'; + /** + * @param array $proxyIPs + */ + #[DataProvider('provideGetIPAddressThruProxy')] + public function testGetIPAddressThruProxy( + string $expected, + string $remoteAddr, + array $proxyIPs, + string $forwardedFor, + ): void { service('superglobals') - ->setServer('REMOTE_ADDR', '10.0.1.200') - ->setServer('HTTP_X_FORWARDED_FOR', $expected); + ->setServer('REMOTE_ADDR', $remoteAddr) + ->setServer('HTTP_X_FORWARDED_FOR', $forwardedFor); $config = new App(); - $config->proxyIPs = [ - '10.0.1.200' => 'X-Forwarded-For', - '192.168.5.0/24' => 'X-Forwarded-For', - ]; + $config->proxyIPs = $proxyIPs; Factories::injectMock('config', App::class, $config); $this->request = new Request(); $this->request->populateHeaders(); - // we should see the original forwarded address $this->assertSame($expected, $this->request->getIPAddress()); } - public function testGetIPAddressThruProxyInvalid(): void - { - $expected = '123.456.23.123'; - service('superglobals') - ->setServer('REMOTE_ADDR', '10.0.1.200') - ->setServer('HTTP_X_FORWARDED_FOR', $expected); - $config = new App(); - $config->proxyIPs = [ - '10.0.1.200' => 'X-Forwarded-For', - '192.168.5.0/24' => 'X-Forwarded-For', - ]; - - $this->request = new Request($config); - $this->request->populateHeaders(); - - // spoofed address invalid - $this->assertSame('10.0.1.200', $this->request->getIPAddress()); - } - - public function testGetIPAddressThruProxyNotWhitelisted(): void + /** + * @return iterable, string}> + */ + public static function provideGetIPAddressThruProxy(): iterable { - $expected = '123.456.23.123'; - service('superglobals') - ->setServer('REMOTE_ADDR', '10.10.1.200') - ->setServer('HTTP_X_FORWARDED_FOR', $expected); - - $config = new App(); - $config->proxyIPs = [ - '10.0.1.200' => 'X-Forwarded-For', - '192.168.5.0/24' => 'X-Forwarded-For', + yield from [ + 'trusted proxy IP' => [ + '123.123.123.123', + '10.0.1.200', + ['10.0.1.200' => 'X-Forwarded-For', '192.168.5.0/24' => 'X-Forwarded-For'], + '123.123.123.123', + ], + 'invalid spoofed address' => [ + '10.0.1.200', + '10.0.1.200', + ['10.0.1.200' => 'X-Forwarded-For', '192.168.5.0/24' => 'X-Forwarded-For'], + '123.456.23.123', + ], + 'not whitelisted proxy IP' => [ + '10.10.1.200', + '10.10.1.200', + ['10.0.1.200' => 'X-Forwarded-For', '192.168.5.0/24' => 'X-Forwarded-For'], + '123.456.23.123', + ], + 'trusted subnet' => [ + '123.123.123.123', + '192.168.5.21', + ['192.168.5.0/24' => 'X-Forwarded-For'], + '123.123.123.123', + ], + 'out of trusted subnet' => [ + '192.168.5.21', + '192.168.5.21', + ['192.168.5.0/28' => 'X-Forwarded-For'], + '123.123.123.123', + ], + 'trusted IPv6 proxy IP' => [ + '123.123.123.123', + '2001:db8::2:1', + ['2001:db8::2:1' => 'X-Forwarded-For'], + '123.123.123.123', + ], + 'trusted IPv6 subnet' => [ + '123.123.123.123', + '2001:db8::2:1', + ['2001:db8::/32' => 'X-Forwarded-For'], + '123.123.123.123', + ], + 'out of trusted IPv6 subnet' => [ + '2001:db9::2:1', + '2001:db9::2:1', + ['2001:db8::/32' => 'X-Forwarded-For'], + '123.123.123.123', + ], + 'proxy entry IP version mismatch' => [ + '192.168.5.21', + '192.168.5.21', + ['2001:db8::/32' => 'X-Forwarded-For'], + '123.123.123.123', + ], + 'invalid proxy IP string' => [ + '10.0.1.200', + '10.0.1.200', + ['not an ip' => 'X-Forwarded-For'], + '123.123.123.123', + ], + 'invalid proxy CIDR mask' => [ + '192.168.5.21', + '192.168.5.21', + ['192.168.5.0/foo' => 'X-Forwarded-For'], + '123.123.123.123', + ], + 'empty proxy CIDR mask' => [ + '192.168.5.21', + '192.168.5.21', + ['192.168.5.0/' => 'X-Forwarded-For'], + '123.123.123.123', + ], + 'negative proxy CIDR mask' => [ + '192.168.5.21', + '192.168.5.21', + ['192.168.5.0/-1' => 'X-Forwarded-For'], + '123.123.123.123', + ], + 'out of range IPv4 proxy CIDR mask' => [ + '192.168.5.21', + '192.168.5.21', + ['192.168.5.0/33' => 'X-Forwarded-For'], + '123.123.123.123', + ], + 'out of range IPv6 proxy CIDR mask' => [ + '2001:db8::2:1', + '2001:db8::2:1', + ['2001:db8::/129' => 'X-Forwarded-For'], + '123.123.123.123', + ], ]; - $this->request = new Request($config); - $this->request->populateHeaders(); - - // spoofed address invalid - $this->assertSame('10.10.1.200', $this->request->getIPAddress()); - } - - public function testGetIPAddressThruProxySubnet(): void - { - $expected = '123.123.123.123'; - service('superglobals') - ->setServer('REMOTE_ADDR', '192.168.5.21') - ->setServer('HTTP_X_FORWARDED_FOR', $expected); - - $config = new App(); - $config->proxyIPs = ['192.168.5.0/24' => 'X-Forwarded-For']; - Factories::injectMock('config', App::class, $config); - $this->request = new Request(); - $this->request->populateHeaders(); - - // we should see the original forwarded address - $this->assertSame($expected, $this->request->getIPAddress()); - } - - public function testGetIPAddressThruProxyOutofSubnet(): void - { - $expected = '123.123.123.123'; - service('superglobals') - ->setServer('REMOTE_ADDR', '192.168.5.21') - ->setServer('HTTP_X_FORWARDED_FOR', $expected); - - $config = new App(); - $config->proxyIPs = ['192.168.5.0/28' => 'X-Forwarded-For']; - $this->request = new Request($config); - $this->request->populateHeaders(); - - // we should see the original forwarded address - $this->assertSame('192.168.5.21', $this->request->getIPAddress()); } // FIXME getIPAddress should have more testing, to 100% code coverage diff --git a/user_guide_src/source/changelogs/v4.7.4.rst b/user_guide_src/source/changelogs/v4.7.4.rst index 10dbc27cdeff..abdf30af9891 100644 --- a/user_guide_src/source/changelogs/v4.7.4.rst +++ b/user_guide_src/source/changelogs/v4.7.4.rst @@ -10,6 +10,17 @@ Release Date: Unreleased :local: :depth: 3 +******** +SECURITY +******** + +- **IncomingRequest:** *HTTPS detection via client-supplied headers* was fixed. + ``IncomingRequest::isSecure()`` now trusts the ``X-Forwarded-Proto`` and + ``Front-End-Https`` headers only when the request comes from a trusted proxy + configured in ``Config\App::$proxyIPs``. + See the `Security advisory GHSA-7wmf-pw8j-mc78 `_ + for more information. + ******** BREAKING ******** diff --git a/user_guide_src/source/incoming/incomingrequest.rst b/user_guide_src/source/incoming/incomingrequest.rst index a96353dadfba..ebe399d7aa7d 100644 --- a/user_guide_src/source/incoming/incomingrequest.rst +++ b/user_guide_src/source/incoming/incomingrequest.rst @@ -76,6 +76,17 @@ You can also check if the request was made through and HTTPS connection with the .. literalinclude:: incomingrequest/006.php +.. important:: The ``X-Forwarded-Proto`` and ``Front-End-Https`` headers are taken + into account only when the request comes from a trusted proxy configured in + ``Config\App::$proxyIPs``. Otherwise, they are client-supplied and could be + spoofed. + +.. note:: On dual-stack servers, a proxy's IPv4 address may be reported as an + IPv4-mapped IPv6 address (e.g., ``::ffff:192.168.5.21``), which does not + match an IPv4 entry such as ``192.168.5.0/24``. In that case, add the + IPv4-mapped form to ``Config\App::$proxyIPs``, e.g., + ``::ffff:192.168.5.21`` or ``::ffff:192.168.5.0/120``. + Retrieving Input **************** @@ -351,6 +362,10 @@ The methods provided by the parent classes that are available are: :returns: True if the request is an HTTPS request, otherwise false. :rtype: bool + .. important:: The ``X-Forwarded-Proto`` and ``Front-End-Https`` headers + are taken into account only when the request comes from a trusted + proxy configured in ``Config\App::$proxyIPs``. + .. php:method:: getVar([$index = null[, $filter = null[, $flags = null]]]) :param string $index: The name of the variable/key to look for. diff --git a/user_guide_src/source/installation/upgrade_474.rst b/user_guide_src/source/installation/upgrade_474.rst index a02083758727..e459eca6c465 100644 --- a/user_guide_src/source/installation/upgrade_474.rst +++ b/user_guide_src/source/installation/upgrade_474.rst @@ -20,6 +20,33 @@ Mandatory File Changes Breaking Changes **************** +HTTPS Detection Behind Proxies +============================== + +For security reasons, ``IncomingRequest::isSecure()`` no longer trusts the +``X-Forwarded-Proto`` and ``Front-End-Https`` headers unless the request comes +from a trusted proxy. See the +`Security advisory GHSA-7wmf-pw8j-mc78 `_ +for more information. + +If your application runs behind a reverse proxy or load balancer that +terminates TLS, you must register the proxy in ``Config\App::$proxyIPs``, e.g.:: + + public array $proxyIPs = [ + '10.0.1.200' => 'X-Forwarded-For', + '192.168.5.0/24' => 'X-Forwarded-For', + ]; + +Otherwise, ``isSecure()``, ``force_https()``, and +``Config\App::$forceGlobalSecureRequests`` will treat such requests as +non-HTTPS, which may result in redirect loops. + +.. note:: On dual-stack servers, a proxy's IPv4 address may be reported as an + IPv4-mapped IPv6 address (e.g., ``::ffff:192.168.5.21``), which does not + match an IPv4 entry such as ``192.168.5.0/24``. In that case, add the + IPv4-mapped form to ``Config\App::$proxyIPs``, e.g., + ``::ffff:192.168.5.21`` or ``::ffff:192.168.5.0/120``. + ********************* Breaking Enhancements ********************* diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 8c8e2bbe900e..4efd8a93d7e4 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 1839 errors +# total 1837 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/method.notFound.neon b/utils/phpstan-baseline/method.notFound.neon index 34144944c976..9075d775989d 100644 --- a/utils/phpstan-baseline/method.notFound.neon +++ b/utils/phpstan-baseline/method.notFound.neon @@ -1,4 +1,4 @@ -# total 80 errors +# total 78 errors parameters: ignoreErrors: @@ -114,7 +114,7 @@ parameters: - message: '#^Call to an undefined method CodeIgniter\\HTTP\\Request\:\:isSecure\(\)\.$#' - count: 3 + count: 1 path: ../../tests/system/HTTP/IncomingRequestTest.php - From f5e463b9a3e986389ce285963e51a7f1fab6559f Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Tue, 30 Jun 2026 12:58:33 +0200 Subject: [PATCH 51/65] Merge commit from fork Co-authored-by: Bogdan --- system/Database/BaseBuilder.php | 35 ++++++-- system/Database/OCI8/Builder.php | 6 +- system/Database/Postgre/Builder.php | 6 +- tests/system/Database/Builder/DeleteTest.php | 79 +++++++++++++++++++ tests/system/Database/Live/DeleteTest.php | 19 +++++ user_guide_src/source/changelogs/v4.7.4.rst | 9 +++ .../source/database/query_builder.rst | 3 +- 7 files changed, 141 insertions(+), 16 deletions(-) diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index 4900d7cd3db7..ce87de3c356f 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -2951,11 +2951,7 @@ protected function _deleteBatch(string $table, array $keys, array $values): stri ); // convert binds in where - foreach ($this->QBWhere as $key => $where) { - foreach ($this->binds as $field => $bind) { - $this->QBWhere[$key]['condition'] = str_replace(':' . $field . ':', $bind[0], $where['condition']); - } - } + $this->convertWhereBindsForBatch(); $sql .= ' ' . $this->compileWhereHaving('QBWhere'); @@ -2981,6 +2977,35 @@ protected function _deleteBatch(string $table, array $keys, array $values): stri return str_replace('{:_table_:}', $data, $sql); } + /** + * Escapes and substitutes the WHERE binds into the QBWhere conditions + * for batch delete queries. + * + * The bound values respect their escape flag and are escaped the same way + * as a regular query (see Query::matchNamedBinds()), instead of being + * injected into the SQL as raw, unescaped values. + * + * @used-by _deleteBatch() + */ + protected function convertWhereBindsForBatch(): void + { + $replacers = []; + + foreach ($this->binds as $field => $bind) { + $escapedValue = $bind[1] ? $this->db->escape($bind[0]) : $bind[0]; + + if (is_array($bind[0])) { + $escapedValue = '(' . implode(',', $escapedValue) . ')'; + } + + $replacers[':' . $field . ':'] = (string) $escapedValue; + } + + foreach ($this->QBWhere as $key => $where) { + $this->QBWhere[$key]['condition'] = strtr($where['condition'], $replacers); + } + } + /** * Increments a numeric column by the specified value. * diff --git a/system/Database/OCI8/Builder.php b/system/Database/OCI8/Builder.php index 25af517dc959..59195b3b533a 100644 --- a/system/Database/OCI8/Builder.php +++ b/system/Database/OCI8/Builder.php @@ -457,11 +457,7 @@ protected function _deleteBatch(string $table, array $keys, array $values): stri ); // convert binds in where - foreach ($this->QBWhere as $key => $where) { - foreach ($this->binds as $field => $bind) { - $this->QBWhere[$key]['condition'] = str_replace(':' . $field . ':', $bind[0], $where['condition']); - } - } + $this->convertWhereBindsForBatch(); $sql .= ' ' . str_replace( 'WHERE ', diff --git a/system/Database/Postgre/Builder.php b/system/Database/Postgre/Builder.php index 4cf0bde40060..c3a673558ec8 100644 --- a/system/Database/Postgre/Builder.php +++ b/system/Database/Postgre/Builder.php @@ -597,11 +597,7 @@ static function ($key, $value) use ($table, $alias, $that): RawSql|string { ); // convert binds in where - foreach ($this->QBWhere as $key => $where) { - foreach ($this->binds as $field => $bind) { - $this->QBWhere[$key]['condition'] = str_replace(':' . $field . ':', $bind[0], $where['condition']); - } - } + $this->convertWhereBindsForBatch(); $sql .= ' ' . str_replace( 'WHERE ', diff --git a/tests/system/Database/Builder/DeleteTest.php b/tests/system/Database/Builder/DeleteTest.php index 53b66eed805b..200eb9ca5b4a 100644 --- a/tests/system/Database/Builder/DeleteTest.php +++ b/tests/system/Database/Builder/DeleteTest.php @@ -13,8 +13,10 @@ namespace CodeIgniter\Database\Builder; +use CodeIgniter\Database\BaseBuilder; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Mock\MockConnection; +use CodeIgniter\Test\Mock\MockQuery; use PHPUnit\Framework\Attributes\Group; /** @@ -90,4 +92,81 @@ public function testGetCompiledDeleteWithLimit(): void EOL; $this->assertSame($expectedSQL, $sql); } + + public function testDeleteBatchEscapesWhereBinds(): void + { + $db = new MockConnection([]); + $builder = new BaseBuilder('jobs', $db); + + $data = [ + ['id' => 1, 'name' => 'Derek J'], + ['id' => 2, 'name' => 'Ahmadinejad'], + ]; + + // A user-controlled value that would break out of the query if it + // were substituted into the SQL unescaped. + $malicious = "anything' OR '1'='1"; + + $db->shouldReturn('execute', new class () {}); + $builder->setData($data, null, 'data') + ->onConstraint(['id' => 'id']) + ->where('jobs.name', $malicious) + ->deleteBatch(); + + $query = $db->getLastQuery(); + $this->assertInstanceOf(MockQuery::class, $query); + + // The bound value must be escaped and quoted as a single string literal: + // it is wrapped in single quotes and the inner quotes are doubled, so it + // cannot break out of the literal and inject SQL. + $this->assertStringContainsString("'anything'' OR ''1''", $query->getQuery()); + $this->assertStringNotContainsString("= anything' OR '1'='1", $query->getQuery()); + } + + public function testDeleteBatchEscapesMultipleWhereBinds(): void + { + $db = new MockConnection([]); + $builder = new BaseBuilder('jobs', $db); + + $data = [ + ['id' => 1, 'name' => 'Derek J'], + ['id' => 2, 'name' => 'Ahmadinejad'], + ]; + + $db->shouldReturn('execute', new class () {}); + $builder->setData($data, null, 'data') + ->onConstraint(['id' => 'id']) + ->where('jobs.name', "anything' OR '1'='1") + ->where('jobs.id', 1) + ->deleteBatch(); + + $query = $db->getLastQuery(); + $this->assertInstanceOf(MockQuery::class, $query); + + $this->assertStringContainsString('"jobs"."name" = \'anything\'\' OR \'\'1\'\' = \'\'1\'', $query->getQuery()); + $this->assertStringContainsString('"jobs"."id" = 1', $query->getQuery()); + } + + public function testDeleteBatchEscapesWhereInBinds(): void + { + $db = new MockConnection([]); + $builder = new BaseBuilder('jobs', $db); + + $data = [ + ['id' => 1, 'name' => 'Derek J'], + ['id' => 2, 'name' => 'Ahmadinejad'], + ]; + + $db->shouldReturn('execute', new class () {}); + $builder->setData($data, null, 'data') + ->onConstraint(['id' => 'id']) + ->whereIn('jobs.name', ["anything' OR '1'='1", 'Ahmadinejad']) + ->deleteBatch(); + + $query = $db->getLastQuery(); + $this->assertInstanceOf(MockQuery::class, $query); + + $this->assertStringContainsString("IN ('anything'' OR ''1''=''1','Ahmadinejad')", $query->getQuery()); + $this->assertStringNotContainsString("IN anything' OR '1'='1", $query->getQuery()); + } } diff --git a/tests/system/Database/Live/DeleteTest.php b/tests/system/Database/Live/DeleteTest.php index 896ab12c1d39..651dff251a7f 100644 --- a/tests/system/Database/Live/DeleteTest.php +++ b/tests/system/Database/Live/DeleteTest.php @@ -100,6 +100,25 @@ public function testDeleteBatch(): void $this->dontSeeInDatabase('user', ['email' => 'ahmadinejad@world.com', 'name' => 'Ahmadinejad']); } + public function testDeleteBatchPreventsSQLInjectionInWhere(): void + { + if ($this->db->DBDriver === 'SQLite3') { + $this->markTestSkipped('SQLite3 driver does not support WHERE for batch deletes.'); + } + + $data = [ + ['userid' => 1, 'username' => 'Derek J'], + ]; + + $this->db->table('user') + ->setData($data, null, 'data') + ->onConstraint(['id' => 'userid']) + ->where('user.name', "nobody' OR 1=1 --") + ->deleteBatch(); + + $this->seeInDatabase('user', ['email' => 'derek@world.com', 'name' => 'Derek Jones']); + } + public function testDeleteBatchConstraintsDate(): void { $table = 'type_test'; diff --git a/user_guide_src/source/changelogs/v4.7.4.rst b/user_guide_src/source/changelogs/v4.7.4.rst index abdf30af9891..c82e7256885d 100644 --- a/user_guide_src/source/changelogs/v4.7.4.rst +++ b/user_guide_src/source/changelogs/v4.7.4.rst @@ -21,6 +21,15 @@ SECURITY See the `Security advisory GHSA-7wmf-pw8j-mc78 `_ for more information. +- **Query Builder:** Fixed a SQL injection vulnerability in ``deleteBatch()``. + When ``deleteBatch()`` was used together with ``where()`` conditions, the + bound values from the WHERE clause were substituted into the generated SQL + with their escape flag ignored, so they were never escaped or quoted. The + WHERE binds are now escaped in the same way as a regular ``delete()``. + + See the `Security advisory GHSA-c9w5-rwh3-7pm9 `_ + for more information. + ******** BREAKING ******** diff --git a/user_guide_src/source/database/query_builder.rst b/user_guide_src/source/database/query_builder.rst index 480b8c18d92e..b0ca8aa1fb5c 100644 --- a/user_guide_src/source/database/query_builder.rst +++ b/user_guide_src/source/database/query_builder.rst @@ -1230,7 +1230,8 @@ Generates a batch **DELETE** statement based on a set of data. This method may be especially useful when deleting data in a table with a composite primary key. -.. note:: SQLite3 does not support the use of ``where()``. +.. note:: The SQLite3 driver does not support additional Query Builder WHERE + conditions for ``deleteBatch()``. Delete from a Query ^^^^^^^^^^^^^^^^^^^ From 20ebcf4694d96d3c97fbc3938e360730e4f54618 Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Tue, 30 Jun 2026 13:04:13 +0200 Subject: [PATCH 52/65] Merge commit from fork Co-authored-by: Bogdan --- system/HTTP/Files/UploadedFile.php | 8 +++-- tests/system/HTTP/Files/FileMovingTest.php | 32 +++++++++++++++++++ user_guide_src/source/changelogs/v4.7.4.rst | 9 ++++++ .../source/libraries/uploaded_files.rst | 2 +- 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/system/HTTP/Files/UploadedFile.php b/system/HTTP/Files/UploadedFile.php index b43923026021..95a393616dff 100644 --- a/system/HTTP/Files/UploadedFile.php +++ b/system/HTTP/Files/UploadedFile.php @@ -123,7 +123,8 @@ public function __construct(string $path, string $originalName, ?string $mimeTyp * @see http://php.net/move_uploaded_file * * @param string $targetPath Path to which to move the uploaded file. - * @param string|null $name the name to rename the file to. + * @param string|null $name The name to rename the file to. When null, the client-provided name is used and sanitized. + * A caller-supplied name is NOT sanitized. * @param bool $overwrite State for indicating whether to overwrite the previously generated file with the same * name or not. * @@ -142,7 +143,10 @@ public function move(string $targetPath, ?string $name = null, bool $overwrite = throw HTTPException::forInvalidFile(); } - $name ??= $this->getName(); + if ($name === null) { + helper('security'); + $name = sanitize_filename($this->getName()); + } $destination = $overwrite ? $targetPath . $name : $this->getDestination($targetPath . $name); try { diff --git a/tests/system/HTTP/Files/FileMovingTest.php b/tests/system/HTTP/Files/FileMovingTest.php index 45a9fd179837..3284c33d0f26 100644 --- a/tests/system/HTTP/Files/FileMovingTest.php +++ b/tests/system/HTTP/Files/FileMovingTest.php @@ -100,6 +100,38 @@ public function testMove(): void $this->assertTrue($this->root->hasChild('destination/' . $finalFilename . '_1.txt')); } + public function testMoveSanitizesClientNameByDefault(): void + { + service('superglobals')->setFilesArray([ + 'userfile' => [ + 'name' => '../../public/shell.php', + 'type' => 'text/plain', + 'size' => 124, + 'tmp_name' => '/tmp/fileA.txt', + 'error' => 0, + ], + ]); + + $collection = new FileCollection(); + + $this->assertTrue($collection->hasFile('userfile')); + + $destination = $this->destination; + if (! is_dir($destination)) { + mkdir($destination, 0777, true); + } + + $file = $collection->getFile('userfile'); + $this->assertInstanceOf(UploadedFile::class, $file); + + // No second argument: the client-provided name must be sanitized. + $file->move($destination); + + $this->assertSame('publicshell.php', $file->getName()); + $this->assertTrue($this->root->hasChild('destination/publicshell.php')); + $this->assertFalse($this->root->hasChild('public/shell.php')); + } + public function testMoveOverwriting(): void { $finalFilename = 'file_with_delimiters_underscore'; diff --git a/user_guide_src/source/changelogs/v4.7.4.rst b/user_guide_src/source/changelogs/v4.7.4.rst index c82e7256885d..e3d2b6ba65c9 100644 --- a/user_guide_src/source/changelogs/v4.7.4.rst +++ b/user_guide_src/source/changelogs/v4.7.4.rst @@ -30,6 +30,15 @@ SECURITY See the `Security advisory GHSA-c9w5-rwh3-7pm9 `_ for more information. +- **UploadedFile:** ``UploadedFile::move()`` now sanitizes the client-provided + filename when called without a second argument. Previously, the unsanitized + client filename was used as the default, allowing path traversal sequences + (e.g. ``../../public/shell.php``) to write the uploaded file outside the + intended directory. A name explicitly passed as the second argument is not + sanitized and remains the caller's responsibility. + See the `Security advisory GHSA-hhmc-q9hp-r662 `_ + for more information. + ******** BREAKING ******** diff --git a/user_guide_src/source/libraries/uploaded_files.rst b/user_guide_src/source/libraries/uploaded_files.rst index 7cc3084d306f..eadc1d8ef2e9 100644 --- a/user_guide_src/source/libraries/uploaded_files.rst +++ b/user_guide_src/source/libraries/uploaded_files.rst @@ -339,7 +339,7 @@ the file to as the first parameter: .. literalinclude:: uploaded_files/016.php -By default, the original filename was used. +By default, the client-provided filename is sanitized and used. with New Filename ----------------- From b6e9a4fa1dca2df3d3f261bdf61532df8c6420aa Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Tue, 30 Jun 2026 13:09:20 +0200 Subject: [PATCH 53/65] Merge commit from fork --- system/Validation/StrictRules/FileRules.php | 39 +++++ .../Validation/StrictRules/FileRulesTest.php | 165 +++++++++++++++++- user_guide_src/source/changelogs/v4.7.4.rst | 16 ++ 3 files changed, 218 insertions(+), 2 deletions(-) diff --git a/system/Validation/StrictRules/FileRules.php b/system/Validation/StrictRules/FileRules.php index b6d4fdb673fd..d381f4f47301 100644 --- a/system/Validation/StrictRules/FileRules.php +++ b/system/Validation/StrictRules/FileRules.php @@ -15,6 +15,7 @@ use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\HTTP\CLIRequest; +use CodeIgniter\HTTP\Files\UploadedFile; use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\RequestInterface; use Config\Mimes; @@ -150,6 +151,10 @@ public function is_image(?string $blank, string $params): bool if (mb_strpos($type, 'image') !== 0) { return false; } + + if ($this->hasInvalidImageClientExtension($file)) { + return false; + } } return true; @@ -182,6 +187,10 @@ public function mime_in(?string $blank, string $params): bool if (! in_array($file->getMimeType(), $params, true)) { return false; } + + if ($this->hasMismatchedClientExtension($file)) { + return false; + } } return true; @@ -321,4 +330,34 @@ public function min_dims(?string $blank, string $params): bool return true; } + + /** + * Reject filenames with non-empty extensions that are not image types. + */ + private function hasInvalidImageClientExtension(UploadedFile $file): bool + { + $clientExtension = trim(strtolower($file->getClientExtension()), '. '); + + if ($clientExtension === '') { + return false; + } + + $type = Mimes::guessTypeFromExtension($clientExtension) ?? ''; + + return mb_strpos($type, 'image') !== 0; + } + + /** + * Reject filenames with non-empty extensions that do not match the detected content. + */ + private function hasMismatchedClientExtension(UploadedFile $file): bool + { + $clientExtension = trim(strtolower($file->getClientExtension()), '. '); + + if ($clientExtension === '') { + return false; + } + + return $file->guessExtension() !== $clientExtension; + } } diff --git a/tests/system/Validation/StrictRules/FileRulesTest.php b/tests/system/Validation/StrictRules/FileRulesTest.php index f7b5543b6f9f..5c58787b5f0b 100644 --- a/tests/system/Validation/StrictRules/FileRulesTest.php +++ b/tests/system/Validation/StrictRules/FileRulesTest.php @@ -266,6 +266,91 @@ public function testIsImage(): void $this->assertTrue($this->validation->run([])); } + public function testIsImageFailsForMismatchedClientExtension(): void + { + $payload = $this->createGifPayload(); + + try { + $this->setUploadedAvatar($payload, 'shell.php'); + + $this->validation->setRules(['avatar' => 'is_image[avatar]']); + $this->assertFalse($this->validation->run([])); + } finally { + unlink($payload); + } + } + + public function testIsImageFailsForNonImageClientExtension(): void + { + $payload = $this->createGifPayload(); + + try { + $this->setUploadedAvatar($payload, 'document.pdf'); + + $this->validation->setRules(['avatar' => 'is_image[avatar]']); + $this->assertFalse($this->validation->run([])); + } finally { + unlink($payload); + } + } + + public function testIsImageAllowsImageClientExtensionThatDoesNotMatchContent(): void + { + $payload = $this->createGifPayload(); + + try { + $this->setUploadedAvatar($payload, 'my-avatar.jpg'); + + $this->validation->setRules(['avatar' => 'is_image[avatar]']); + $this->assertTrue($this->validation->run([])); + } finally { + unlink($payload); + } + } + + public function testIsImageAllowsExtensionlessClientFilename(): void + { + $payload = $this->createGifPayload(); + + try { + $this->setUploadedAvatar($payload, 'blob'); + + $this->validation->setRules(['avatar' => 'is_image[avatar]']); + $this->assertTrue($this->validation->run([])); + } finally { + unlink($payload); + } + } + + public function testIsImageAllowsSvg(): void + { + $payload = $this->createSvgPayload(); + + try { + $this->setUploadedAvatar($payload, 'my-avatar.svg', 'image/svg+xml'); + + $this->validation->setRules(['avatar' => 'is_image[avatar]']); + $this->assertTrue($this->validation->run([])); + } finally { + unlink($payload); + } + } + + public function testIsImageFailsForNonImageContent(): void + { + $payload = $this->createPhpPayload(); + + try { + // An image extension is not enough; the file content must be an image too. + $this->setUploadedAvatar($payload, 'fake.gif'); + + $this->validation->setRules(['avatar' => 'is_image[avatar]']); + $this->assertFalse($this->validation->run([])); + } finally { + unlink($payload); + } + } + public function testIsntImage(): void { $_FILES['stuff'] = [ @@ -294,6 +379,62 @@ public function testMimeTypeOk(): void $this->assertTrue($this->validation->run([])); } + public function testMimeTypeFailsForMismatchedClientExtension(): void + { + $payload = $this->createGifPayload(); + + try { + $this->setUploadedAvatar($payload, 'shell.php'); + + $this->validation->setRules(['avatar' => 'mime_in[avatar,image/gif]']); + $this->assertFalse($this->validation->run([])); + } finally { + unlink($payload); + } + } + + public function testMimeTypeFailsForIncompatibleClientExtension(): void + { + $payload = $this->createGifPayload(); + + try { + $this->setUploadedAvatar($payload, 'document.pdf'); + + $this->validation->setRules(['avatar' => 'mime_in[avatar,image/gif]']); + $this->assertFalse($this->validation->run([])); + } finally { + unlink($payload); + } + } + + public function testMimeTypeFailsForAllowedClientExtensionThatDoesNotMatchContent(): void + { + $payload = $this->createGifPayload(); + + try { + $this->setUploadedAvatar($payload, 'my-avatar.jpg'); + + $this->validation->setRules(['avatar' => 'mime_in[avatar,image/gif,image/jpeg]']); + $this->assertFalse($this->validation->run([])); + } finally { + unlink($payload); + } + } + + public function testMimeTypeAllowsExtensionlessClientFilename(): void + { + $payload = $this->createGifPayload(); + + try { + $this->setUploadedAvatar($payload, 'blob'); + + $this->validation->setRules(['avatar' => 'mime_in[avatar,image/gif]']); + $this->assertTrue($this->validation->run([])); + } finally { + unlink($payload); + } + } + public function testMimeTypeNotOk(): void { $this->validation->setRules([ @@ -397,14 +538,34 @@ private function createGifPayload(): string return $payload; } - private function setUploadedAvatar(string $payload, string $name): void + private function createPhpPayload(): string + { + $payload = tempnam(sys_get_temp_dir(), 'ci4-upload-poc-'); + $this->assertIsString($payload); + + file_put_contents($payload, "\n"); + + return $payload; + } + + private function createSvgPayload(): string + { + $payload = tempnam(sys_get_temp_dir(), 'ci4-upload-poc-'); + $this->assertIsString($payload); + + file_put_contents($payload, ''); + + return $payload; + } + + private function setUploadedAvatar(string $payload, string $name, string $clientMimeType = 'image/gif'): void { service('superglobals')->setFilesArray([ 'avatar' => [ 'tmp_name' => $payload, 'name' => $name, 'size' => filesize($payload), - 'type' => 'image/gif', + 'type' => $clientMimeType, 'error' => UPLOAD_ERR_OK, ], ]); diff --git a/user_guide_src/source/changelogs/v4.7.4.rst b/user_guide_src/source/changelogs/v4.7.4.rst index e3d2b6ba65c9..531c16a7cc5b 100644 --- a/user_guide_src/source/changelogs/v4.7.4.rst +++ b/user_guide_src/source/changelogs/v4.7.4.rst @@ -39,6 +39,22 @@ SECURITY See the `Security advisory GHSA-hhmc-q9hp-r662 `_ for more information. +- **Validation:** The ``is_image`` and ``mime_in`` file upload validation rules + now also verify non-empty client filename extensions. Previously, these rules + classified an upload solely by its content-derived MIME type, so a file with a + dangerous client extension (for example, a ``.php`` file prepended with image + magic bytes) could pass validation while keeping its original extension on + disk. + See the `Security advisory GHSA-mmj4-63m4-r6h5 `_ + for more information. + + The ``is_image`` rule now rejects uploads when a non-empty client filename + extension is not an image extension. The ``mime_in`` rule now rejects uploads + when a non-empty client filename extension does not match the detected file + content, matching the same extension/content agreement used by ``ext_in``. + Files uploaded without any extension (such as JavaScript ``Blob`` uploads) + are still accepted, since the rules validate the file content. + ******** BREAKING ******** From 9331fa84dbf2cf418014b5a09925ff659e04baca Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Tue, 30 Jun 2026 13:32:55 +0200 Subject: [PATCH 54/65] refactor: optimize `prepQuotedPrintable()` with hash lookup and int-length tracking (#10344) * refactor: optimize prepQuotedPrintable() with O(1) lookup, int-length tracking, and sprintf encoding * cs-fix --- system/Email/Email.php | 122 ++++++------------------------- tests/system/Email/EmailTest.php | 107 +++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 98 deletions(-) diff --git a/system/Email/Email.php b/system/Email/Email.php index b886d07fdc72..4b3e94dfb38b 100644 --- a/system/Email/Email.php +++ b/system/Email/Email.php @@ -197,7 +197,7 @@ class Email * * @see http://www.ietf.org/rfc/rfc822.txt * - * @var "\r\n"|"n" + * @var "\n"|"\r\n" */ public $CRLF = "\r\n"; @@ -1335,89 +1335,6 @@ protected function appendAttachments(&$body, $boundary, $multipart = null) */ protected function prepQuotedPrintable($str) { - // ASCII code numbers for "safe" characters that can always be - // used literally, without encoding, as described in RFC 2049. - // http://www.ietf.org/rfc/rfc2049.txt - static $asciiSafeChars = [ - // ' ( ) + , - . / : = ? - 39, - 40, - 41, - 43, - 44, - 45, - 46, - 47, - 58, - 61, - 63, - // numbers - 48, - 49, - 50, - 51, - 52, - 53, - 54, - 55, - 56, - 57, - // upper-case letters - 65, - 66, - 67, - 68, - 69, - 70, - 71, - 72, - 73, - 74, - 75, - 76, - 77, - 78, - 79, - 80, - 81, - 82, - 83, - 84, - 85, - 86, - 87, - 88, - 89, - 90, - // lower-case letters - 97, - 98, - 99, - 100, - 101, - 102, - 103, - 104, - 105, - 106, - 107, - 108, - 109, - 110, - 111, - 112, - 113, - 114, - 115, - 116, - 117, - 118, - 119, - 120, - 121, - 122, - ]; - // We are intentionally wrapping so mail servers will encode characters // properly and MUAs will behave, so {unwrap} must go! $str = str_replace(['{unwrap}', '{/unwrap}'], '', $str); @@ -1438,46 +1355,55 @@ protected function prepQuotedPrintable($str) $str = str_replace(["\r\n", "\r"], "\n", $str); } - $escape = '='; + static $asciiSafeChars; + if ($asciiSafeChars === null) { + $safeChars = [39, 40, 41, 43, 44, 45, 46, 47, 58, 61, 63]; + $safeChars = array_merge($safeChars, range(48, 57), range(65, 90), range(97, 122)); + $asciiSafeChars = array_fill_keys($safeChars, true); + } + $output = ''; foreach (explode("\n", $str) as $line) { - $length = static::strlen($line); - $temp = ''; + $length = static::strlen($line); + $temp = ''; + $tempLen = 0; // Loop through each character in the line to add soft-wrap // characters at the end of a line " =\r\n" and add the newly // processed line(s) to the output (see comment on $crlf class property) for ($i = 0; $i < $length; $i++) { - // Grab the next character - $char = $line[$i]; - $ascii = ord($char); + $char = $line[$i]; + $ascii = ord($char); + $charLen = 1; // Convert spaces and tabs but only if it's the end of the line if ($ascii === 32 || $ascii === 9) { if ($i === ($length - 1)) { - $char = $escape . sprintf('%02s', dechex($ascii)); + $char = sprintf('=%02X', $ascii); + $charLen = 3; } } // DO NOT move this below the $ascii_safe_chars line! // // = (equals) signs are allowed by RFC2049, but must be encoded // as they are the encoding delimiter! - elseif ($ascii === 61) { - $char = $escape . strtoupper(sprintf('%02s', dechex($ascii))); // =3D - } elseif (! in_array($ascii, $asciiSafeChars, true)) { - $char = $escape . strtoupper(sprintf('%02s', dechex($ascii))); + elseif ($ascii === 61 || ! isset($asciiSafeChars[$ascii])) { + $char = sprintf('=%02X', $ascii); + $charLen = 3; } // If we're at the character limit, add the line to the output, // reset our temp variable, and keep on chuggin' - if ((static::strlen($temp) + static::strlen($char)) >= 76) { - $output .= $temp . $escape . $this->CRLF; - $temp = ''; + if (($tempLen + $charLen) >= 76) { + $output .= $temp . '=' . $this->CRLF; + $temp = ''; + $tempLen = 0; } // Add the character to our temporary line $temp .= $char; + $tempLen += $charLen; } // Add our completed line to the output diff --git a/tests/system/Email/EmailTest.php b/tests/system/Email/EmailTest.php index ca42613505e4..9ca5c5a51a4a 100644 --- a/tests/system/Email/EmailTest.php +++ b/tests/system/Email/EmailTest.php @@ -302,4 +302,111 @@ public function testGetHostnameFallsBackToGethostnameFunction(): void $this->assertSame(gethostname(), $getHostname()); } + + #[DataProvider('providePrepQuotedPrintableWithLfCrlf')] + public function testPrepQuotedPrintableWithLfCrlf(string $input, string $expected): void + { + $email = new Email(); + $email->CRLF = "\n"; + $prepQP = self::getPrivateMethodInvoker($email, 'prepQuotedPrintable'); + + $this->assertSame($expected, $prepQP($input)); + } + + /** + * @return iterable + */ + public static function providePrepQuotedPrintableWithLfCrlf(): iterable + { + return [ + 'empty string' => ['', ''], + 'safe ascii only' => ['hello world', 'hello world'], + 'safe chars only' => ['abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789(),-./:?', 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789(),-./:?'], + 'unsafe char encoded' => ["a\x01b", 'a=01b'], + 'trailing space encoded' => ["hello \nworld", "hello=20\nworld"], + 'trailing tab encoded' => ["hello\t\nworld", "hello=09\nworld"], + 'equals sign encoded as =3D' => ['a=b', 'a=3Db'], + 'multiple spaces reduced' => ['a b', 'a b'], + 'null bytes removed' => ["a\x00b", 'ab'], + 'unwrap tags removed' => ['{unwrap}secret{/unwrap}', 'secret'], + 'single line' => ['test', 'test'], + 'two lines' => ["line1\nline2", "line1\nline2"], + 'three lines trailing empty' => ["line1\nline2\n", "line1\nline2\n"], + ]; + } + + public function testPrepQuotedPrintableWithCrlfNative(): void + { + $email = new Email(); + $email->CRLF = "\r\n"; + $prepQP = self::getPrivateMethodInvoker($email, 'prepQuotedPrintable'); + + $result = $prepQP('test'); + + $this->assertSame(quoted_printable_encode('test'), $result); + } + + public function testPrepQuotedPrintableSoftLineBreak(): void + { + $email = new Email(); + $email->CRLF = "\n"; + $prepQP = self::getPrivateMethodInvoker($email, 'prepQuotedPrintable'); + + // 76 'a' chars fit in one line; add 2 more 'b' chars and they soft-wrap + // After reduction: no trailing spaces, just safe chars + $input = str_repeat('a', 76) . 'bb'; + $result = $prepQP($input); + + $this->assertStringContainsString("=\n", $result, 'Soft line break must be present'); + $this->assertStringNotContainsString("\r\n", $result, 'Custom CRLF must not contain \\r'); + } + + public function testPrepQuotedPrintableSoftBreakAfterEncodedChar(): void + { + $email = new Email(); + $email->CRLF = "\n"; + $prepQP = self::getPrivateMethodInvoker($email, 'prepQuotedPrintable'); + + // 74 safe chars + 1 encoded (=3D = 3 bytes) = 77 → must break before encoded + $input = str_repeat('a', 74) . '='; + $result = $prepQP($input); + + $this->assertSame(str_repeat('a', 74) . "=\n=3D", $result); + } + + public function testPrepQuotedPrintableHardLineBreakNoInternalSpaceReduction(): void + { + $email = new Email(); + $email->CRLF = "\n"; + $prepQP = self::getPrivateMethodInvoker($email, 'prepQuotedPrintable'); + + // Spaces not at end of line must be left as-is + $this->assertSame('a b', $prepQP('a b')); + } + + public function testPrepQuotedPrintableMixedContent(): void + { + $email = new Email(); + $email->CRLF = "\n"; + $prepQP = self::getPrivateMethodInvoker($email, 'prepQuotedPrintable'); + + $input = "Hello, World!\nline ends with tab\t\n=special chars: \x01\x02"; + $result = $prepQP($input); + + $this->assertStringContainsString('Hello, World=21', $result); + $this->assertStringContainsString('=09', $result); + $this->assertStringContainsString('=3D', $result); + $this->assertStringContainsString('=01', $result); + $this->assertStringContainsString('=02', $result); + } + + public function testPrepQuotedPrintableUnwrapRemovesTagsOnly(): void + { + $email = new Email(); + $email->CRLF = "\n"; + $prepQP = self::getPrivateMethodInvoker($email, 'prepQuotedPrintable'); + + $this->assertSame('keep =7Bbraces=7D', $prepQP('keep {braces}')); + $this->assertSame('keep (parentheses)', $prepQP('keep (parentheses)')); + } } From 01da624a898e84f79e02a6388819c4f65584b8aa Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Tue, 30 Jun 2026 21:39:13 +0800 Subject: [PATCH 55/65] refactor: add precise `array` phpdocs for `CLI` (#10354) --- system/CLI/CLI.php | 36 ++++++----- system/CLI/Console.php | 2 + utils/phpstan-baseline/loader.neon | 2 +- .../missingType.iterableValue.neon | 62 +------------------ 4 files changed, 24 insertions(+), 78 deletions(-) diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index 3996f31ee349..11f49bbc0c45 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -214,9 +214,9 @@ public static function input(?string $prefix = null): string * // Do not provide options but requires a valid email * $email = CLI::prompt('What is your email?', null, 'required|valid_email'); * - * @param string $field Output "field" question - * @param list|string $options String to a default value, array to a list of options (the first option will be the default value) - * @param array|string|null $validation Validation rules + * @param string $field Output "field" question + * @param list|string $options String to a default value, array to a list of options (the first option will be the default value) + * @param list|string|null $validation Validation rules * * @return string The user input */ @@ -273,10 +273,10 @@ public static function prompt(string $field, $options = null, $validation = null /** * prompt(), but based on the option's key * - * @param array|string $text Output "field" text or an one or two value array where the first value is the text before listing the options - * and the second value the text before asking to select one option. Provide empty string to omit - * @param array $options A list of options (array(key => description)), the first option will be the default value - * @param array|string|null $validation Validation rules + * @param list|string $text Output "field" text or an one or two value array where the first value is the text before listing the options + * and the second value the text before asking to select one option. Provide empty string to omit + * @param array $options A list of options (array(key => description)), the first option will be the default value + * @param list|string|null $validation Validation rules * * @return string The selected key of $options */ @@ -302,11 +302,11 @@ public static function promptByKey($text, array $options, $validation = null): s /** * This method is the same as promptByKey(), but this method supports multiple keys, separated by commas. * - * @param string $text Output "field" text or an one or two value array where the first value is the text before listing the options - * and the second value the text before asking to select one option. Provide empty string to omit - * @param array $options A list of options (array(key => description)), the first option will be the default value + * @param string $text Output "field" text or an one or two value array where the first value is the text before listing the options + * and the second value the text before asking to select one option. Provide empty string to omit + * @param array $options A list of options (array(key => description)), the first option will be the default value * - * @return array The selected key(s) and value(s) of $options + * @return array The selected key(s) and value(s) of $options */ public static function promptByMultipleKeys(string $text, array $options): array { @@ -375,6 +375,8 @@ public static function promptByMultipleKeys(string $text, array $options): array /** * Validation for $options in promptByKey() and promptByMultipleKeys(). Return an error if $options is an empty array. + * + * @param array $options */ private static function isZeroOptions(array $options): void { @@ -385,6 +387,8 @@ private static function isZeroOptions(array $options): void /** * Print each key and value one by one + * + * @param array $options */ private static function printKeysAndValues(array $options): void { @@ -404,9 +408,9 @@ private static function printKeysAndValues(array $options): void /** * Validate one prompt "field" at a time * - * @param string $field Prompt "field" output - * @param string $value Input value - * @param array|string $rules Validation rules + * @param string $field Prompt "field" output + * @param string $value Input value + * @param list|string $rules Validation rules */ protected static function validate(string $field, string $value, $rules): bool { @@ -1025,8 +1029,8 @@ public static function getOptionString(bool $useLongOpts = false, bool $trim = f /** * Returns a well formatted table * - * @param array $tbody List of rows - * @param array $thead List of columns + * @param list> $tbody List of rows + * @param list $thead List of columns * * @return void */ diff --git a/system/CLI/Console.php b/system/CLI/Console.php index 89415e265134..b24562e52d46 100644 --- a/system/CLI/Console.php +++ b/system/CLI/Console.php @@ -75,6 +75,8 @@ public function showHeader(bool $suppress = false) * unshift it as argument instead. * * @param array $params + * + * @return array */ private function parseParamsForHelpOption(array $params): array { diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 4efd8a93d7e4..2e67047327fc 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 1837 errors +# total 1825 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index 80d45074b96e..1627bbb4a52a 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,67 +1,7 @@ -# total 1153 errors +# total 1141 errors parameters: ignoreErrors: - - - message: '#^Method CodeIgniter\\CLI\\CLI\:\:isZeroOptions\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/CLI/CLI.php - - - - message: '#^Method CodeIgniter\\CLI\\CLI\:\:printKeysAndValues\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/CLI/CLI.php - - - - message: '#^Method CodeIgniter\\CLI\\CLI\:\:promptByKey\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/CLI/CLI.php - - - - message: '#^Method CodeIgniter\\CLI\\CLI\:\:promptByKey\(\) has parameter \$text with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/CLI/CLI.php - - - - message: '#^Method CodeIgniter\\CLI\\CLI\:\:promptByKey\(\) has parameter \$validation with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/CLI/CLI.php - - - - message: '#^Method CodeIgniter\\CLI\\CLI\:\:promptByMultipleKeys\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/CLI/CLI.php - - - - message: '#^Method CodeIgniter\\CLI\\CLI\:\:promptByMultipleKeys\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/CLI/CLI.php - - - - message: '#^Method CodeIgniter\\CLI\\CLI\:\:prompt\(\) has parameter \$validation with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/CLI/CLI.php - - - - message: '#^Method CodeIgniter\\CLI\\CLI\:\:table\(\) has parameter \$tbody with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/CLI/CLI.php - - - - message: '#^Method CodeIgniter\\CLI\\CLI\:\:table\(\) has parameter \$thead with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/CLI/CLI.php - - - - message: '#^Method CodeIgniter\\CLI\\CLI\:\:validate\(\) has parameter \$rules with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/CLI/CLI.php - - - - message: '#^Method CodeIgniter\\CLI\\Console\:\:parseParamsForHelpOption\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/CLI/Console.php - - message: '#^Method CodeIgniter\\CodeIgniter\:\:getPerformanceStats\(\) return type has no value type specified in iterable type array\.$#' count: 1 From 2b0f79958317dcaf5cead35cc59a143b2b5cf3d1 Mon Sep 17 00:00:00 2001 From: memleakd <121398829+memleakd@users.noreply.github.com> Date: Thu, 2 Jul 2026 02:54:09 +0300 Subject: [PATCH 56/65] chore: adapt output buffer handling to PHPStan 2.2.3 (#10364) * fix: satisfy PHPStan 2.2.3 - Remove redundant output-buffer level checks after ob_start() - Narrow output-buffer helper return types Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com> * fix: handle command output buffer changes Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com> --------- Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com> --- system/Common.php | 10 +++++++++- tests/system/CommonFunctionsSendTest.php | 4 +--- tests/system/HTTP/DownloadResponseTest.php | 4 +--- tests/system/HTTP/ResponseSendTest.php | 16 ++++------------ tests/system/Test/BootstrapFCPATHTest.php | 2 +- tests/system/Test/TestCaseEmissionsTest.php | 8 ++------ 6 files changed, 18 insertions(+), 26 deletions(-) diff --git a/system/Common.php b/system/Common.php index d74fe403c302..fc8abb394f06 100644 --- a/system/Common.php +++ b/system/Common.php @@ -185,13 +185,21 @@ function command(string $command) $params[$arg] = $value; } + $bufferLevel = ob_get_level(); + try { ob_start(); service('commands')->run($command, $params); + if (ob_get_level() <= $bufferLevel) { + return false; + } + return ob_get_contents(); } finally { - ob_end_clean(); + while (ob_get_level() > $bufferLevel) { + ob_end_clean(); + } } } } diff --git a/tests/system/CommonFunctionsSendTest.php b/tests/system/CommonFunctionsSendTest.php index 66e285d26f84..1cf2b7917459 100644 --- a/tests/system/CommonFunctionsSendTest.php +++ b/tests/system/CommonFunctionsSendTest.php @@ -58,9 +58,7 @@ public function testRedirectResponseCookiesSent(): void // send it ob_start(); $response->send(); - if (ob_get_level() > 0) { - ob_end_clean(); - } + ob_end_clean(); // and what actually got sent? $this->assertHeaderEmitted('Set-Cookie: foo=onething;'); diff --git a/tests/system/HTTP/DownloadResponseTest.php b/tests/system/HTTP/DownloadResponseTest.php index d9c5e30fe207..d7cfcf937612 100644 --- a/tests/system/HTTP/DownloadResponseTest.php +++ b/tests/system/HTTP/DownloadResponseTest.php @@ -377,9 +377,7 @@ public function testRealOutput(): void // send it ob_start(); $response->send(); - if (ob_get_level() > 0) { - ob_end_clean(); - } + ob_end_clean(); // and what actually got sent? $this->assertHeaderEmitted('Content-Length: ' . filesize(__FILE__)); diff --git a/tests/system/HTTP/ResponseSendTest.php b/tests/system/HTTP/ResponseSendTest.php index 211d806ba487..93076b9a3756 100644 --- a/tests/system/HTTP/ResponseSendTest.php +++ b/tests/system/HTTP/ResponseSendTest.php @@ -67,9 +67,7 @@ public function testHeadersMissingDate(): void // send it ob_start(); $response->send(); - if (ob_get_level() > 0) { - ob_end_clean(); - } + ob_end_clean(); // and what actually got sent? $this->assertHeaderEmitted('Date:'); @@ -102,9 +100,7 @@ public function testHeadersWithCSP(): void // send it ob_start(); $response->send(); - if (ob_get_level() > 0) { - ob_end_clean(); - } + ob_end_clean(); // and what actually got sent?; test both ways $this->assertHeaderEmitted('Content-Security-Policy:'); @@ -140,9 +136,7 @@ public function testRedirectResponseCookies(): void // send it ob_start(); $response->send(); - if (ob_get_level() > 0) { - ob_end_clean(); - } + ob_end_clean(); // and what actually got sent? $this->assertHeaderEmitted('Set-Cookie: foo=bar;'); @@ -213,9 +207,7 @@ public function testHeaderOverride(): void header('Access-Control-Expose-Headers: Content-Encoding'); header('Allow: GET, POST'); $response->send(); - if (ob_get_level() > 0) { - ob_end_clean(); - } + ob_end_clean(); // single header $this->assertHeaderEmitted('Vary: Accept-Encoding'); diff --git a/tests/system/Test/BootstrapFCPATHTest.php b/tests/system/Test/BootstrapFCPATHTest.php index 2af9b87b560d..6b876006c3d1 100644 --- a/tests/system/Test/BootstrapFCPATHTest.php +++ b/tests/system/Test/BootstrapFCPATHTest.php @@ -100,7 +100,7 @@ private function fileContents(): string return $fileContents . 'echo FCPATH;' . PHP_EOL; } - private function readOutput(string $file): false|string + private function readOutput(string $file): string { ob_start(); system('php -f ' . $file); diff --git a/tests/system/Test/TestCaseEmissionsTest.php b/tests/system/Test/TestCaseEmissionsTest.php index 8ebcdcc28d92..224121f60012 100644 --- a/tests/system/Test/TestCaseEmissionsTest.php +++ b/tests/system/Test/TestCaseEmissionsTest.php @@ -62,9 +62,7 @@ public function testHeadersEmitted(): void // send it ob_start(); $response->send(); - if (ob_get_level() > 0) { - ob_end_clean(); - } + ob_end_clean(); // and what actually got sent?; test both ways $this->assertHeaderEmitted('Set-Cookie: foo=bar;'); @@ -90,9 +88,7 @@ public function testHeadersNotEmitted(): void // send it ob_start(); $response->send(); // what really was sent - if (ob_get_level() > 0) { - ob_end_clean(); - } + ob_end_clean(); $this->assertHeaderNotEmitted('Set-Cookie: pop=corn', true); } From ad8526ffed6a5fd8daa6ad8a03b77493e8254dfb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 08:32:30 +0700 Subject: [PATCH 57/65] chore(deps-dev): update boundwize/structarmed requirement (#10365) Updates the requirements on [boundwize/structarmed](https://github.com/boundwize/structarmed) to permit the latest version. Updates `boundwize/structarmed` to 0.14.7 - [Release notes](https://github.com/boundwize/structarmed/releases) - [Commits](https://github.com/boundwize/structarmed/compare/0.14.6...0.14.7) --- updated-dependencies: - dependency-name: boundwize/structarmed dependency-version: 0.14.7 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index bb24bbdd48f4..b547cc9e9794 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "psr/log": "^3.0" }, "require-dev": { - "boundwize/structarmed": "0.14.6", + "boundwize/structarmed": "0.14.7", "codeigniter/phpstan-codeigniter": "^2.1", "fakerphp/faker": "^1.24", "kint-php/kint": "^6.1", From 297ce28080322031f2f688140b1b8131677ab470 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 02:43:55 +0700 Subject: [PATCH 58/65] chore(deps-dev): update boundwize/structarmed requirement (#10368) Updates the requirements on [boundwize/structarmed](https://github.com/boundwize/structarmed) to permit the latest version. Updates `boundwize/structarmed` to 0.14.9 - [Release notes](https://github.com/boundwize/structarmed/releases) - [Commits](https://github.com/boundwize/structarmed/compare/0.14.7...0.14.9) --- updated-dependencies: - dependency-name: boundwize/structarmed dependency-version: 0.14.9 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index b547cc9e9794..7c255bde18ca 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "psr/log": "^3.0" }, "require-dev": { - "boundwize/structarmed": "0.14.7", + "boundwize/structarmed": "0.14.9", "codeigniter/phpstan-codeigniter": "^2.1", "fakerphp/faker": "^1.24", "kint-php/kint": "^6.1", From 5e7c6efbda299a33466e0510965e5386ca35183a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 22:38:40 +0700 Subject: [PATCH 59/65] chore(deps-dev): update boundwize/structarmed requirement (#10371) Updates the requirements on [boundwize/structarmed](https://github.com/boundwize/structarmed) to permit the latest version. Updates `boundwize/structarmed` to 0.14.10 - [Release notes](https://github.com/boundwize/structarmed/releases) - [Commits](https://github.com/boundwize/structarmed/compare/0.14.9...0.14.10) --- updated-dependencies: - dependency-name: boundwize/structarmed dependency-version: 0.14.10 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 7c255bde18ca..49236ee763b5 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "psr/log": "^3.0" }, "require-dev": { - "boundwize/structarmed": "0.14.9", + "boundwize/structarmed": "0.14.10", "codeigniter/phpstan-codeigniter": "^2.1", "fakerphp/faker": "^1.24", "kint-php/kint": "^6.1", From d9ec94b7ec4bbded8d66a8dc07ed8ceb7f402f69 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Mon, 6 Jul 2026 11:37:05 +0700 Subject: [PATCH 60/65] chore: Bump Rector to 2.5.3 and re-run it (#10375) * chore: Bump Rector to 2.5.3 and re-run it * chore: apply cs fix --- composer.json | 2 +- rector.php | 10 +++++----- system/Database/BaseBuilder.php | 4 ++-- system/Database/MySQLi/Builder.php | 2 +- system/Database/OCI8/Builder.php | 6 +++--- system/Database/SQLSRV/Builder.php | 2 +- tests/system/Test/FeatureTestTraitTest.php | 10 +++++----- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/composer.json b/composer.json index 49236ee763b5..57277bc5011b 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.5.2", + "rector/rector": "2.5.3", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { diff --git a/rector.php b/rector.php index dcf59d665e7f..c3a1b22ad716 100644 --- a/rector.php +++ b/rector.php @@ -12,6 +12,7 @@ */ use Rector\Caching\ValueObject\Storage\FileCacheStorage; +use Rector\CodeQuality\Rector\BooleanNot\NegatedAndsToPositiveOrsRector; use Rector\CodeQuality\Rector\Empty_\SimplifyEmptyCheckOnEmptyArrayRector; use Rector\CodeQuality\Rector\FuncCall\CompactToVariablesRector; use Rector\CodeQuality\Rector\FunctionLike\SimplifyUselessVariableRector; @@ -22,6 +23,7 @@ use Rector\CodingStyle\Rector\FuncCall\CountArrayToEmptyArrayComparisonRector; use Rector\CodingStyle\Rector\FuncCall\VersionCompareFuncCallToConstantRector; use Rector\Config\RectorConfig; +use Rector\DeadCode\Rector\ClassMethod\RemoveDuplicatedReturnSelfDocblockRector; use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedConstructorParamRector; use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPrivateMethodRector; use Rector\DeadCode\Rector\MethodCall\RemoveNullArgOnNullDefaultParamRector; @@ -36,7 +38,6 @@ use Rector\PHPUnit\CodeQuality\Rector\Class_\YieldDataProviderRector; use Rector\PHPUnit\CodeQuality\Rector\FuncCall\AssertFuncCallToPHPUnitAssertRector; use Rector\PHPUnit\CodeQuality\Rector\StmtsAwareInterface\DeclareStrictTypesTestsRector; -use Rector\PostRector\Rector\UnusedImportRemovingPostRector; use Rector\Privatization\Rector\Class_\FinalizeTestCaseClassRector; use Rector\Privatization\Rector\Property\PrivatizeFinalClassPropertyRector; use Rector\Renaming\Rector\ConstFetch\RenameConstantRector; @@ -175,10 +176,9 @@ __DIR__ . '/tests/system/Models', ], - UnusedImportRemovingPostRector::class => [ - // buggy on auto import removed - __DIR__ . '/system/HTTP/Response.php', - ], + // to be applied in separate PRs to ease review + NegatedAndsToPositiveOrsRector::class, + RemoveDuplicatedReturnSelfDocblockRector::class, ]) // auto import fully qualified class names ->withImportNames() diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index ce87de3c356f..f1a9428c13eb 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -2686,7 +2686,7 @@ protected function _updateBatch(string $table, array $keys, array $values): stri $sql .= 'WHERE ' . implode( ' AND ', array_map( - static fn ($key, $value) => ( + static fn ($key, $value): RawSql|string => ( ($value instanceof RawSql && is_string($key)) ? $table . '.' . $key . ' = ' . $value @@ -2936,7 +2936,7 @@ protected function _deleteBatch(string $table, array $keys, array $values): stri $sql .= 'ON ' . implode( ' AND ', array_map( - static fn ($key, $value) => ( + static fn ($key, $value): RawSql|string => ( $value instanceof RawSql ? $value : ( diff --git a/system/Database/MySQLi/Builder.php b/system/Database/MySQLi/Builder.php index a9e248fc4584..bba90421e9e4 100644 --- a/system/Database/MySQLi/Builder.php +++ b/system/Database/MySQLi/Builder.php @@ -92,7 +92,7 @@ protected function _updateBatch(string $table, array $keys, array $values): stri $sql .= 'ON ' . implode( ' AND ', array_map( - static fn ($key, $value) => ( + static fn ($key, $value): RawSql|string => ( ($value instanceof RawSql && is_string($key)) ? $table . '.' . $key . ' = ' . $value diff --git a/system/Database/OCI8/Builder.php b/system/Database/OCI8/Builder.php index 59195b3b533a..ff5b1289c34a 100644 --- a/system/Database/OCI8/Builder.php +++ b/system/Database/OCI8/Builder.php @@ -248,7 +248,7 @@ protected function _updateBatch(string $table, array $keys, array $values): stri $sql .= 'ON (' . implode( ' AND ', array_map( - static fn ($key, $value) => ( + static fn ($key, $value): RawSql|string => ( ($value instanceof RawSql && is_string($key)) ? $table . '.' . $key . ' = ' . $value @@ -353,7 +353,7 @@ protected function _upsertBatch(string $table, array $keys, array $values): stri $sql .= implode( ' AND ', array_map( - static fn ($key, $value) => ( + static fn ($key, $value): RawSql|string => ( ($value instanceof RawSql && is_string($key)) ? $table . '.' . $key . ' = ' . $value @@ -442,7 +442,7 @@ protected function _deleteBatch(string $table, array $keys, array $values): stri $sql .= 'WHERE ' . implode( ' AND ', array_map( - static fn ($key, $value) => ( + static fn ($key, $value): RawSql|string => ( $value instanceof RawSql ? $value : ( diff --git a/system/Database/SQLSRV/Builder.php b/system/Database/SQLSRV/Builder.php index 098b08599808..cd7e7968fe72 100644 --- a/system/Database/SQLSRV/Builder.php +++ b/system/Database/SQLSRV/Builder.php @@ -752,7 +752,7 @@ protected function _upsertBatch(string $table, array $keys, array $values): stri $sql .= implode( ' AND ', array_map( - static fn ($key, $value) => ( + static fn ($key, $value): RawSql|string => ( ($value instanceof RawSql && is_string($key)) ? $fullTableName . '.' . $key . ' = ' . $value diff --git a/tests/system/Test/FeatureTestTraitTest.php b/tests/system/Test/FeatureTestTraitTest.php index f36767f7f971..79a00104a079 100644 --- a/tests/system/Test/FeatureTestTraitTest.php +++ b/tests/system/Test/FeatureTestTraitTest.php @@ -412,7 +412,7 @@ public function testCallGetWithParams(): void [ 'GET', 'home', - static fn () => json_encode(service('request')->getGet()), + static fn (): false|string => json_encode(service('request')->getGet()), ], ]); $data = [ @@ -439,7 +439,7 @@ public function testCallGetWithParamsAndREQUEST(): void [ 'GET', 'home', - static fn () => json_encode(service('request')->fetchGlobal('request')), + static fn (): false|string => json_encode(service('request')->fetchGlobal('request')), ], ]); $data = [ @@ -466,7 +466,7 @@ public function testCallPostWithParams(): void [ 'POST', 'home', - static fn () => json_encode(service('request')->getPost()), + static fn (): false|string => json_encode(service('request')->getPost()), ], ]); $data = [ @@ -493,7 +493,7 @@ public function testCallPostWithParamsAndREQUEST(): void [ 'POST', 'home', - static fn () => json_encode(service('request')->fetchGlobal('request')), + static fn (): false|string => json_encode(service('request')->fetchGlobal('request')), ], ]); $data = [ @@ -544,7 +544,7 @@ public function testCallPutWithJsonRequestAndREQUEST(): void [ 'PUT', 'home', - static fn () => json_encode(service('request')->fetchGlobal('request')), + static fn (): false|string => json_encode(service('request')->fetchGlobal('request')), ], ]); $data = [ From 9a75151e4c687713499b0dc1d1f00b98e175b8b7 Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Tue, 7 Jul 2026 08:13:56 +0200 Subject: [PATCH 61/65] refactor(database): optimize `_processForeignKeys()` string allocations and loop invariants (#10351) * perf: optimize _processForeignKeys() Refactors _processForeignKeys() to: - calculate invariant SQL fragments outside the loop - cache escaped table identifier - build SQL string locally before concatenating to the array, saving intermediate string allocations * refactor: apply code review fixes for _processForeignKeys optimization - Restore fast return without foreign keys\n- Remove unrelated formatting changes\n- Remove non-English comment --- system/Database/Forge.php | 53 ++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/system/Database/Forge.php b/system/Database/Forge.php index f6eb617fd389..9291683acb5f 100644 --- a/system/Database/Forge.php +++ b/system/Database/Forge.php @@ -1209,6 +1209,10 @@ protected function _processIndexes(string $table, bool $asQuery = false): array */ protected function _processForeignKeys(string $table, bool $asQuery = false): array { + if ($this->foreignKeys === []) { + return ['']; + } + $errorNames = []; foreach ($this->foreignKeys as $fkeyInfo) { @@ -1225,40 +1229,43 @@ protected function _processForeignKeys(string $table, bool $asQuery = false): ar throw new DatabaseException(lang('Database.fieldNotExists', $errorNames)); } - $sqls = ['']; - - foreach ($this->foreignKeys as $index => $fkey) { - if ($asQuery === false) { - $index = 0; - } else { - $sqls[$index] = ''; - } + $sqls = ['']; + $isOci8 = $this->db->DBDriver === 'OCI8'; + $dbPrefix = $this->db->DBPrefix; + $fkSuffix = $isOci8 ? '_fk' : '_foreign'; - $nameIndex = $fkey['fkName'] !== '' ? - $fkey['fkName'] : - $table . '_' . implode('_', $fkey['field']) . ($this->db->DBDriver === 'OCI8' ? '_fk' : '_foreign'); + $prefixSql = $asQuery + ? 'ALTER TABLE ' . $this->db->escapeIdentifiers($dbPrefix . $table) . ' ADD ' + : ",\n\t"; - $nameIndexFilled = $this->db->escapeIdentifiers($nameIndex); - $foreignKeyFilled = implode(', ', $this->db->escapeIdentifiers($fkey['field'])); - $referenceTableFilled = $this->db->escapeIdentifiers($this->db->DBPrefix . $fkey['referenceTable']); - $referenceFieldFilled = implode(', ', $this->db->escapeIdentifiers($fkey['referenceField'])); + foreach ($this->foreignKeys as $index => $fkey) { + $idx = $asQuery ? $index : 0; if ($asQuery) { - $sqls[$index] .= 'ALTER TABLE ' . $this->db->escapeIdentifiers($this->db->DBPrefix . $table) . ' ADD '; - } else { - $sqls[$index] .= ",\n\t"; + $sqls[$idx] = ''; } - $formatSql = 'CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s(%s)'; - $sqls[$index] .= sprintf($formatSql, $nameIndexFilled, $foreignKeyFilled, $referenceTableFilled, $referenceFieldFilled); + $nameIndex = $fkey['fkName'] !== '' + ? $fkey['fkName'] + : $table . '_' . implode('_', $fkey['field']) . $fkSuffix; + + $sql = $prefixSql . sprintf( + 'CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s(%s)', + $this->db->escapeIdentifiers($nameIndex), + implode(', ', $this->db->escapeIdentifiers($fkey['field'])), + $this->db->escapeIdentifiers($dbPrefix . $fkey['referenceTable']), + implode(', ', $this->db->escapeIdentifiers($fkey['referenceField'])), + ); if ($fkey['onDelete'] !== false && in_array($fkey['onDelete'], $this->fkAllowActions, true)) { - $sqls[$index] .= ' ON DELETE ' . $fkey['onDelete']; + $sql .= ' ON DELETE ' . $fkey['onDelete']; } - if ($this->db->DBDriver !== 'OCI8' && $fkey['onUpdate'] !== false && in_array($fkey['onUpdate'], $this->fkAllowActions, true)) { - $sqls[$index] .= ' ON UPDATE ' . $fkey['onUpdate']; + if (! $isOci8 && $fkey['onUpdate'] !== false && in_array($fkey['onUpdate'], $this->fkAllowActions, true)) { + $sql .= ' ON UPDATE ' . $fkey['onUpdate']; } + + $sqls[$idx] .= $sql; } return $sqls; From 4d7346d2dfdf87827f4d21430148c049381cda01 Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Tue, 7 Jul 2026 08:58:40 +0200 Subject: [PATCH 62/65] fix: preserve zero values in XML export (#10367) --- system/Database/BaseUtils.php | 2 +- tests/system/Database/Live/DbUtilsTest.php | 20 ++++++++++++++++++++ user_guide_src/source/changelogs/v4.7.4.rst | 1 + utils/phpstan-baseline/empty.notAllowed.neon | 2 +- 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/system/Database/BaseUtils.php b/system/Database/BaseUtils.php index 0286e177a060..938c28364444 100644 --- a/system/Database/BaseUtils.php +++ b/system/Database/BaseUtils.php @@ -250,7 +250,7 @@ public function getXMLFromResult(ResultInterface $query, array $params = []): st $xml .= $tab . '<' . $element . '>' . $newline; foreach ($row as $key => $val) { - $val = empty($val) ? '' : xml_convert((string) $val); + $val = ($val === null || $val === '') ? '' : xml_convert((string) $val); $xml .= $tab . $tab . '<' . $key . '>' . $val . '' . $newline; } diff --git a/tests/system/Database/Live/DbUtilsTest.php b/tests/system/Database/Live/DbUtilsTest.php index e20a32d8bed2..c90c8c6333a4 100644 --- a/tests/system/Database/Live/DbUtilsTest.php +++ b/tests/system/Database/Live/DbUtilsTest.php @@ -205,4 +205,24 @@ public function testUtilsXMLFromResult(): void $this->assertSame($expected, $actual); } + + public function testUtilsXMLFromResultWithZero(): void + { + $this->db->table('job')->insert([ + 'name' => '0', + 'description' => 'Testing zero value', + ]); + $data = $this->db->table('job')->where('name', '0')->get(); + + $util = (new Database())->loadUtils($this->db); + + $data = $util->getXMLFromResult($data); + + $expected = '50Testing zero value'; + + $actual = preg_replace('#\R+#', '', $data); + $actual = preg_replace('/[ ]{2,}|[\t]/', '', $actual); + + $this->assertSame($expected, $actual); + } } diff --git a/user_guide_src/source/changelogs/v4.7.4.rst b/user_guide_src/source/changelogs/v4.7.4.rst index 531c16a7cc5b..1040c407298e 100644 --- a/user_guide_src/source/changelogs/v4.7.4.rst +++ b/user_guide_src/source/changelogs/v4.7.4.rst @@ -82,6 +82,7 @@ Bugs Fixed - **Commands:** Fixed a bug where ``spark lang:find`` treated translation keys already provided by the framework or another namespace (such as ``Errors.*`` in ``system/Language``) as new, listing them under ``--show-new`` and writing untranslated placeholders into ``app/Language`` that overrode the existing translations. - **Config:** Fixed a bug where ``BaseService::injectMock`` did not apply ``strtolower`` consistently, causing inconsistent Service and Mock registration and resolution. - **Database:** Fixed a bug where ``updateBatch()`` could be called after Query Builder ``where()`` conditions, even though it's not supported. In this situation, now the ``DatabaseException`` is thrown. +- **Database:** Fixed a bug in ``BaseUtils::getXMLFromResult()`` where database values of ``0`` or ``'0'`` were treated as empty and omitted from the generated XML export. - **Encryption:** Fixed bugs in ``SodiumHandler`` where runtime ``blockSize`` overrides without ``key`` were handled incorrectly, invalid key lengths could leak native Sodium errors, and mismatched decrypt ``blockSize`` values could throw ``SodiumException`` instead of ``EncryptionException``. - **Filters:** Fixed a bug in ``InvalidChars`` filter where invalid UTF-8 or control characters in array keys were not checked. - **HTTP:** Fixed a bug where the User Agent library reported Safari's WebKit version instead of the browser version from the ``Version`` token. diff --git a/utils/phpstan-baseline/empty.notAllowed.neon b/utils/phpstan-baseline/empty.notAllowed.neon index a302b0433f39..1d8d1796b5c2 100644 --- a/utils/phpstan-baseline/empty.notAllowed.neon +++ b/utils/phpstan-baseline/empty.notAllowed.neon @@ -54,7 +54,7 @@ parameters: - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 4 + count: 3 path: ../../system/Database/BaseUtils.php - From 441c2266123d5f56eac10bc3dc25055c4bce7a90 Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Tue, 7 Jul 2026 09:00:35 +0200 Subject: [PATCH 63/65] fix: support array indexes in `getPostGet()` and `getGetPost()` (#10362) * fix: support array indexes in getPostGet() and getGetPost() * fix phpstan --- system/HTTP/IncomingRequest.php | 20 +++++++++++++ tests/system/HTTP/IncomingRequestTest.php | 28 +++++++++++++++++++ user_guide_src/source/changelogs/v4.7.4.rst | 1 + .../source/incoming/incomingrequest.rst | 15 ++++++---- utils/phpstan-baseline/loader.neon | 2 +- utils/phpstan-baseline/method.notFound.neon | 6 ++-- 6 files changed, 63 insertions(+), 9 deletions(-) diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index 457d4da0fa84..3a43d6dec050 100644 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -602,6 +602,16 @@ public function getPostGet($index = null, $filter = null, $flags = null) return array_merge($this->getGet($index, $filter, $flags), $this->getPost($index, $filter, $flags)); } + if (is_array($index)) { + $output = []; + + foreach ($index as $key) { + $output[$key] = $this->getPostGet($key, $filter, $flags); + } + + return $output; + } + // Use $_POST directly here, since filter_has_var only // checks the initial POST data, not anything that might // have been added since. @@ -625,6 +635,16 @@ public function getGetPost($index = null, $filter = null, $flags = null) return array_merge($this->getPost($index, $filter, $flags), $this->getGet($index, $filter, $flags)); } + if (is_array($index)) { + $output = []; + + foreach ($index as $key) { + $output[$key] = $this->getGetPost($key, $filter, $flags); + } + + return $output; + } + // Use $_GET directly here, since filter_has_var only // checks the initial GET data, not anything that might // have been added since. diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index d4a15da40537..2634e31a4b84 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -101,6 +101,34 @@ public function testCanGrabPostBeforeGet(): void $this->assertSame('3', $this->request->getGetPost('TEST')); } + public function testCanGrabMultiplePostAndGetVars(): void + { + service('superglobals') + ->setPostArray([ + 'post' => 'post value', + 'shared' => 'post shared value', + ]) + ->setGetArray([ + 'get' => 'get value', + 'shared' => 'get shared value', + ]); + + $index = ['post', 'get', 'shared', 'missing']; + + $this->assertSame([ + 'post' => 'post value', + 'get' => 'get value', + 'shared' => 'post shared value', + 'missing' => null, + ], $this->request->getPostGet($index)); + $this->assertSame([ + 'post' => 'post value', + 'get' => 'get value', + 'shared' => 'get shared value', + 'missing' => null, + ], $this->request->getGetPost($index)); + } + public function testNoOldInput(): void { $this->assertNull($this->request->getOldInput('name')); diff --git a/user_guide_src/source/changelogs/v4.7.4.rst b/user_guide_src/source/changelogs/v4.7.4.rst index 1040c407298e..69225e083cd7 100644 --- a/user_guide_src/source/changelogs/v4.7.4.rst +++ b/user_guide_src/source/changelogs/v4.7.4.rst @@ -85,6 +85,7 @@ Bugs Fixed - **Database:** Fixed a bug in ``BaseUtils::getXMLFromResult()`` where database values of ``0`` or ``'0'`` were treated as empty and omitted from the generated XML export. - **Encryption:** Fixed bugs in ``SodiumHandler`` where runtime ``blockSize`` overrides without ``key`` were handled incorrectly, invalid key lengths could leak native Sodium errors, and mismatched decrypt ``blockSize`` values could throw ``SodiumException`` instead of ``EncryptionException``. - **Filters:** Fixed a bug in ``InvalidChars`` filter where invalid UTF-8 or control characters in array keys were not checked. +- **HTTP:** Fixed a bug where ``IncomingRequest::getPostGet()`` and ``IncomingRequest::getGetPost()`` threw a ``TypeError`` when passed an array of indexes. - **HTTP:** Fixed a bug where the User Agent library reported Safari's WebKit version instead of the browser version from the ``Version`` token. - **Model:** Fixed a bug in ``Model::objectToRawArray()`` where the ``$recursive`` parameter was ignored. - **Security:** Fixed a bug where ``CheckPhpIni`` could raise a type error when ``ini_get_all()`` returned ``null`` for a configured directive value. diff --git a/user_guide_src/source/incoming/incomingrequest.rst b/user_guide_src/source/incoming/incomingrequest.rst index ebe399d7aa7d..99c5762129d6 100644 --- a/user_guide_src/source/incoming/incomingrequest.rst +++ b/user_guide_src/source/incoming/incomingrequest.rst @@ -384,7 +384,7 @@ The methods provided by the parent classes that are available are: .. php:method:: getGet([$index = null[, $filter = null[, $flags = null]]]) - :param string $index: The name of the variable/key to look for. + :param array|string $index: The name of the variable/key to look for, or an array of names. :param int $filter: The type of filter to apply. A list of filters can be found in `Types of filters `__. :param int $flags: Flags to apply. A list of flags can be found in @@ -423,7 +423,7 @@ The methods provided by the parent classes that are available are: .. php:method:: getPost([$index = null[, $filter = null[, $flags = null]]]) - :param string $index: The name of the variable/key to look for. + :param array|string $index: The name of the variable/key to look for, or an array of names. :param int $filter: The type of filter to apply. A list of filters can be found `here `__. :param int $flags: Flags to apply. A list of flags can be found @@ -435,7 +435,7 @@ The methods provided by the parent classes that are available are: .. php:method:: getPostGet([$index = null[, $filter = null[, $flags = null]]]) - :param string $index: The name of the variable/key to look for. + :param array|string $index: The name of the variable/key to look for, or an array of names. :param int $filter: The type of filter to apply. A list of filters can be found in `Types of filters `__. :param int $flags: Flags to apply. A list of flags can be found in @@ -450,12 +450,15 @@ The methods provided by the parent classes that are available are: .. literalinclude:: incomingrequest/032.php + If an array of indexes is specified, the method returns an associative array and applies + the POST-then-GET lookup order to each index. + If no index is specified, it will return both POST and GET streams combined. Although POST data will be preferred in case of name conflict. .. php:method:: getGetPost([$index = null[, $filter = null[, $flags = null]]]) - :param string $index: The name of the variable/key to look for. + :param array|string $index: The name of the variable/key to look for, or an array of names. :param int $filter: The type of filter to apply. A list of filters can be found in `Types of filters `__. :param int $flags: Flags to apply. A list of flags can be found in @@ -470,6 +473,9 @@ The methods provided by the parent classes that are available are: .. literalinclude:: incomingrequest/033.php + If an array of indexes is specified, the method returns an associative array and applies + the GET-then-POST lookup order to each index. + If no index is specified, it will return both GET and POST streams combined. Although GET data will be preferred in case of name conflict. @@ -534,4 +540,3 @@ The methods provided by the parent classes that are available are: .. note:: Prior to v4.4.0, this was the safest method to determine the "current URI", since ``IncomingRequest::$uri`` might not be aware of the complete App configuration for base URLs. - diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 2e67047327fc..bf2719972413 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 1825 errors +# total 1827 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/method.notFound.neon b/utils/phpstan-baseline/method.notFound.neon index 9075d775989d..8ced93d9ad27 100644 --- a/utils/phpstan-baseline/method.notFound.neon +++ b/utils/phpstan-baseline/method.notFound.neon @@ -1,4 +1,4 @@ -# total 78 errors +# total 80 errors parameters: ignoreErrors: @@ -69,7 +69,7 @@ parameters: - message: '#^Call to an undefined method CodeIgniter\\HTTP\\Request\:\:getGetPost\(\)\.$#' - count: 5 + count: 6 path: ../../tests/system/HTTP/IncomingRequestTest.php - @@ -89,7 +89,7 @@ parameters: - message: '#^Call to an undefined method CodeIgniter\\HTTP\\Request\:\:getPostGet\(\)\.$#' - count: 5 + count: 6 path: ../../tests/system/HTTP/IncomingRequestTest.php - From 6ea12271ef91f4d6a47c7ffbf186f81601aedc79 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 14:15:36 +0700 Subject: [PATCH 64/65] chore(deps-dev): update rector/rector requirement (#10378) Updates the requirements on [rector/rector](https://github.com/rectorphp/rector) to permit the latest version. Updates `rector/rector` to 2.5.4 - [Release notes](https://github.com/rectorphp/rector/releases) - [Commits](https://github.com/rectorphp/rector/compare/2.5.3...2.5.4) --- updated-dependencies: - dependency-name: rector/rector dependency-version: 2.5.4 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 57277bc5011b..8e0990839d9f 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.5.3", + "rector/rector": "2.5.4", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { From cf49152d610ca338dda8ead19c43498ab26719c6 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Tue, 7 Jul 2026 17:02:23 +0800 Subject: [PATCH 65/65] Prep for 4.7.4 release (#10379) --- CHANGELOG.md | 76 +++++++++++++++++++ phpdoc.dist.xml | 2 +- system/CodeIgniter.php | 2 +- user_guide_src/source/changelogs/v4.7.4.rst | 18 +---- user_guide_src/source/conf.py | 2 +- .../source/installation/upgrade_474.rst | 12 +-- 6 files changed, 82 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f8cbd59deb0..82d3974606b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,81 @@ # Changelog +## [v4.7.4](https://github.com/codeigniter4/CodeIgniter4/tree/v4.7.4) (2026-07-07) +[Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/v4.7.3...v4.7.4) + +### Security + +* **IncomingRequest:** *HTTPS detection via client-supplied headers* was fixed. + ``IncomingRequest::isSecure()`` now trusts the ``X-Forwarded-Proto`` and + ``Front-End-Https`` headers only when the request comes from a trusted proxy + configured in ``Config\App::$proxyIPs``. + See the `Security advisory GHSA-7wmf-pw8j-mc78 `_ + for more information. + +* **Query Builder:** Fixed a SQL injection vulnerability in ``deleteBatch()``. + When ``deleteBatch()`` was used together with ``where()`` conditions, the + bound values from the WHERE clause were substituted into the generated SQL + with their escape flag ignored, so they were never escaped or quoted. The + WHERE binds are now escaped in the same way as a regular ``delete()``. + + See the `Security advisory GHSA-c9w5-rwh3-7pm9 `_ + for more information. + +* **UploadedFile:** ``UploadedFile::move()`` now sanitizes the client-provided + filename when called without a second argument. Previously, the unsanitized + client filename was used as the default, allowing path traversal sequences + (e.g. ``../../public/shell.php``) to write the uploaded file outside the + intended directory. A name explicitly passed as the second argument is not + sanitized and remains the caller's responsibility. + See the `Security advisory GHSA-hhmc-q9hp-r662 `_ + for more information. + +* **Validation:** The ``is_image`` and ``mime_in`` file upload validation rules + now also verify non-empty client filename extensions. Previously, these rules + classified an upload solely by its content-derived MIME type, so a file with a + dangerous client extension (for example, a ``.php`` file prepended with image + magic bytes) could pass validation while keeping its original extension on + disk. + See the `Security advisory GHSA-mmj4-63m4-r6h5 `_ + for more information. + + The ``is_image`` rule now rejects uploads when a non-empty client filename + extension is not an image extension. The ``mime_in`` rule now rejects uploads + when a non-empty client filename extension does not match the detected file + content, matching the same extension/content agreement used by ``ext_in``. + Files uploaded without any extension (such as JavaScript ``Blob`` uploads) + are still accepted, since the rules validate the file content. + +### Fixed Bugs + +* fix: prevent updateBatch with existing where conditions by @michalsn in https://github.com/codeigniter4/CodeIgniter4/pull/10236 +* fix: detect Safari version from Version token by @memleakd in https://github.com/codeigniter4/CodeIgniter4/pull/10251 +* fix: nested transformer request scope leakage by @michalsn in https://github.com/codeigniter4/CodeIgniter4/pull/10281 +* fix(model): pass $recursive parameter to parent in objectToRawArray by @gr8man in https://github.com/codeigniter4/CodeIgniter4/pull/10258 +* fix: preserve enclosing stream filter when using `MockInputOutput` by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/10307 +* fix: skip already-translated keys in `spark lang:find` by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/10308 +* fix(Filters): check both keys and values in `InvalidChars` arrays by @gr8man in https://github.com/codeigniter4/CodeIgniter4/pull/10303 +* fix: use dynamic lockMaxRetries limit in RedisHandler by @gr8man in https://github.com/codeigniter4/CodeIgniter4/pull/10295 +* fix: preserve sub-namespace when generating Entity from Model (i.e., `--return entity`) by @xgrind in https://github.com/codeigniter4/CodeIgniter4/pull/10232 +* fix: `env()` TypeError for non-string `$_SERVER` values + `esc()` fixes by @gr8man in https://github.com/codeigniter4/CodeIgniter4/pull/10305 +* fix: unify casing in BaseService::injectMock by @ThomasMeschke in https://github.com/codeigniter4/CodeIgniter4/pull/10316 +* fix: normalize SodiumHandler params and padding failures by @gr8man in https://github.com/codeigniter4/CodeIgniter4/pull/10321 +* fix(Validation): correct required_without logic and prevent array key warnings by @gr8man in https://github.com/codeigniter4/CodeIgniter4/pull/10328 +* fix: handle null ini values in phpini check by @Will-thom in https://github.com/codeigniter4/CodeIgniter4/pull/10233 +* fix: preserve zero values in XML export by @gr8man in https://github.com/codeigniter4/CodeIgniter4/pull/10367 +* fix: support array indexes in `getPostGet()` and `getGetPost()` by @michalsn in https://github.com/codeigniter4/CodeIgniter4/pull/10362 + +### Refactoring + +* refactor: narrow `Image` original dimension types to `int` by @michalsn in https://github.com/codeigniter4/CodeIgniter4/pull/10326 +* refactor(HTTP): optimize file path parsing in `download()` method by @gr8man in https://github.com/codeigniter4/CodeIgniter4/pull/10330 +* refactor(database): optimize groupGetType by caching it inside BaseBuilder loops by @gr8man in https://github.com/codeigniter4/CodeIgniter4/pull/10340 +* refactor: bump to phpstan-codeigniter v2.1 by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/10312 +* refactor: replace type `mixed` with more specific types by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/10345 +* refactor: optimize `prepQuotedPrintable()` with hash lookup and int-length tracking by @gr8man in https://github.com/codeigniter4/CodeIgniter4/pull/10344 +* refactor: add precise `array` phpdocs for `CLI` by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/10354 +* refactor(database): optimize `_processForeignKeys()` string allocations and loop invariants by @gr8man in https://github.com/codeigniter4/CodeIgniter4/pull/10351 + ## [v4.7.3](https://github.com/codeigniter4/CodeIgniter4/tree/v4.7.3) (2026-05-22) [Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/v4.7.2...v4.7.3) diff --git a/phpdoc.dist.xml b/phpdoc.dist.xml index 33101eeaa704..4ec68a4bfaea 100644 --- a/phpdoc.dist.xml +++ b/phpdoc.dist.xml @@ -10,7 +10,7 @@ api/build/ api/cache/ - + system diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 01d585eb246e..b806fe00a740 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.4-dev'; + public const CI_VERSION = '4.7.4'; /** * App startup time. diff --git a/user_guide_src/source/changelogs/v4.7.4.rst b/user_guide_src/source/changelogs/v4.7.4.rst index 69225e083cd7..48c90add3232 100644 --- a/user_guide_src/source/changelogs/v4.7.4.rst +++ b/user_guide_src/source/changelogs/v4.7.4.rst @@ -2,7 +2,7 @@ Version 4.7.4 ############# -Release Date: Unreleased +Release Date: July 7, 2026 **4.7.4 release of CodeIgniter4** @@ -55,22 +55,6 @@ SECURITY Files uploaded without any extension (such as JavaScript ``Blob`` uploads) are still accepted, since the rules validate the file content. -******** -BREAKING -******** - -*************** -Message Changes -*************** - -******* -Changes -******* - -************ -Deprecations -************ - ********** Bugs Fixed ********** diff --git a/user_guide_src/source/conf.py b/user_guide_src/source/conf.py index d7f994c7a943..820b3acd54a7 100644 --- a/user_guide_src/source/conf.py +++ b/user_guide_src/source/conf.py @@ -26,7 +26,7 @@ version = '4.7' # The full version, including alpha/beta/rc tags. -release = '4.7.3' +release = '4.7.4' # -- General configuration --------------------------------------------------- diff --git a/user_guide_src/source/installation/upgrade_474.rst b/user_guide_src/source/installation/upgrade_474.rst index e459eca6c465..dd236de98eac 100644 --- a/user_guide_src/source/installation/upgrade_474.rst +++ b/user_guide_src/source/installation/upgrade_474.rst @@ -12,10 +12,6 @@ Please refer to the upgrade instructions corresponding to your installation meth :local: :depth: 2 -********************** -Mandatory File Changes -********************** - **************** Breaking Changes **************** @@ -47,10 +43,6 @@ non-HTTPS, which may result in redirect loops. IPv4-mapped form to ``Config\App::$proxyIPs``, e.g., ``::ffff:192.168.5.21`` or ``::ffff:192.168.5.0/120``. -********************* -Breaking Enhancements -********************* - ************* Project Files ************* @@ -71,7 +63,7 @@ and it is recommended that you merge the updated versions with your application: Config ------ -- @TODO +- No config files were changed in this release. All Changes =========== @@ -79,4 +71,4 @@ 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 +- No project files were changed in this release.