diff --git a/.gitattributes b/.gitattributes index c41e331ff63c..3f67f2e35167 100644 --- a/.gitattributes +++ b/.gitattributes @@ -17,7 +17,6 @@ CONTRIBUTING.md export-ignore # contributor/development files tests/ export-ignore -tools/ export-ignore utils/ export-ignore .php-cs-fixer.dist.php export-ignore .php-cs-fixer.no-header.php export-ignore diff --git a/.github/workflows/reusable-phpunit-test.yml b/.github/workflows/reusable-phpunit-test.yml index 4943d3f3ac2c..84ade1d08862 100644 --- a/.github/workflows/reusable-phpunit-test.yml +++ b/.github/workflows/reusable-phpunit-test.yml @@ -95,13 +95,13 @@ jobs: mssql: image: mcr.microsoft.com/mssql/server:2022-latest env: - SA_PASSWORD: 1Secure*Password1 + MSSQL_SA_PASSWORD: 1Secure*Password1 ACCEPT_EULA: Y MSSQL_PID: Developer ports: - 1433:1433 options: >- - --health-cmd="/opt/mssql-tools/bin/sqlcmd -S 127.0.0.1 -U sa -P 1Secure*Password1 -Q 'SELECT @@VERSION'" + --health-cmd="/opt/mssql-tools18/bin/sqlcmd -C -S 127.0.0.1 -U sa -P 1Secure*Password1 -Q 'SELECT @@VERSION'" --health-interval=10s --health-timeout=5s --health-retries=3 diff --git a/.github/workflows/test-coding-standards.yml b/.github/workflows/test-coding-standards.yml index 7e5f18820288..f30475a76dba 100644 --- a/.github/workflows/test-coding-standards.yml +++ b/.github/workflows/test-coding-standards.yml @@ -57,14 +57,5 @@ jobs: - name: Install dependencies run: composer update --ansi --no-interaction - - name: Run lint on `app/`, `admin/`, `public/` - run: vendor/bin/php-cs-fixer fix --verbose --ansi --dry-run --config=.php-cs-fixer.no-header.php --using-cache=no --diff - - - name: Run lint on `system/`, `utils/`, and root PHP files - run: vendor/bin/php-cs-fixer fix --verbose --ansi --dry-run --using-cache=no --diff - - - name: Run lint on `tests` - run: vendor/bin/php-cs-fixer fix --verbose --ansi --dry-run --config=.php-cs-fixer.tests.php --using-cache=no --diff - - - name: Run lint on `user_guide_src/source/` - run: vendor/bin/php-cs-fixer fix --verbose --ansi --dry-run --config=.php-cs-fixer.user-guide.php --using-cache=no --diff + - name: Run lint + run: composer cs diff --git a/.github/workflows/test-phpstan.yml b/.github/workflows/test-phpstan.yml index 360b403ac923..aca331f30481 100644 --- a/.github/workflows/test-phpstan.yml +++ b/.github/workflows/test-phpstan.yml @@ -85,4 +85,4 @@ jobs: run: composer update --ansi --no-interaction - name: Run static analysis - run: vendor/bin/phpstan analyse + run: composer phpstan:check diff --git a/.github/workflows/test-psalm.yml b/.github/workflows/test-psalm.yml index a623109fbb15..b1f5891a46d9 100644 --- a/.github/workflows/test-psalm.yml +++ b/.github/workflows/test-psalm.yml @@ -24,7 +24,7 @@ jobs: build: name: Psalm Analysis runs-on: ubuntu-latest - if: "!contains(github.event.head_commit.message, '[ci skip]')" + if: (! contains(github.event.head_commit.message, '[ci skip]')) steps: - name: Checkout @@ -68,4 +68,4 @@ jobs: fi - name: Run Psalm analysis - run: vendor/bin/psalm + run: utils/vendor/bin/psalm diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 4ce5e25fbc1d..a50b126db075 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -28,8 +28,6 @@ 'ThirdParty', 'Validation/Views', ]) - ->notPath([ - ]) ->notName('#Foobar.php$#') ->append([ __FILE__, @@ -41,17 +39,12 @@ __DIR__ . '/spark', ]); -$overrides = [ - // for updating to coding-standard - 'modernize_strpos' => true, - 'ordered_attributes' => ['order' => [], 'sort_algorithm' => 'alpha'], - 'php_unit_attributes' => true, -]; +$overrides = []; $options = [ 'cacheFile' => 'build/.php-cs-fixer.cache', 'finder' => $finder, - 'customFixers' => FixerGenerator::create('vendor/nexusphp/cs-config/src/Fixer', 'Nexus\\CsConfig\\Fixer'), + 'customFixers' => FixerGenerator::create('utils/vendor/nexusphp/cs-config/src/Fixer', 'Nexus\\CsConfig\\Fixer'), 'customRules' => [ NoCodeSeparatorCommentFixer::name() => true, ], diff --git a/.php-cs-fixer.no-header.php b/.php-cs-fixer.no-header.php index e7d9647e317e..f3bc97dc78ac 100644 --- a/.php-cs-fixer.no-header.php +++ b/.php-cs-fixer.no-header.php @@ -11,12 +11,12 @@ * the LICENSE file that was distributed with this source code. */ -use CodeIgniter\CodingStandard\CodeIgniter4; -use Nexus\CsConfig\Factory; -use Nexus\CsConfig\Fixer\Comment\NoCodeSeparatorCommentFixer; -use Nexus\CsConfig\FixerGenerator; +use PhpCsFixer\ConfigInterface; use PhpCsFixer\Finder; +/** @var ConfigInterface $config */ +$config = require __DIR__ . '/.php-cs-fixer.dist.php'; + $finder = Finder::create() ->files() ->in([ @@ -30,19 +30,10 @@ ]); $overrides = [ - // for updating to coding-standard - 'modernize_strpos' => true, - 'ordered_attributes' => ['order' => [], 'sort_algorithm' => 'alpha'], - 'php_unit_attributes' => true, -]; - -$options = [ - 'cacheFile' => 'build/.php-cs-fixer.no-header.cache', - 'finder' => $finder, - 'customFixers' => FixerGenerator::create('vendor/nexusphp/cs-config/src/Fixer', 'Nexus\\CsConfig\\Fixer'), - 'customRules' => [ - NoCodeSeparatorCommentFixer::name() => true, - ], + 'header_comment' => false, ]; -return Factory::create(new CodeIgniter4(), $overrides, $options)->forProjects(); +return $config + ->setFinder($finder) + ->setCacheFile('build/.php-cs-fixer.no-header.cache') + ->setRules(array_merge($config->getRules(), $overrides)); diff --git a/.php-cs-fixer.tests.php b/.php-cs-fixer.tests.php index 1b4bfd71e30d..28a7124909e5 100644 --- a/.php-cs-fixer.tests.php +++ b/.php-cs-fixer.tests.php @@ -11,47 +11,29 @@ * the LICENSE file that was distributed with this source code. */ -use CodeIgniter\CodingStandard\CodeIgniter4; -use Nexus\CsConfig\Factory; -use Nexus\CsConfig\Fixer\Comment\NoCodeSeparatorCommentFixer; -use Nexus\CsConfig\FixerGenerator; +use PhpCsFixer\ConfigInterface; use PhpCsFixer\Finder; +/** @var ConfigInterface $config */ +$config = require __DIR__ . '/.php-cs-fixer.dist.php'; + $finder = Finder::create() ->files() ->in([ __DIR__ . '/tests', ]) - ->exclude([ - ]) ->notPath([ '_support/View/Cells/multiplier.php', '_support/View/Cells/colors.php', '_support/View/Cells/addition.php', ]) - ->notName('#Foobar.php$#') - ->append([ - ]); + ->notName('#Foobar.php$#'); $overrides = [ 'void_return' => true, - // for updating to coding-standard - 'modernize_strpos' => true, - 'ordered_attributes' => ['order' => [], 'sort_algorithm' => 'alpha'], - 'php_unit_attributes' => true, -]; - -$options = [ - 'cacheFile' => 'build/.php-cs-fixer.tests.cache', - 'finder' => $finder, - 'customFixers' => FixerGenerator::create('vendor/nexusphp/cs-config/src/Fixer', 'Nexus\\CsConfig\\Fixer'), - 'customRules' => [ - NoCodeSeparatorCommentFixer::name() => true, - ], ]; -return Factory::create(new CodeIgniter4(), $overrides, $options)->forLibrary( - 'CodeIgniter 4 framework', - 'CodeIgniter Foundation', - 'admin@codeigniter.com' -); +return $config + ->setFinder($finder) + ->setCacheFile('build/.php-cs-fixer.tests.cache') + ->setRules(array_merge($config->getRules(), $overrides)); diff --git a/.php-cs-fixer.user-guide.php b/.php-cs-fixer.user-guide.php index 3be7cd45c708..6b925ee8b0b7 100644 --- a/.php-cs-fixer.user-guide.php +++ b/.php-cs-fixer.user-guide.php @@ -11,12 +11,12 @@ * the LICENSE file that was distributed with this source code. */ -use CodeIgniter\CodingStandard\CodeIgniter4; -use Nexus\CsConfig\Factory; -use Nexus\CsConfig\Fixer\Comment\NoCodeSeparatorCommentFixer; -use Nexus\CsConfig\FixerGenerator; +use PhpCsFixer\ConfigInterface; use PhpCsFixer\Finder; +/** @var ConfigInterface $config */ +$config = require __DIR__ . '/.php-cs-fixer.dist.php'; + $finder = Finder::create() ->files() ->in([ @@ -32,6 +32,7 @@ $overrides = [ 'echo_tag_syntax' => false, + 'header_comment' => false, 'php_unit_internal_class' => false, 'no_unused_imports' => false, 'class_attributes_separation' => false, @@ -39,19 +40,9 @@ 'import_symbols' => false, 'leading_backslash_in_global_namespace' => true, ], - // for updating to coding-standard - 'modernize_strpos' => true, - 'ordered_attributes' => ['order' => [], 'sort_algorithm' => 'alpha'], - 'php_unit_attributes' => true, -]; - -$options = [ - 'cacheFile' => 'build/.php-cs-fixer.user-guide.cache', - 'finder' => $finder, - 'customFixers' => FixerGenerator::create('vendor/nexusphp/cs-config/src/Fixer', 'Nexus\\CsConfig\\Fixer'), - 'customRules' => [ - NoCodeSeparatorCommentFixer::name() => true, - ], ]; -return Factory::create(new CodeIgniter4(), $overrides, $options)->forProjects(); +return $config + ->setFinder($finder) + ->setCacheFile('build/.php-cs-fixer.user-guide.cache') + ->setRules(array_merge($config->getRules(), $overrides)); diff --git a/CHANGELOG.md b/CHANGELOG.md index 9351b205e637..1d2778139204 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## [v4.5.4](https://github.com/codeigniter4/CodeIgniter4/tree/v4.5.3) (2024-07-27) +[Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/v4.5.3...v4.5.4) + +### Fixed Bugs + +* fix: [OCI8] Easy Connect string validation by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/9006 +* fix: [QueryBuilder] select() with RawSql may cause TypeError by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/9009 +* fix: [QueryBuilder] `select()` does not escape after `NULL` by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/9010 +* fix: allow string as parameter to CURLRequest version by @tangix in https://github.com/codeigniter4/CodeIgniter4/pull/9021 +* fix: `spark phpini:check` may cause TypeError by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/9026 +* fix: Prevent invalid session handlers by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/9036 +* fix: DebugBar CSS for daisyUI by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/9046 +* docs: `referrer` is undefined by @totoprayogo1916 in https://github.com/codeigniter4/CodeIgniter4/pull/9059 +* fix: filters passed to the ``$routes->group()`` are not merged into the filters passed to the inner routes by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/9064 + +### Refactoring + +* refactor: use first class callable on function call by @samsonasik in https://github.com/codeigniter4/CodeIgniter4/pull/9004 +* refactor: enable AddClosureVoidReturnTypeWhereNoReturnRector to add void return on closure by @samsonasik in https://github.com/codeigniter4/CodeIgniter4/pull/9008 +* refactor: enable AddFunctionVoidReturnTypeWhereNoReturnRector to add void to functions by @samsonasik in https://github.com/codeigniter4/CodeIgniter4/pull/9014 +* refactor: Enable phpunit 10 attribute Rector rules by @samsonasik in https://github.com/codeigniter4/CodeIgniter4/pull/9015 +* refactor: fix `Throttler::check()` $tokens by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/9067 + ## [v4.5.3](https://github.com/codeigniter4/CodeIgniter4/tree/v4.5.3) (2024-06-25) [Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/v4.5.2...v4.5.3) diff --git a/admin/RELEASE.md b/admin/RELEASE.md index 8c5f463355fb..4f199a821177 100644 --- a/admin/RELEASE.md +++ b/admin/RELEASE.md @@ -6,16 +6,22 @@ > > -MGatner, kenjis -## Merge `develop` branch into next minor version branch `4.x` +## Notation + +- `4.x.x`: The new release version. (e.g., `4.5.3`) +- `4.y`: The next minor version. (e.g., `4.6`) +- `4.z`: The next next minor version. (e.g., `4.7`) + +## Merge `develop` branch into next minor version branch `4.y` Before starting release process, if there are commits in `develop` branch that -are not merged into `4.x` branch, merge them. This is because if conflicts occur, +are not merged into `4.y` branch, merge them. This is because if conflicts occur, merging will take time. ```console git fetch upstream -git switch 4.x -git merge upstream/4.x +git switch 4.y +git merge upstream/4.y git merge upstream/develop git push upstream HEAD ``` @@ -24,11 +30,10 @@ git push upstream HEAD If you release a new minor version. -* [ ] Create PR to merge `4.x` into `develop` and merge it +* [ ] Create PR to merge `4.y` into `develop` and merge it * [ ] Rename the current minor version (e.g., `4.5`) in Setting > Branches > "Branch protection rules" to the next minor version. E.g. `4.5` → `4.6` -* [ ] Delete the merged `4.x` branch (This closes all PRs to the branch) -* Do the regular release process. Go to the next "Changelog" section +* [ ] Delete the merged `4.y` branch (This closes all PRs to the branch) ## Changelog @@ -90,8 +95,8 @@ Work off direct clones of the repos so the release branches persist for a time. * [ ] Replace **CHANGELOG.md** with the new version generated above * [ ] Update **user_guide_src/source/changelogs/v4.x.x.rst** * Remove the section titles that have no items -* [ ] Update **user_guide_src/source/installation/upgrade_{ver}.rst** - * [ ] fill in the "All Changes" section, and add it to **upgrading.rst** +* [ ] Update **user_guide_src/source/installation/upgrade_4xx.rst** + * [ ] fill in the "All Changes" section, and add it to **upgrade_4xx.rst** * git diff --name-status origin/master -- . ':!system' ':!tests' ':!user_guide_src' * Note: `tests/` is not used for distribution repos. See `admin/starter/tests/` * [ ] Remove the section titles that have no items @@ -137,7 +142,7 @@ Work off direct clones of the repos so the release branches persist for a time. ## New Contributors * - **Full Changelog**: https://github.com/codeigniter4/CodeIgniter4/compare/v4.x.x...v4.x.x + **Full Changelog**: https://github.com/codeigniter4/CodeIgniter4/compare/v4.x.w...v4.x.x ``` Click the "Generate release notes" button, and get the "New Contributors". * [ ] Watch for the "Deploy Distributable Repos" action to make sure **framework**, @@ -164,19 +169,19 @@ Work off direct clones of the repos so the release branches persist for a time. git merge origin/master git push origin HEAD ``` -* [ ] Update the next minor version branch `4.x`: +* [ ] Update the next minor version branch `4.y`: ```console git fetch origin - git checkout 4.x - git merge origin/4.x + git checkout 4.y + git merge origin/4.y git merge origin/develop git push origin HEAD ``` -* [ ] [Minor version only] Create the new next minor version branch `4.x`: +* [ ] [Minor version only] Create the new next minor version branch `4.z`: ```console git fetch origin git switch develop - git switch -c 4.x + git switch -c 4.z git push origin HEAD ``` * [ ] Request CVEs and Publish any Security Advisories that were resolved from private forks diff --git a/admin/css/debug-toolbar/toolbar.scss b/admin/css/debug-toolbar/toolbar.scss index 97a92690b937..eec3d25546e3 100644 --- a/admin/css/debug-toolbar/toolbar.scss +++ b/admin/css/debug-toolbar/toolbar.scss @@ -95,6 +95,7 @@ } h2 { + font-weight: bold; font-size: $base-size; margin: 0; padding: 5px 0 10px 0; @@ -292,6 +293,8 @@ // The tabs container .tab { + height: fit-content; + text-align: left; bottom: 35px; display: none; left: 0; @@ -306,6 +309,8 @@ // The "Timeline" tab .timeline { + position: static; + display: table; margin-left: 0; width: 100%; diff --git a/admin/pre-commit b/admin/pre-commit index f64268277e72..19bec4f4726b 100644 --- a/admin/pre-commit +++ b/admin/pre-commit @@ -24,39 +24,10 @@ if [ "$FILES" != "" ]; then echo "Running PHP CS Fixer..." # Run on whole codebase to skip on unnecessary filtering - # Run first on app, admin, public - if [ -d /proc/cygdrive ]; then - ./vendor/bin/php-cs-fixer fix --verbose --dry-run --diff --config=.php-cs-fixer.no-header.php - else - php ./vendor/bin/php-cs-fixer fix --verbose --dry-run --diff --config=.php-cs-fixer.no-header.php - fi - - if [ $? != 0 ]; then - echo "Files in app, admin, or public are not following the coding standards. Please fix them before commit." - exit 1 - fi - - # Next, run on system, tests, utils, and root PHP files - if [ -d /proc/cygdrive ]; then - ./vendor/bin/php-cs-fixer fix --verbose --dry-run --diff - else - php ./vendor/bin/php-cs-fixer fix --verbose --dry-run --diff - fi - - if [ $? != 0 ]; then - echo "Files in system, tests, utils, or root are not following the coding standards. Please fix them before commit." - exit 1 - fi - - # Next, run on user_guide_src/source PHP files - if [ -d /proc/cygdrive ]; then - ./vendor/bin/php-cs-fixer fix --verbose --dry-run --diff --config=.php-cs-fixer.user-guide.php - else - php ./vendor/bin/php-cs-fixer fix --verbose --dry-run --diff --config=.php-cs-fixer.user-guide.php - fi + composer cs if [ $? != 0 ]; then - echo "Files in user_guide_src/source are not following the coding standards. Please fix them before commit." + echo "There are PHP files which are not following the coding standards. Please fix them before commit." exit 1 fi fi diff --git a/admin/starter/.github/workflows/phpunit.yml b/admin/starter/.github/workflows/phpunit.yml index f43435df9765..2be22ec16095 100644 --- a/admin/starter/.github/workflows/phpunit.yml +++ b/admin/starter/.github/workflows/phpunit.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest - if: "!contains(github.event.head_commit.message, '[ci skip]')" + if: (! contains(github.event.head_commit.message, '[ci skip]')) steps: - name: Checkout diff --git a/admin/starter/composer.json b/admin/starter/composer.json index b84684d82a4f..38a51e29fb64 100644 --- a/admin/starter/composer.json +++ b/admin/starter/composer.json @@ -21,7 +21,7 @@ "autoload": { "psr-4": { "App\\": "app/", - "Config\\": "app/Config" + "Config\\": "app/Config/" }, "exclude-from-classmap": [ "**/Database/Migrations/**" diff --git a/app/Config/Events.php b/app/Config/Events.php index 993abd24ebc7..62a7b86d46c8 100644 --- a/app/Config/Events.php +++ b/app/Config/Events.php @@ -23,7 +23,7 @@ * Events::on('create', [$myInstance, 'myMethod']); */ -Events::on('pre_system', static function () { +Events::on('pre_system', static function (): void { if (ENVIRONMENT !== 'testing') { if (ini_get('zlib.output_compression')) { throw FrameworkException::forEnabledZlibOutputCompression(); @@ -47,7 +47,7 @@ Services::toolbar()->respond(); // Hot Reload route - for framework use on the hot reloader. if (ENVIRONMENT === 'development') { - Services::routes()->get('__hot-reload', static function () { + Services::routes()->get('__hot-reload', static function (): void { (new HotReloader())->run(); }); } diff --git a/composer.json b/composer.json index 76020a1f6ea5..6915af74aa8f 100644 --- a/composer.json +++ b/composer.json @@ -17,23 +17,18 @@ "psr/log": "^3.0" }, "require-dev": { - "codeigniter/coding-standard": "^1.7", "codeigniter/phpstan-codeigniter": "^1.4", - "ergebnis/composer-normalize": "^2.28", "fakerphp/faker": "^1.9", - "friendsofphp/php-cs-fixer": "^3.47.1", "kint-php/kint": "^5.0.4", "mikey179/vfsstream": "^1.6", - "nexusphp/cs-config": "^3.6", "nexusphp/tachycardia": "^2.0", - "phpstan/extension-installer": "^1.3", + "phpstan/extension-installer": "^1.4", "phpstan/phpstan": "^1.11", "phpstan/phpstan-strict-rules": "^1.6", "phpunit/phpcov": "^9.0.2", "phpunit/phpunit": "^10.5.16", "predis/predis": "^1.1 || ^2.0", - "rector/rector": "1.1.1", - "vimeo/psalm": "^5.0" + "rector/rector": "1.2.2" }, "replace": { "codeigniter4/framework": "self.version" @@ -72,12 +67,11 @@ "autoload-dev": { "psr-4": { "CodeIgniter\\": "tests/system/", - "Utils\\": "utils/" + "Utils\\": "utils/src/" } }, "config": { "allow-plugins": { - "ergebnis/composer-normalize": true, "phpstan/extension-installer": true }, "optimize-autoloader": true, @@ -91,29 +85,33 @@ }, "scripts": { "post-update-cmd": [ - "CodeIgniter\\ComposerScripts::postUpdate", - "composer update --working-dir=tools/phpmetrics" + "CodeIgniter\\ComposerScripts::postUpdate" + ], + "post-autoload-dump": [ + "@composer update --working-dir=utils" ], "analyze": [ "Composer\\Config::disableProcessTimeout", - "bash -c \"XDEBUG_MODE=off phpstan analyse\"", - "rector process --dry-run" + "@phpstan:check", + "vendor/bin/rector process --dry-run" ], "cs": [ "Composer\\Config::disableProcessTimeout", - "php-cs-fixer fix --ansi --verbose --dry-run --diff --config=.php-cs-fixer.user-guide.php", - "php-cs-fixer fix --ansi --verbose --dry-run --diff --config=.php-cs-fixer.no-header.php", - "php-cs-fixer fix --ansi --verbose --dry-run --diff --config=.php-cs-fixer.tests.php", - "php-cs-fixer fix --ansi --verbose --dry-run --diff" + "utils/vendor/bin/php-cs-fixer check --ansi --verbose --diff --config=.php-cs-fixer.user-guide.php", + "utils/vendor/bin/php-cs-fixer check --ansi --verbose --diff --config=.php-cs-fixer.no-header.php", + "utils/vendor/bin/php-cs-fixer check --ansi --verbose --diff --config=.php-cs-fixer.tests.php", + "utils/vendor/bin/php-cs-fixer check --ansi --verbose --diff" ], "cs-fix": [ "Composer\\Config::disableProcessTimeout", - "php-cs-fixer fix --ansi --verbose --diff --config=.php-cs-fixer.user-guide.php", - "php-cs-fixer fix --ansi --verbose --diff --config=.php-cs-fixer.no-header.php", - "php-cs-fixer fix --ansi --verbose --diff --config=.php-cs-fixer.tests.php", - "php-cs-fixer fix --ansi --verbose --diff" + "utils/vendor/bin/php-cs-fixer fix --ansi --verbose --diff --config=.php-cs-fixer.user-guide.php", + "utils/vendor/bin/php-cs-fixer fix --ansi --verbose --diff --config=.php-cs-fixer.no-header.php", + "utils/vendor/bin/php-cs-fixer fix --ansi --verbose --diff --config=.php-cs-fixer.tests.php", + "utils/vendor/bin/php-cs-fixer fix --ansi --verbose --diff" ], - "metrics": "tools/phpmetrics/vendor/bin/phpmetrics --config=phpmetrics.json", + "metrics": "utils/vendor/bin/phpmetrics --config=phpmetrics.json", + "phpstan:baseline": "vendor/bin/phpstan analyse --ansi --generate-baseline=phpstan-baseline.php", + "phpstan:check": "vendor/bin/phpstan analyse --verbose --ansi", "sa": "@analyze", "style": "@cs-fix", "test": "phpunit" @@ -123,6 +121,8 @@ "cs": "Check the coding style", "cs-fix": "Fix the coding style", "metrics": "Run PhpMetrics", + "phpstan:baseline": "Run PHPStan then dump all errors to baseline", + "phpstan:check": "Run PHPStan with support for identifiers", "test": "Run unit tests" } } diff --git a/phpdoc.dist.xml b/phpdoc.dist.xml index 5e5a31cb483a..e67b1b29ed55 100644 --- a/phpdoc.dist.xml +++ b/phpdoc.dist.xml @@ -10,7 +10,7 @@ api/build/ api/cache/ - + system diff --git a/phpstan-baseline.php b/phpstan-baseline.php index 5a39d840a54d..1b36c4a8f8ee 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -1819,12 +1819,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/Database/BaseBuilder.php', ]; -$ignoreErrors[] = [ - // identifier: missingType.iterableValue - 'message' => '#^Method CodeIgniter\\\\Database\\\\BaseBuilder\\:\\:select\\(\\) has parameter \\$select with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Database/BaseBuilder.php', -]; $ignoreErrors[] = [ // identifier: missingType.iterableValue 'message' => '#^Method CodeIgniter\\\\Database\\\\BaseBuilder\\:\\:set\\(\\) has parameter \\$key with no value type specified in iterable type array\\.$#', @@ -1993,12 +1987,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/Database/BaseBuilder.php', ]; -$ignoreErrors[] = [ - // identifier: missingType.iterableValue - 'message' => '#^Property CodeIgniter\\\\Database\\\\BaseBuilder\\:\\:\\$QBNoEscape type has no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Database/BaseBuilder.php', -]; $ignoreErrors[] = [ // identifier: missingType.iterableValue 'message' => '#^Property CodeIgniter\\\\Database\\\\BaseBuilder\\:\\:\\$QBOptions type has no value type specified in iterable type array\\.$#', @@ -7465,12 +7453,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/Helpers/test_helper.php', ]; -$ignoreErrors[] = [ - // identifier: empty.notAllowed - 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Helpers/text_helper.php', -]; $ignoreErrors[] = [ // identifier: missingType.iterableValue 'message' => '#^Function strip_slashes\\(\\) has parameter \\$str with no value type specified in iterable type array\\.$#', @@ -18811,11 +18793,5 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/View/ViewTest.php', ]; -$ignoreErrors[] = [ - // identifier: method.childParameterType - 'message' => '#^Parameter \\#1 \\$node \\(PhpParser\\\\Node\\\\Stmt\\) of method Utils\\\\PHPStan\\\\CheckUseStatementsAfterLicenseRule\\:\\:processNode\\(\\) should be contravariant with parameter \\$node \\(PhpParser\\\\Node\\) of method PHPStan\\\\Rules\\\\Rule\\\\:\\:processNode\\(\\)$#', - 'count' => 1, - 'path' => __DIR__ . '/utils/PHPStan/CheckUseStatementsAfterLicenseRule.php', -]; return ['parameters' => ['ignoreErrors' => $ignoreErrors]]; diff --git a/phpstan.neon.dist b/phpstan.neon.dist index eccd0f88db75..c67aa93fcb04 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -18,7 +18,7 @@ parameters: - app - system - tests - - utils/PHPStan + - utils/src/PHPStan excludePaths: - app/Views/errors/cli/* - app/Views/errors/html/* diff --git a/rector.php b/rector.php index 1a12e38f3223..d8ab56f3cc8e 100644 --- a/rector.php +++ b/rector.php @@ -42,66 +42,61 @@ use Rector\EarlyReturn\Rector\Return_\PreparedValueToEarlyReturnRector; use Rector\Php55\Rector\String_\StringClassNameToClassConstantRector; use Rector\Php70\Rector\FuncCall\RandomFunctionRector; -use Rector\Php80\Rector\Class_\AnnotationToAttributeRector; use Rector\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector; use Rector\Php80\Rector\FunctionLike\MixedTypeRector; use Rector\Php81\Rector\FuncCall\NullToStrictStringFuncCallArgRector; -use Rector\PHPUnit\AnnotationsToAttributes\Rector\Class_\AnnotationWithValueToAttributeRector; -use Rector\PHPUnit\AnnotationsToAttributes\Rector\Class_\CoversAnnotationWithValueToAttributeRector; -use Rector\PHPUnit\AnnotationsToAttributes\Rector\ClassMethod\DataProviderAnnotationToAttributeRector; -use Rector\PHPUnit\AnnotationsToAttributes\Rector\ClassMethod\DependsAnnotationWithValueToAttributeRector; use Rector\PHPUnit\CodeQuality\Rector\Class_\YieldDataProviderRector; use Rector\PHPUnit\Set\PHPUnitSetList; use Rector\Privatization\Rector\Property\PrivatizeFinalClassPropertyRector; -use Rector\Set\ValueObject\LevelSetList; -use Rector\Set\ValueObject\SetList; use Rector\Strict\Rector\Empty_\DisallowedEmptyRuleFixerRector; use Rector\Strict\Rector\If_\BooleanInIfConditionRuleFixerRector; use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector; +use Rector\TypeDeclaration\Rector\Closure\AddClosureVoidReturnTypeWhereNoReturnRector; use Rector\TypeDeclaration\Rector\Empty_\EmptyOnNullableObjectToInstanceOfRector; +use Rector\TypeDeclaration\Rector\Function_\AddFunctionVoidReturnTypeWhereNoReturnRector; use Rector\TypeDeclaration\Rector\StmtsAwareInterface\DeclareStrictTypesRector; use Utils\Rector\PassStrictParameterToFunctionParameterRector; use Utils\Rector\RemoveErrorSuppressInTryCatchStmtsRector; use Utils\Rector\UnderscoreToCamelCaseVariableNameRector; -return static function (RectorConfig $rectorConfig): void { - $rectorConfig->sets([ - SetList::DEAD_CODE, - LevelSetList::UP_TO_PHP_81, +return RectorConfig::configure() + ->withPhpSets(php81: true) + ->withPreparedSets(deadCode: true) + ->withSets([ PHPUnitSetList::PHPUNIT_CODE_QUALITY, PHPUnitSetList::PHPUNIT_100, - ]); - - $rectorConfig->parallel(120, 8, 10); - - // Github action cache - $rectorConfig->cacheClass(FileCacheStorage::class); - if (is_dir('/tmp')) { - $rectorConfig->cacheDirectory('/tmp/rector'); - } - + ]) + ->withParallel(120, 8, 10) + ->withCache( + // Github action cache or local + is_dir('/tmp') ? '/tmp/rector' : null, + FileCacheStorage::class + ) // paths to refactor; solid alternative to CLI arguments - $rectorConfig->paths([__DIR__ . '/app', __DIR__ . '/system', __DIR__ . '/tests', __DIR__ . '/utils']); - + ->withPaths([ + __DIR__ . '/app', + __DIR__ . '/system', + __DIR__ . '/tests', + __DIR__ . '/utils/src', + ]) // do you need to include constants, class aliases or custom autoloader? files listed will be executed - $rectorConfig->bootstrapFiles([ + ->withBootstrapFiles([ __DIR__ . '/system/Test/bootstrap.php', - ]); - - $rectorConfig->phpstanConfigs([ + ]) + ->withPHPStanConfigs([ __DIR__ . '/phpstan.neon.dist', __DIR__ . '/vendor/codeigniter/phpstan-codeigniter/extension.neon', __DIR__ . '/vendor/phpstan/phpstan-strict-rules/rules.neon', - ]); - + ]) // is there a file you need to skip? - $rectorConfig->skip([ + ->withSkip([ __DIR__ . '/system/Debug/Toolbar/Views/toolbar.tpl.php', __DIR__ . '/system/ThirdParty', __DIR__ . '/tests/system/Config/fixtures', __DIR__ . '/tests/system/Filters/fixtures', __DIR__ . '/tests/_support/Commands/Foobar.php', __DIR__ . '/tests/_support/View', + __DIR__ . '/tests/system/View/Views', YieldDataProviderRector::class, @@ -180,55 +175,46 @@ // Unnecessary (string) is inserted NullToStrictStringFuncCallArgRector::class, - - // PHPUnit 10 (requires PHP 8.1) features - DataProviderAnnotationToAttributeRector::class, - DependsAnnotationWithValueToAttributeRector::class, - AnnotationWithValueToAttributeRector::class, - AnnotationToAttributeRector::class, - CoversAnnotationWithValueToAttributeRector::class, - ]); - + ]) // auto import fully qualified class names - $rectorConfig->importNames(); - $rectorConfig->removeUnusedImports(); - - $rectorConfig->rule(DeclareStrictTypesRector::class); - $rectorConfig->rule(UnderscoreToCamelCaseVariableNameRector::class); - $rectorConfig->rule(SimplifyUselessVariableRector::class); - $rectorConfig->rule(RemoveAlwaysElseRector::class); - $rectorConfig->rule(PassStrictParameterToFunctionParameterRector::class); - $rectorConfig->rule(CountArrayToEmptyArrayComparisonRector::class); - $rectorConfig->rule(ChangeNestedForeachIfsToEarlyContinueRector::class); - $rectorConfig->rule(ChangeIfElseValueAssignToEarlyReturnRector::class); - $rectorConfig->rule(SimplifyStrposLowerRector::class); - $rectorConfig->rule(CombineIfRector::class); - $rectorConfig->rule(SimplifyIfReturnBoolRector::class); - $rectorConfig->rule(InlineIfToExplicitIfRector::class); - $rectorConfig->rule(PreparedValueToEarlyReturnRector::class); - $rectorConfig->rule(ShortenElseIfRector::class); - $rectorConfig->rule(SimplifyIfElseToTernaryRector::class); - $rectorConfig->rule(UnusedForeachValueToArrayKeysRector::class); - $rectorConfig->rule(ChangeArrayPushToArrayAssignRector::class); - $rectorConfig->rule(UnnecessaryTernaryExpressionRector::class); - $rectorConfig->rule(RemoveErrorSuppressInTryCatchStmtsRector::class); - $rectorConfig->rule(FuncGetArgsToVariadicParamRector::class); - $rectorConfig->rule(MakeInheritedMethodVisibilitySameAsParentRector::class); - $rectorConfig->rule(SimplifyEmptyArrayCheckRector::class); - $rectorConfig->rule(SimplifyEmptyCheckOnEmptyArrayRector::class); - $rectorConfig->rule(TernaryEmptyArrayArrayDimFetchToCoalesceRector::class); - $rectorConfig->rule(EmptyOnNullableObjectToInstanceOfRector::class); - $rectorConfig->rule(DisallowedEmptyRuleFixerRector::class); - $rectorConfig->rule(PrivatizeFinalClassPropertyRector::class); - $rectorConfig->rule(CompleteDynamicPropertiesRector::class); - $rectorConfig->rule(BooleanInIfConditionRuleFixerRector::class); - $rectorConfig->rule(SingleInArrayToCompareRector::class); - $rectorConfig->rule(VersionCompareFuncCallToConstantRector::class); - $rectorConfig->rule(ExplicitBoolCompareRector::class); - - $rectorConfig - ->ruleWithConfiguration(StringClassNameToClassConstantRector::class, [ - // keep '\\' prefix string on string '\Foo\Bar' - StringClassNameToClassConstantRector::SHOULD_KEEP_PRE_SLASH => true, - ]); -}; + ->withImportNames(removeUnusedImports: true) + ->withRules([ + DeclareStrictTypesRector::class, + UnderscoreToCamelCaseVariableNameRector::class, + SimplifyUselessVariableRector::class, + RemoveAlwaysElseRector::class, + PassStrictParameterToFunctionParameterRector::class, + CountArrayToEmptyArrayComparisonRector::class, + ChangeNestedForeachIfsToEarlyContinueRector::class, + ChangeIfElseValueAssignToEarlyReturnRector::class, + SimplifyStrposLowerRector::class, + CombineIfRector::class, + SimplifyIfReturnBoolRector::class, + InlineIfToExplicitIfRector::class, + PreparedValueToEarlyReturnRector::class, + ShortenElseIfRector::class, + SimplifyIfElseToTernaryRector::class, + UnusedForeachValueToArrayKeysRector::class, + ChangeArrayPushToArrayAssignRector::class, + UnnecessaryTernaryExpressionRector::class, + RemoveErrorSuppressInTryCatchStmtsRector::class, + FuncGetArgsToVariadicParamRector::class, + MakeInheritedMethodVisibilitySameAsParentRector::class, + SimplifyEmptyArrayCheckRector::class, + SimplifyEmptyCheckOnEmptyArrayRector::class, + TernaryEmptyArrayArrayDimFetchToCoalesceRector::class, + EmptyOnNullableObjectToInstanceOfRector::class, + DisallowedEmptyRuleFixerRector::class, + PrivatizeFinalClassPropertyRector::class, + CompleteDynamicPropertiesRector::class, + BooleanInIfConditionRuleFixerRector::class, + SingleInArrayToCompareRector::class, + VersionCompareFuncCallToConstantRector::class, + ExplicitBoolCompareRector::class, + AddClosureVoidReturnTypeWhereNoReturnRector::class, + AddFunctionVoidReturnTypeWhereNoReturnRector::class, + ]) + ->withConfiguredRule(StringClassNameToClassConstantRector::class, [ + // keep '\\' prefix string on string '\Foo\Bar' + StringClassNameToClassConstantRector::SHOULD_KEEP_PRE_SLASH => true, + ]); diff --git a/system/Autoloader/Autoloader.php b/system/Autoloader/Autoloader.php index 78d31eae0d7f..28c183f12552 100644 --- a/system/Autoloader/Autoloader.php +++ b/system/Autoloader/Autoloader.php @@ -507,7 +507,7 @@ private function autoloadKint(): void { // If we have KINT_DIR it means it's already loaded via composer if (! defined('KINT_DIR')) { - spl_autoload_register(function ($class) { + spl_autoload_register(function ($class): void { $class = explode('\\', $class); if (array_shift($class) !== 'Kint') { diff --git a/system/CLI/BaseCommand.php b/system/CLI/BaseCommand.php index 86ba504feaa6..afeb80eb5267 100644 --- a/system/CLI/BaseCommand.php +++ b/system/CLI/BaseCommand.php @@ -163,7 +163,7 @@ public function showHelp() if ($this->arguments !== []) { CLI::newLine(); CLI::write(lang('CLI.helpArguments'), 'yellow'); - $length = max(array_map('strlen', array_keys($this->arguments))); + $length = max(array_map(strlen(...), array_keys($this->arguments))); foreach ($this->arguments as $argument => $description) { CLI::write(CLI::color($this->setPad($argument, $length, 2, 2), 'green') . $description); @@ -173,7 +173,7 @@ public function showHelp() if ($this->options !== []) { CLI::newLine(); CLI::write(lang('CLI.helpOptions'), 'yellow'); - $length = max(array_map('strlen', array_keys($this->options))); + $length = max(array_map(strlen(...), array_keys($this->options))); foreach ($this->options as $option => $description) { CLI::write(CLI::color($this->setPad($option, $length, 2, 2), 'green') . $description); diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index 7badf1807dcd..bfe1511eed02 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -393,7 +393,7 @@ private static function isZeroOptions(array $options): void private static function printKeysAndValues(array $options): void { // +2 for the square brackets around the key - $keyMaxLength = max(array_map('mb_strwidth', array_keys($options))) + 2; + $keyMaxLength = max(array_map(mb_strwidth(...), array_keys($options))) + 2; foreach ($options as $key => $description) { $name = str_pad(' [' . $key . '] ', $keyMaxLength + 4, ' '); @@ -857,7 +857,7 @@ public static function wrap(?string $string = null, int $max = 0, int $padLeft = $first = true; - array_walk($lines, static function (&$line) use ($padLeft, &$first) { + array_walk($lines, static function (&$line) use ($padLeft, &$first): void { if (! $first) { $line = str_repeat(' ', $padLeft) . $line; } else { diff --git a/system/CLI/GeneratorTrait.php b/system/CLI/GeneratorTrait.php index 065197be5090..6a061a36b8b3 100644 --- a/system/CLI/GeneratorTrait.php +++ b/system/CLI/GeneratorTrait.php @@ -325,7 +325,7 @@ private function normalizeInputClassName(): string implode( '\\', array_map( - 'pascalize', + pascalize(...), explode('\\', str_replace('/', '\\', trim($class))) ) ), diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 95feae8160a1..bcfb7bd67976 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -56,7 +56,7 @@ class CodeIgniter /** * The current version of CodeIgniter Framework */ - public const CI_VERSION = '4.5.3'; + public const CI_VERSION = '4.5.4'; /** * App startup time. @@ -253,7 +253,7 @@ private function autoloadKint(): void { // If we have KINT_DIR it means it's already loaded via composer if (! defined('KINT_DIR')) { - spl_autoload_register(function ($class) { + spl_autoload_register(function ($class): void { $class = explode('\\', $class); if (array_shift($class) !== 'Kint') { diff --git a/system/Commands/ListCommands.php b/system/Commands/ListCommands.php index 87c3e96b824a..3411b103430d 100644 --- a/system/Commands/ListCommands.php +++ b/system/Commands/ListCommands.php @@ -101,7 +101,7 @@ protected function listFull(array $commands) $groups[$command['group']][$title] = $command; } - $length = max(array_map('strlen', array_keys($commands))); + $length = max(array_map(strlen(...), array_keys($commands))); ksort($groups); diff --git a/system/Commands/Utilities/Routes.php b/system/Commands/Utilities/Routes.php index f9575c063973..9adcc1bc39e7 100644 --- a/system/Commands/Utilities/Routes.php +++ b/system/Commands/Utilities/Routes.php @@ -119,8 +119,8 @@ public function run(array $params) $route['route'], $routeName, $route['handler'], - implode(' ', array_map('class_basename', $filters['before'])), - implode(' ', array_map('class_basename', $filters['after'])), + implode(' ', array_map(class_basename(...), $filters['before'])), + implode(' ', array_map(class_basename(...), $filters['after'])), ]; } @@ -166,8 +166,8 @@ public function run(array $params) // There is no `AUTO` method, but it is intentional not to get route filters. $filters = $filterCollector->get('AUTO', $uriGenerator->get($routes[1])); - $routes[] = implode(' ', array_map('class_basename', $filters['before'])); - $routes[] = implode(' ', array_map('class_basename', $filters['after'])); + $routes[] = implode(' ', array_map(class_basename(...), $filters['before'])); + $routes[] = implode(' ', array_map(class_basename(...), $filters['after'])); } } diff --git a/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php b/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php index 0d426a3b1716..579969346637 100644 --- a/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php +++ b/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php @@ -125,8 +125,8 @@ private function addFilters($routes) $filters['before'] = array_intersect($filtersLongest['before'], $filtersShortest['before']); $filters['after'] = array_intersect($filtersLongest['after'], $filtersShortest['after']); - $route['before'] = implode(' ', array_map('class_basename', $filters['before'])); - $route['after'] = implode(' ', array_map('class_basename', $filters['after'])); + $route['before'] = implode(' ', array_map(class_basename(...), $filters['before'])); + $route['after'] = implode(' ', array_map(class_basename(...), $filters['after'])); } return $routes; diff --git a/system/Common.php b/system/Common.php index f96e9f100f00..49b3d2896753 100644 --- a/system/Common.php +++ b/system/Common.php @@ -766,10 +766,8 @@ function lang(string $line, array $args = [], ?string $locale = null) * - notice * - info * - debug - * - * @return void */ - function log_message(string $level, string $message, array $context = []) + function log_message(string $level, string $message, array $context = []): void { // When running tests, we want to always ensure that the // TestLogger is running, which provides utilities for diff --git a/system/Config/BaseService.php b/system/Config/BaseService.php index 5bd55403beba..687044be0713 100644 --- a/system/Config/BaseService.php +++ b/system/Config/BaseService.php @@ -75,6 +75,7 @@ use Config\Optimize; use Config\Pager as ConfigPager; use Config\Services as AppServices; +use Config\Session as ConfigSession; use Config\Toolbar as ConfigToolbar; use Config\Validation as ConfigValidation; use Config\View as ConfigView; @@ -130,7 +131,7 @@ * @method static Router router(RouteCollectionInterface $routes = null, Request $request = null, $getShared = true) * @method static RouteCollection routes($getShared = true) * @method static Security security(App $config = null, $getShared = true) - * @method static Session session(App $config = null, $getShared = true) + * @method static Session session(ConfigSession $config = null, $getShared = true) * @method static SiteURIFactory siteurifactory(App $config = null, Superglobals $superglobals = null, $getShared = true) * @method static Superglobals superglobals(array $server = null, array $get = null, bool $getShared = true) * @method static Throttler throttler($getShared = true) diff --git a/system/Config/Services.php b/system/Config/Services.php index 422799e0b87c..e37b1278e9a7 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -51,6 +51,7 @@ use CodeIgniter\Router\RouteCollectionInterface; use CodeIgniter\Router\Router; use CodeIgniter\Security\Security; +use CodeIgniter\Session\Handlers\BaseHandler as SessionBaseHandler; use CodeIgniter\Session\Handlers\Database\MySQLiHandler; use CodeIgniter\Session\Handlers\Database\PostgreHandler; use CodeIgniter\Session\Handlers\DatabaseHandler; @@ -88,6 +89,7 @@ use Config\Toolbar as ToolbarConfig; use Config\Validation as ValidationConfig; use Config\View as ViewConfig; +use InvalidArgumentException; use Locale; /** @@ -674,17 +676,24 @@ public static function session(?SessionConfig $config = null, bool $getShared = if ($driverName === DatabaseHandler::class) { $DBGroup = $config->DBGroup ?? config(Database::class)->defaultGroup; - $db = Database::connect($DBGroup); - $driver = $db->getPlatform(); + $driverPlatform = Database::connect($DBGroup)->getPlatform(); - if ($driver === 'MySQLi') { + if ($driverPlatform === 'MySQLi') { $driverName = MySQLiHandler::class; - } elseif ($driver === 'Postgre') { + } elseif ($driverPlatform === 'Postgre') { $driverName = PostgreHandler::class; } } + if (! class_exists($driverName) || ! is_a($driverName, SessionBaseHandler::class, true)) { + throw new InvalidArgumentException(sprintf( + 'Invalid session handler "%s" provided.', + $driverName + )); + } + + /** @var SessionBaseHandler $driver */ $driver = new $driverName($config, AppServices::get('request')->getIPAddress()); $driver->setLogger($logger); diff --git a/system/Cookie/Cookie.php b/system/Cookie/Cookie.php index 78dc44258eb9..01b10db19b65 100644 --- a/system/Cookie/Cookie.php +++ b/system/Cookie/Cookie.php @@ -283,7 +283,7 @@ public function getPrefixedName(): string $name .= $this->getName(); } else { $search = str_split(self::$reservedCharsList); - $replace = array_map('rawurlencode', $search); + $replace = array_map(rawurlencode(...), $search); $name .= str_replace($search, $replace, $this->getName()); } diff --git a/system/DataCaster/DataCaster.php b/system/DataCaster/DataCaster.php index 29de3987faa9..5a6786a03331 100644 --- a/system/DataCaster/DataCaster.php +++ b/system/DataCaster/DataCaster.php @@ -156,7 +156,7 @@ public function castAs(mixed $value, string $field, string $method = 'get'): mix // type[param, param2,param3] if (preg_match('/\A(.+)\[(.+)\]\z/', $type, $matches)) { $type = $matches[1]; - $params = array_map('trim', explode(',', $matches[2])); + $params = array_map(trim(...), explode(',', $matches[2])); } if ($isNullable && ! $this->strict) { diff --git a/system/DataConverter/DataConverter.php b/system/DataConverter/DataConverter.php index b79ede5b08ff..43b42a08bbdf 100644 --- a/system/DataConverter/DataConverter.php +++ b/system/DataConverter/DataConverter.php @@ -140,7 +140,7 @@ public function reconstruct(string $classname, array $row): object return $classObj; } - $classSet = Closure::bind(function ($key, $value) { + $classSet = Closure::bind(function ($key, $value): void { $this->{$key} = $value; }, $classObj, $classname); diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index 4e6e422b2a6f..649a201112e4 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -124,9 +124,9 @@ class BaseBuilder protected array $QBUnion = []; /** - * QB NO ESCAPE data + * Whether to protect identifiers in SELECT * - * @var array + * @var list true=protect, false=not protect */ public $QBNoEscape = []; @@ -390,7 +390,8 @@ public function ignore(bool $ignore = true) /** * Generates the SELECT portion of the query * - * @param array|RawSql|string $select + * @param list|RawSql|string $select + * @param bool|null $escape Whether to protect identifiers * * @return $this */ @@ -402,16 +403,21 @@ public function select($select = '*', ?bool $escape = null) } if ($select instanceof RawSql) { - $this->QBSelect[] = $select; - - return $this; + $select = [$select]; } if (is_string($select)) { - $select = $escape === false ? [$select] : explode(',', $select); + $select = ($escape === false) ? [$select] : explode(',', $select); } foreach ($select as $val) { + if ($val instanceof RawSql) { + $this->QBSelect[] = $val; + $this->QBNoEscape[] = false; + + continue; + } + $val = trim($val); if ($val !== '') { @@ -424,8 +430,10 @@ public function select($select = '*', ?bool $escape = null) * This prevents NULL being escaped * @see https://github.com/codeigniter4/CodeIgniter4/issues/1169 */ - if (mb_stripos(trim($val), 'NULL') === 0) { - $escape = false; + if (mb_stripos($val, 'NULL') === 0) { + $this->QBNoEscape[] = false; + + continue; } $this->QBNoEscape[] = $escape; @@ -3054,15 +3062,17 @@ protected function compileSelect($selectOverride = false): string if (empty($this->QBSelect)) { $sql .= '*'; - } elseif ($this->QBSelect[0] instanceof RawSql) { - $sql .= (string) $this->QBSelect[0]; } else { // Cycle through the "select" portion of the query and prep each column name. // The reason we protect identifiers here rather than in the select() function // is because until the user calls the from() function we don't know if there are aliases foreach ($this->QBSelect as $key => $val) { - $noEscape = $this->QBNoEscape[$key] ?? null; - $this->QBSelect[$key] = $this->db->protectIdentifiers($val, false, $noEscape); + if ($val instanceof RawSql) { + $this->QBSelect[$key] = (string) $val; + } else { + $protect = $this->QBNoEscape[$key] ?? null; + $this->QBSelect[$key] = $this->db->protectIdentifiers($val, false, $protect); + } } $sql .= implode(', ', $this->QBSelect); diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index 6581dc7c5ac0..dce399bdefb5 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -1519,7 +1519,7 @@ public function tableExists(string $tableName, bool $cached = true): bool if (! empty($this->dataCache['table_names'])) { $key = array_search( strtolower($tableName), - array_map('strtolower', $this->dataCache['table_names']), + array_map(strtolower(...), $this->dataCache['table_names']), true ); diff --git a/system/Database/Forge.php b/system/Database/Forge.php index 797b685e861c..595cd6156b5f 100644 --- a/system/Database/Forge.php +++ b/system/Database/Forge.php @@ -306,7 +306,7 @@ public function dropDatabase(string $dbName): bool if (! empty($this->db->dataCache['db_names'])) { $key = array_search( strtolower($dbName), - array_map('strtolower', $this->db->dataCache['db_names']), + array_map(strtolower(...), $this->db->dataCache['db_names']), true ); if ($key !== false) { @@ -667,7 +667,7 @@ public function dropTable(string $tableName, bool $ifExists = false, bool $casca if ($query && ! empty($this->db->dataCache['table_names'])) { $key = array_search( strtolower($this->db->DBPrefix . $tableName), - array_map('strtolower', $this->db->dataCache['table_names']), + array_map(strtolower(...), $this->db->dataCache['table_names']), true ); @@ -729,7 +729,7 @@ public function renameTable(string $tableName, string $newTableName) if ($result && ! empty($this->db->dataCache['table_names'])) { $key = array_search( strtolower($this->db->DBPrefix . $tableName), - array_map('strtolower', $this->db->dataCache['table_names']), + array_map(strtolower(...), $this->db->dataCache['table_names']), true ); diff --git a/system/Database/MigrationRunner.php b/system/Database/MigrationRunner.php index 719d45c15592..055b8ba6eaf3 100644 --- a/system/Database/MigrationRunner.php +++ b/system/Database/MigrationRunner.php @@ -686,7 +686,7 @@ public function getBatches(): array ->get() ->getResultArray(); - return array_map('intval', array_column($batches, 'batch')); + return array_map(intval(...), array_column($batches, 'batch')); } /** diff --git a/system/Database/OCI8/Connection.php b/system/Database/OCI8/Connection.php index f7983a695c0c..2b870b755b44 100644 --- a/system/Database/OCI8/Connection.php +++ b/system/Database/OCI8/Connection.php @@ -53,10 +53,24 @@ class Connection extends BaseConnection ]; protected $validDSNs = [ - 'tns' => '/^\(DESCRIPTION=(\(.+\)){2,}\)$/', // TNS - // Easy Connect string (Oracle 10g+) - 'ec' => '/^(\/\/)?[a-z0-9.:_-]+(:[1-9][0-9]{0,4})?(\/[a-z0-9$_]+)?(:[^\/])?(\/[a-z0-9$_]+)?$/i', - 'in' => '/^[a-z0-9$_]+$/i', // Instance name (defined in tnsnames.ora) + // TNS + 'tns' => '/^\(DESCRIPTION=(\(.+\)){2,}\)$/', + // Easy Connect string (Oracle 10g+). + // https://docs.oracle.com/en/database/oracle/oracle-database/23/netag/configuring-naming-methods.html#GUID-36F3A17D-843C-490A-8A23-FB0FE005F8E8 + // [//]host[:port][/[service_name][:server_type][/instance_name]] + 'ec' => '/^ + (\/\/)? + (\[)?[a-z0-9.:_-]+(\])? # Host or IP address + (:[1-9][0-9]{0,4})? # Port + ( + (\/) + ([a-z0-9.$_]+)? # Service name + (:[a-z]+)? # Server type + (\/[a-z0-9$_]+)? # Instance name + )? + $/ix', + // Instance name (defined in tnsnames.ora) + 'in' => '/^[a-z0-9$_]+$/i', ]; /** diff --git a/system/Database/OCI8/Forge.php b/system/Database/OCI8/Forge.php index 69a42ed1dbeb..5a10363be769 100644 --- a/system/Database/OCI8/Forge.php +++ b/system/Database/OCI8/Forge.php @@ -202,7 +202,7 @@ protected function _processColumn(array $processedField): string $constraint = ' CHECK(' . $this->db->escapeIdentifiers($processedField['name']) . ' IN ' . $processedField['length'] . ')'; - $processedField['length'] = '(' . max(array_map('mb_strlen', explode("','", mb_substr($processedField['length'], 2, -2)))) . ')' . $constraint; + $processedField['length'] = '(' . max(array_map(mb_strlen(...), explode("','", mb_substr($processedField['length'], 2, -2)))) . ')' . $constraint; } elseif (isset($this->primaryKeys['fields']) && count($this->primaryKeys['fields']) === 1 && $processedField['name'] === $this->primaryKeys['fields'][0]) { $processedField['unique'] = ''; } diff --git a/system/Database/Postgre/Builder.php b/system/Database/Postgre/Builder.php index 857760930275..1071dfedf06b 100644 --- a/system/Database/Postgre/Builder.php +++ b/system/Database/Postgre/Builder.php @@ -159,7 +159,7 @@ public function replace(?array $set = null) $table = $this->QBFrom[0]; $set = $this->binds; - array_walk($set, static function (array &$item) { + array_walk($set, static function (array &$item): void { $item = $item[0]; }); diff --git a/system/Database/Postgre/Connection.php b/system/Database/Postgre/Connection.php index d3af2380156e..ec886dbd4d8b 100644 --- a/system/Database/Postgre/Connection.php +++ b/system/Database/Postgre/Connection.php @@ -58,8 +58,7 @@ class Connection extends BaseConnection /** * Connect to the database. * - * @return false|resource - * @phpstan-return false|PgSqlConnection + * @return false|PgSqlConnection */ public function connect(bool $persistent = false) { @@ -197,8 +196,7 @@ public function getVersion(): string /** * Executes the query against the database. * - * @return false|resource - * @phpstan-return false|PgSqlResult + * @return false|PgSqlResult */ protected function execute(string $sql) { diff --git a/system/Database/Postgre/PreparedQuery.php b/system/Database/Postgre/PreparedQuery.php index 832108675ab9..fbea6ac14f3a 100644 --- a/system/Database/Postgre/PreparedQuery.php +++ b/system/Database/Postgre/PreparedQuery.php @@ -95,8 +95,7 @@ public function _execute(array $data): bool /** * Returns the result object for the prepared query or false on failure. * - * @return resource|null - * @phpstan-return PgSqlResult|null + * @return PgSqlResult|null */ public function _getResult() { diff --git a/system/Database/Query.php b/system/Database/Query.php index 11085a91f6a0..ba8e9066063f 100644 --- a/system/Database/Query.php +++ b/system/Database/Query.php @@ -118,7 +118,7 @@ public function setQuery(string $sql, $binds = null, bool $setEscape = true) } if ($setEscape) { - array_walk($binds, static function (&$item) { + array_walk($binds, static function (&$item): void { $item = [ $item, true, @@ -141,7 +141,7 @@ public function setQuery(string $sql, $binds = null, bool $setEscape = true) public function setBinds(array $binds, bool $setEscape = true) { if ($setEscape) { - array_walk($binds, static function (&$item) { + array_walk($binds, static function (&$item): void { $item = [$item, true]; }); } diff --git a/system/Database/SQLSRV/Builder.php b/system/Database/SQLSRV/Builder.php index e2301f7ee2ab..a6d0b8e85ccd 100644 --- a/system/Database/SQLSRV/Builder.php +++ b/system/Database/SQLSRV/Builder.php @@ -420,7 +420,7 @@ protected function _replace(string $table, array $keys, array $values): string // Get the binds $binds = $this->binds; - array_walk($binds, static function (&$item) { + array_walk($binds, static function (&$item): void { $item = $item[0]; }); diff --git a/system/Database/SQLite3/Forge.php b/system/Database/SQLite3/Forge.php index 32e447829c29..48e9abcc425c 100644 --- a/system/Database/SQLite3/Forge.php +++ b/system/Database/SQLite3/Forge.php @@ -101,7 +101,7 @@ public function dropDatabase(string $dbName): bool } if (! empty($this->db->dataCache['db_names'])) { - $key = array_search(strtolower($dbName), array_map('strtolower', $this->db->dataCache['db_names']), true); + $key = array_search(strtolower($dbName), array_map(strtolower(...), $this->db->dataCache['db_names']), true); if ($key !== false) { unset($this->db->dataCache['db_names'][$key]); } diff --git a/system/Database/SQLite3/Result.php b/system/Database/SQLite3/Result.php index 70d8dc47d2b6..aa5abf28682a 100644 --- a/system/Database/SQLite3/Result.php +++ b/system/Database/SQLite3/Result.php @@ -147,7 +147,7 @@ protected function fetchObject(string $className = 'stdClass') return $classObj->injectRawData($row); } - $classSet = Closure::bind(function ($key, $value) { + $classSet = Closure::bind(function ($key, $value): void { $this->{$key} = $value; }, $classObj, $className); diff --git a/system/Debug/Toolbar.php b/system/Debug/Toolbar.php index 115016225062..9f20f987f59c 100644 --- a/system/Debug/Toolbar.php +++ b/system/Debug/Toolbar.php @@ -294,7 +294,7 @@ protected function collectTimelineData($collectors): array array_multisort(...$sortArray); // Add end time to each element - array_walk($data, static function (&$row) { + array_walk($data, static function (&$row): void { $row['end'] = $row['start'] + $row['duration']; }); diff --git a/system/Debug/Toolbar/Views/toolbar.css b/system/Debug/Toolbar/Views/toolbar.css index 2e165b825e99..b9f13c51ff2e 100644 --- a/system/Debug/Toolbar/Views/toolbar.css +++ b/system/Debug/Toolbar/Views/toolbar.css @@ -60,6 +60,7 @@ margin-right: 5px; } #debug-bar h2 { + font-weight: bold; font-size: 16px; margin: 0; padding: 5px 0 10px 0; @@ -213,6 +214,8 @@ white-space: nowrap; } #debug-bar .tab { + height: fit-content; + text-align: left; bottom: 35px; display: none; left: 0; @@ -225,6 +228,8 @@ z-index: 9999; } #debug-bar .timeline { + position: static; + display: table; margin-left: 0; width: 100%; } diff --git a/system/Files/File.php b/system/Files/File.php index 02c8f181b19b..15b53262e9c2 100644 --- a/system/Files/File.php +++ b/system/Files/File.php @@ -59,7 +59,8 @@ public function __construct(string $path, bool $checkFile = false) * * Implementations SHOULD return the value stored in the "size" key of * the file in the $_FILES array if available, as PHP calculates this based - * on the actual size transmitted. + * on the actual size transmitted. A RuntimeException will be thrown if the file + * does not exist or an error occurs. * * @return false|int The file size in bytes, or false on failure */ diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index d8e22835e2ed..1255e0c4055d 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -522,7 +522,7 @@ private function getCleanName(string $name): array [$name, $arguments] = explode(':', $name); $arguments = explode(',', $arguments); - array_walk($arguments, static function (&$item) { + array_walk($arguments, static function (&$item): void { $item = trim($item); }); } diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php index 4b1c9c625399..9da231c17716 100644 --- a/system/HTTP/CURLRequest.php +++ b/system/HTTP/CURLRequest.php @@ -651,11 +651,12 @@ protected function setCURLOptions(array $curlOptions = [], array $config = []) // version if (! empty($config['version'])) { - if ($config['version'] === 1.0) { + $version = sprintf('%.1F', $config['version']); + if ($version === '1.0') { $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0; - } elseif ($config['version'] === 1.1) { + } elseif ($version === '1.1') { $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1; - } elseif ($config['version'] === 2.0) { + } elseif ($version === '2.0') { $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0; } } diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index ec13e40f6ff5..c905de0a3ae3 100644 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -586,7 +586,7 @@ public function getJsonVar($index = null, bool $assoc = false, ?int $filter = nu ) { if (is_array($data)) { // Iterate over array and append filter and flags - array_walk_recursive($data, static function (&$val) use ($filter, $flags) { + array_walk_recursive($data, static function (&$val) use ($filter, $flags): void { $valType = gettype($val); $val = filter_var($val, $filter, $flags); @@ -672,7 +672,7 @@ public function getRawInputVar($index = null, ?int $filter = null, $flags = null ) ) { // Iterate over array and append filter and flags - array_walk_recursive($output, static function (&$val) use ($filter, $flags) { + array_walk_recursive($output, static function (&$val) use ($filter, $flags): void { $val = filter_var($val, $filter, $flags); }); diff --git a/system/HTTP/RequestTrait.php b/system/HTTP/RequestTrait.php index cfdc52b55f69..43a4f23a0e87 100644 --- a/system/HTTP/RequestTrait.php +++ b/system/HTTP/RequestTrait.php @@ -322,7 +322,7 @@ public function fetchGlobal(string $name, $index = null, ?int $filter = null, $f ) ) { // Iterate over array and append filter and flags - array_walk_recursive($value, static function (&$val) use ($filter, $flags) { + array_walk_recursive($value, static function (&$val) use ($filter, $flags): void { $val = filter_var($val, $filter, $flags); }); diff --git a/system/Helpers/cookie_helper.php b/system/Helpers/cookie_helper.php index 49e568346b68..27007104e68e 100644 --- a/system/Helpers/cookie_helper.php +++ b/system/Helpers/cookie_helper.php @@ -35,8 +35,6 @@ * @param bool|null $httpOnly True makes the cookie accessible via http(s) only (no javascript) * @param string|null $sameSite The cookie SameSite value * - * @return void - * * @see \CodeIgniter\HTTP\Response::setCookie() */ function set_cookie( @@ -49,7 +47,7 @@ function set_cookie( ?bool $secure = null, ?bool $httpOnly = null, ?string $sameSite = null - ) { + ): void { $response = service('response'); $response->setCookie($name, $value, $expire, $domain, $path, $prefix, $secure, $httpOnly, $sameSite); } @@ -92,11 +90,9 @@ function get_cookie($index, bool $xssClean = false, ?string $prefix = '') * @param string $path the cookie path * @param string $prefix the cookie prefix * - * @return void - * * @see \CodeIgniter\HTTP\Response::deleteCookie() */ - function delete_cookie($name, string $domain = '', string $path = '/', string $prefix = '') + function delete_cookie($name, string $domain = '', string $path = '/', string $prefix = ''): void { service('response')->deleteCookie($name, $domain, $path, $prefix); } diff --git a/system/Helpers/kint_helper.php b/system/Helpers/kint_helper.php index 075c162315aa..1f89dc78c55b 100644 --- a/system/Helpers/kint_helper.php +++ b/system/Helpers/kint_helper.php @@ -24,7 +24,7 @@ * * @codeCoverageIgnore Can't be tested ... exits */ - function dd(...$vars) + function dd(...$vars): void { // @codeCoverageIgnoreStart Kint::$aliases[] = 'dd'; @@ -71,10 +71,8 @@ function d(...$vars) */ /** * trace function - * - * @return void */ - function trace() + function trace(): void { Kint::$aliases[] = 'trace'; Kint::trace(); diff --git a/system/Helpers/text_helper.php b/system/Helpers/text_helper.php index 7f5fb2cf2d3f..ae6b6f111c81 100644 --- a/system/Helpers/text_helper.php +++ b/system/Helpers/text_helper.php @@ -310,7 +310,7 @@ function convert_accented_characters(string $str): string if (! is_array($arrayFrom)) { $config = new ForeignCharacters(); - if (empty($config->characterList) || ! is_array($config->characterList)) { + if ($config->characterList === [] || ! is_array($config->characterList)) { $arrayFrom = []; $arrayTo = []; diff --git a/system/Images/Handlers/GDHandler.php b/system/Images/Handlers/GDHandler.php index 350dbc2306a8..062a69d89212 100644 --- a/system/Images/Handlers/GDHandler.php +++ b/system/Images/Handlers/GDHandler.php @@ -471,7 +471,7 @@ protected function textOverlay(string $text, array $options = [], bool $isShadow // shorthand hex, #f00 if (strlen($color) === 3) { - $color = implode('', array_map('str_repeat', str_split($color), [2, 2, 2])); + $color = implode('', array_map(str_repeat(...), str_split($color), [2, 2, 2])); } $color = str_split(substr($color, 0, 6), 2); diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 3198d342aa9d..8fba39dc03be 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -853,7 +853,7 @@ public function resource(string $name, ?array $options = null): RouteCollectionI // In order to allow customization of the route the // resources are sent to, we need to have a new name // to store the values in. - $newName = implode('\\', array_map('ucfirst', explode('/', $name))); + $newName = implode('\\', array_map(ucfirst(...), explode('/', $name))); // If a new controller is specified, then we replace the // $name value with the name of the new controller. @@ -947,7 +947,7 @@ public function presenter(string $name, ?array $options = null): RouteCollection // In order to allow customization of the route the // resources are sent to, we need to have a new name // to store the values in. - $newName = implode('\\', array_map('ucfirst', explode('/', $name))); + $newName = implode('\\', array_map(ucfirst(...), explode('/', $name))); // If a new controller is specified, then we replace the // $name value with the name of the new controller. @@ -1456,6 +1456,12 @@ protected function create(string $verb, string $from, $to, ?array $options = nul $to = $this->processArrayCallableSyntax($from, $to); } + // Merge group filters. + if (isset($options['filter'])) { + $currentFilter = (array) ($this->currentOptions['filter'] ?? []); + $options['filter'] = array_merge($currentFilter, (array) $options['filter']); + } + $options = array_merge($this->currentOptions ?? [], $options ?? []); // Route priority detect diff --git a/system/Security/CheckPhpIni.php b/system/Security/CheckPhpIni.php index 4cef565ad7d6..1e92cdc403aa 100644 --- a/system/Security/CheckPhpIni.php +++ b/system/Security/CheckPhpIni.php @@ -50,17 +50,17 @@ public static function run(bool $isCli = true) private static function outputForCli(array $output, array $thead, array $tbody): void { foreach ($output as $directive => $values) { - $current = $values['current']; + $current = $values['current'] ?? ''; $notRecommended = false; if ($values['recommended'] !== '') { - if ($values['recommended'] !== $values['current']) { + if ($values['recommended'] !== $current) { $notRecommended = true; } $current = $notRecommended - ? CLI::color($values['current'] === '' ? 'n/a' : $values['current'], 'red') - : $values['current']; + ? CLI::color($current === '' ? 'n/a' : $current, 'red') + : $current; } $directive = $notRecommended ? CLI::color($directive, 'red') : $directive; diff --git a/system/Test/DatabaseTestTrait.php b/system/Test/DatabaseTestTrait.php index 863ed542319d..c0d4473c7b9e 100644 --- a/system/Test/DatabaseTestTrait.php +++ b/system/Test/DatabaseTestTrait.php @@ -19,6 +19,7 @@ use Config\Database; use Config\Migrations; use Config\Services; +use PHPUnit\Framework\Attributes\AfterClass; /** * DatabaseTestTrait @@ -228,14 +229,12 @@ public function seed(string $name) // -------------------------------------------------------------------- // Utility // -------------------------------------------------------------------- - /** * Reset $doneMigration and $doneSeed * - * @afterClass - * * @return void */ + #[AfterClass] public static function resetMigrationSeedCount() { self::$doneMigration = false; diff --git a/system/Throttle/Throttler.php b/system/Throttle/Throttler.php index 44d12005a80d..c023b5e4d052 100644 --- a/system/Throttle/Throttler.php +++ b/system/Throttle/Throttler.php @@ -102,8 +102,11 @@ public function check(string $key, int $capacity, int $seconds, int $cost = 1): // Number of seconds to get one token $refresh = 1 / $rate; + /** @var float|int|null $tokens */ + $tokens = $this->cache->get($tokenName); + // Check to see if the bucket has even been created yet. - if (($tokens = $this->cache->get($tokenName)) === null) { + if ($tokens === null) { // If it hasn't been created, then we'll set it to the maximum // capacity - 1, and save it to the cache. $tokens = $capacity - $cost; @@ -124,7 +127,7 @@ public function check(string $key, int $capacity, int $seconds, int $cost = 1): // should be refilled, then checked against capacity // to be sure the bucket didn't overflow. $tokens += $rate * $elapsed; - $tokens = $tokens > $capacity ? $capacity : $tokens; + $tokens = min($tokens, $capacity); // If $tokens >= 1, then we are safe to perform the action, but // we need to decrement the number of available tokens. diff --git a/system/Validation/Rules.php b/system/Validation/Rules.php index 272b2dcf61fe..04e8a0546e77 100644 --- a/system/Validation/Rules.php +++ b/system/Validation/Rules.php @@ -159,7 +159,7 @@ public function in_list($value, string $list): bool $value = (string) $value; } - $list = array_map('trim', explode(',', $list)); + $list = array_map(trim(...), explode(',', $list)); return in_array($value, $list, true); } diff --git a/tests/system/Config/ServicesTest.php b/tests/system/Config/ServicesTest.php index 7825687841c7..e80e2d394eeb 100644 --- a/tests/system/Config/ServicesTest.php +++ b/tests/system/Config/ServicesTest.php @@ -46,6 +46,9 @@ use Config\App; use Config\Exceptions; use Config\Security as SecurityConfig; +use Config\Session as ConfigSession; +use InvalidArgumentException; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\PreserveGlobalState; use PHPUnit\Framework\Attributes\RunInSeparateProcess; @@ -259,6 +262,32 @@ public function testNewSessionWithNullConfig(): void $this->assertInstanceOf(Session::class, $actual); } + #[DataProvider('provideNewSessionInvalid')] + #[PreserveGlobalState(false)] + #[RunInSeparateProcess] + public function testNewSessionWithInvalidHandler(string $driver): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage(sprintf('Invalid session handler "%s" provided.', $driver)); + + $config = new ConfigSession(); + + $config->driver = $driver; + Services::session($config, false); + } + + /** + * @return iterable + */ + public static function provideNewSessionInvalid(): iterable + { + yield 'just a string' => ['file']; + + yield 'inexistent class' => ['Foo']; + + yield 'other class' => [self::class]; + } + #[PreserveGlobalState(false)] #[RunInSeparateProcess] public function testCallStatic(): void diff --git a/tests/system/Database/Builder/SelectTest.php b/tests/system/Database/Builder/SelectTest.php index cd3f39029f6b..411e26a695bf 100644 --- a/tests/system/Database/Builder/SelectTest.php +++ b/tests/system/Database/Builder/SelectTest.php @@ -19,6 +19,7 @@ use CodeIgniter\Database\SQLSRV\Builder as SQLSRVBuilder; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Mock\MockConnection; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; /** @@ -67,6 +68,65 @@ public function testSelectAcceptsArray(): void $this->assertSame($expected, str_replace("\n", ' ', $builder->getCompiledSelect())); } + /** + * @param list $select + */ + #[DataProvider('provideSelectAcceptsArrayWithRawSql')] + public function testSelectAcceptsArrayWithRawSql(array $select, string $expected): void + { + $builder = new BaseBuilder('employees', $this->db); + + $builder->select($select); + + $this->assertSame($expected, str_replace("\n", ' ', $builder->getCompiledSelect())); + } + + /** + * @return list|string> + */ + public static function provideSelectAcceptsArrayWithRawSql(): iterable + { + yield from [ + [ + [ + new RawSql("IF(salary > 5000, 'High', 'Low') AS salary_level"), + 'employee_id', + ], + <<<'SQL' + SELECT IF(salary > 5000, 'High', 'Low') AS salary_level, "employee_id" FROM "employees" + SQL, + ], + [ + [ + 'employee_id', + new RawSql("IF(salary > 5000, 'High', 'Low') AS salary_level"), + ], + <<<'SQL' + SELECT "employee_id", IF(salary > 5000, 'High', 'Low') AS salary_level FROM "employees" + SQL, + ], + [ + [ + new RawSql("CONCAT(first_name, ' ', last_name) AS full_name"), + new RawSql("IF(salary > 5000, 'High', 'Low') AS salary_level"), + ], + <<<'SQL' + SELECT CONCAT(first_name, ' ', last_name) AS full_name, IF(salary > 5000, 'High', 'Low') AS salary_level FROM "employees" + SQL, + ], + [ + [ + new RawSql("CONCAT(first_name, ' ', last_name) AS full_name"), + 'employee_id', + new RawSql("IF(salary > 5000, 'High', 'Low') AS salary_level"), + ], + <<<'SQL' + SELECT CONCAT(first_name, ' ', last_name) AS full_name, "employee_id", IF(salary > 5000, 'High', 'Low') AS salary_level FROM "employees" + SQL, + ], + ]; + } + public function testSelectAcceptsMultipleColumns(): void { $builder = new BaseBuilder('users', $this->db); @@ -100,6 +160,28 @@ public function testSelectWorksWithComplexSelects(): void $this->assertSame($expected, str_replace("\n", ' ', $builder->getCompiledSelect())); } + public function testSelectNullAsInString(): void + { + $builder = new BaseBuilder('users', $this->db); + + $builder->select('NULL as field_alias, name'); + + $expected = 'SELECT NULL as field_alias, "name" FROM "users"'; + + $this->assertSame($expected, str_replace("\n", ' ', $builder->getCompiledSelect())); + } + + public function testSelectNullAsInArray(): void + { + $builder = new BaseBuilder('users', $this->db); + + $builder->select(['NULL as field_alias', 'name']); + + $expected = 'SELECT NULL as field_alias, "name" FROM "users"'; + + $this->assertSame($expected, str_replace("\n", ' ', $builder->getCompiledSelect())); + } + /** * @see https://github.com/codeigniter4/CodeIgniter4/issues/4355 */ diff --git a/tests/system/Database/Live/OCI8/ConnectionTest.php b/tests/system/Database/Live/OCI8/ConnectionTest.php new file mode 100644 index 000000000000..caf97ed632fd --- /dev/null +++ b/tests/system/Database/Live/OCI8/ConnectionTest.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Database\Live\OCI8; + +use CodeIgniter\Database\OCI8\Connection; +use CodeIgniter\Test\CIUnitTestCase; +use Config\Database as DbConfig; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; + +/** + * @internal + */ +#[Group('DatabaseLive')] +final class ConnectionTest extends CIUnitTestCase +{ + /** + * @var array Database connection settings + */ + private array $settings = []; + + protected function setUp(): void + { + $dbConfig = config(DbConfig::class); + $this->settings = $dbConfig->{$this->DBGroup}; + + if ($this->settings['DBDriver'] !== 'OCI8') { + $this->markTestSkipped('This test is only for OCI8.'); + } + } + + #[DataProvider('provideIsValidDSN')] + public function testIsValidDSN(string $dsn): void + { + $this->settings['DSN'] = $dsn; + + $db = new Connection($this->settings); + + $isValidDSN = $this->getPrivateMethodInvoker($db, 'isValidDSN'); + + $this->assertTrue($isValidDSN()); + } + + /** + * @return array> + */ + public static function provideIsValidDSN(): iterable + { + yield from [ + // Easy Connect string + // See https://docs.oracle.com/en/database/oracle/oracle-database/23/netag/configuring-naming-methods.html#GUID-36F3A17D-843C-490A-8A23-FB0FE005F8E8 + 'HostOnly' => ['sales-server'], + 'Host:Port' => ['sales-server:3456'], + 'Host/ServiceName' => ['sales-server/sales'], + 'IPv6Address:Port/ServiceName' => ['[2001:0db8:0:0::200C:417A]:80/sales'], + 'Host:Port/ServiceName' => ['sales-server:80/sales'], + 'Host/ServiceName:ServerType/InstanceName' => ['sales-server/sales:dedicated/inst1'], + 'Host:InstanceName' => ['sales-server//inst1'], + 'Host/ServiceNameWithDots' => ['myhost/my.service.name'], + 'Host:Port/ServiceNameWithDots' => ['myhost:1521/my.service.name'], + ]; + } +} diff --git a/tests/system/HTTP/CURLRequestTest.php b/tests/system/HTTP/CURLRequestTest.php index dc48eb621d80..f14198be2264 100644 --- a/tests/system/HTTP/CURLRequestTest.php +++ b/tests/system/HTTP/CURLRequestTest.php @@ -1227,4 +1227,34 @@ public function testGetHeaderLineContentType(): void $this->assertSame('text/html; charset=UTF-8', $response->getHeaderLine('Content-Type')); } + + public function testHTTPversionAsString(): void + { + $this->request->request('POST', '/post', [ + 'version' => '1.0', + ]); + + $options = $this->request->curl_options; + + $this->assertArrayHasKey(CURLOPT_HTTP_VERSION, $options); + $this->assertSame(CURL_HTTP_VERSION_1_0, $options[CURLOPT_HTTP_VERSION]); + + $this->request->request('POST', '/post', [ + 'version' => '1.1', + ]); + + $options = $this->request->curl_options; + + $this->assertArrayHasKey(CURLOPT_HTTP_VERSION, $options); + $this->assertSame(CURL_HTTP_VERSION_1_1, $options[CURLOPT_HTTP_VERSION]); + + $this->request->request('POST', '/post', [ + 'version' => '2.0', + ]); + + $options = $this->request->curl_options; + + $this->assertArrayHasKey(CURLOPT_HTTP_VERSION, $options); + $this->assertSame(CURL_HTTP_VERSION_2_0, $options[CURLOPT_HTTP_VERSION]); + } } diff --git a/tests/system/Router/RouteCollectionTest.php b/tests/system/Router/RouteCollectionTest.php index 89959cd5f33e..5c06154a5d31 100644 --- a/tests/system/Router/RouteCollectionTest.php +++ b/tests/system/Router/RouteCollectionTest.php @@ -440,6 +440,22 @@ static function ($routes): void { $this->assertSame($expected, $routes->getRoutesOptions()); } + public function testGroupFilterAndRouteFilter(): void + { + $routes = $this->getCollector(); + + $routes->group('admin', ['filter' => ['csrf']], static function ($routes): void { + $routes->get('profile', 'Admin\Profile::index', ['filter' => ['honeypot']]); + }); + + $expected = [ + 'admin/profile' => [ + 'filter' => ['csrf', 'honeypot'], + ], + ]; + $this->assertSame($expected, $routes->getRoutesOptions()); + } + public function testGroupingWorksWithEmptyStringPrefix(): void { $routes = $this->getCollector(); diff --git a/tools/phpmetrics/composer.json b/tools/phpmetrics/composer.json deleted file mode 100644 index eef46d0db139..000000000000 --- a/tools/phpmetrics/composer.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "require-dev": { - "phpmetrics/phpmetrics": "^3.0rc6" - } -} diff --git a/user_guide_src/source/changelogs/index.rst b/user_guide_src/source/changelogs/index.rst index d97ccaa0b0ac..3dc5cb698f75 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.5.4 v4.5.3 v4.5.2 v4.5.1 diff --git a/user_guide_src/source/changelogs/v4.5.4.rst b/user_guide_src/source/changelogs/v4.5.4.rst new file mode 100644 index 000000000000..257e42bbfb02 --- /dev/null +++ b/user_guide_src/source/changelogs/v4.5.4.rst @@ -0,0 +1,24 @@ +############# +Version 4.5.4 +############# + +Release Date: July 27, 2024 + +**4.5.4 release of CodeIgniter4** + +.. contents:: + :local: + :depth: 3 + +********** +Bugs Fixed +********** + +- **Routing:** Fixed a bug that filters passed to ``$routes->group()`` were not + merged into filters passed to the inner routes. +- **CURLRequest:** Fixed a bug preventing the use of strings for ``version`` in the config array + when making requests. + +See the repo's +`CHANGELOG.md `_ +for a complete list of bugs fixed. diff --git a/user_guide_src/source/conf.py b/user_guide_src/source/conf.py index 442a6f49e0d6..431dc6d57bd9 100644 --- a/user_guide_src/source/conf.py +++ b/user_guide_src/source/conf.py @@ -26,7 +26,7 @@ version = '4.5' # The full version, including alpha/beta/rc tags. -release = '4.5.3' +release = '4.5.4' # -- General configuration --------------------------------------------------- diff --git a/user_guide_src/source/database/query_builder.rst b/user_guide_src/source/database/query_builder.rst index 171c354d1120..454fc56c6bbf 100644 --- a/user_guide_src/source/database/query_builder.rst +++ b/user_guide_src/source/database/query_builder.rst @@ -247,6 +247,8 @@ Use the ``$db->newQuery()`` method to make a subquery the main table: Join ==== +.. _query-builder-join: + $builder->join() ---------------- @@ -270,7 +272,7 @@ RawSql .. versionadded:: 4.2.0 -Since v4.2.0, ``$builder->join()`` accepts a ``CodeIgniter\Database\RawSql`` instance, which expresses raw SQL strings. +Since v4.2.0, ``$builder->join()`` accepts a ``CodeIgniter\Database\RawSql`` instance as the JOIN ON condition, which expresses raw SQL strings. .. literalinclude:: query_builder/102.php @@ -1498,13 +1500,14 @@ Class Reference .. php:method:: join($table, $cond[, $type = ''[, $escape = null]]) :param string $table: Table name to join - :param string $cond: The JOIN ON condition + :param string|RawSql $cond: The JOIN ON condition :param string $type: The JOIN type :param bool $escape: Whether to escape values and identifiers :returns: ``BaseBuilder`` instance (method chaining) :rtype: ``BaseBuilder`` - Adds a ``JOIN`` clause to a query. + Adds a ``JOIN`` clause to a query. Since v4.2.0, ``RawSql`` can be used + as the JOIN ON condition. See also :ref:`query-builder-join`. .. php:method:: where($key[, $value = null[, $escape = null]]) diff --git a/user_guide_src/source/general/configuration.rst b/user_guide_src/source/general/configuration.rst index bfb4b0f1b4ef..cc24b3f3723c 100644 --- a/user_guide_src/source/general/configuration.rst +++ b/user_guide_src/source/general/configuration.rst @@ -314,6 +314,10 @@ Registrars provide a means of altering a configuration at runtime across namespa Registrars work if :ref:`auto-discovery` is enabled in :doc:`Modules `. It alters configuration properties when the Config object is instantiated. +.. note:: This feature is implemented in the ``CodeIgniter\Config\BaseConfig`` + class. So it will not work with a few files in the **app/Config** folder + that do not extends the class. + There are two ways to implement a Registrar: **implicit** and **explicit**. .. note:: Values from **.env** always take priority over Registrars. diff --git a/user_guide_src/source/incoming/filters.rst b/user_guide_src/source/incoming/filters.rst index 4425ce03d3bf..737fd191392a 100644 --- a/user_guide_src/source/incoming/filters.rst +++ b/user_guide_src/source/incoming/filters.rst @@ -82,7 +82,7 @@ Configuring Filters There are two ways to configure filters when they get run. One is done in **app/Config/Filters.php**, the other is done in **app/Config/Routes.php**. -If you want to specify filter to a specific route, use **app/Config/Routes.php** +If you want to specify filters to defined routes, use **app/Config/Routes.php** and see :ref:`URI Routing `. .. Note:: The safest way to apply filters is to :ref:`disable auto-routing `, and :ref:`set filters to routes `. @@ -95,9 +95,11 @@ configure exactly when the filters run. .. Warning:: It is recommended that you should always add ``*`` at the end of a URI in the filter settings. Because a controller method might be accessible by different URLs than you think. - For example, when :ref:`auto-routing-legacy` is enabled, if you have ``Blog::index``, + For example, when :ref:`auto-routing-legacy` is enabled, if you have ``Blog::index()``, it can be accessible with ``blog``, ``blog/index``, and ``blog/index/1``, etc. +.. _filters-aliases: + $aliases -------- @@ -106,7 +108,9 @@ filters to run: .. literalinclude:: filters/003.php -Aliases are mandatory and if you try to use a full class name later, the system will throw an error. Defining them +Aliases are mandatory and if you try to use a full class name later, the system will throw an error. + +Defining them in this way makes it simple to switch out the class used. Great for when you decided you need to change to a different authentication system since you only change the filter's class and you're done. @@ -170,7 +174,7 @@ an array with the ``except`` key and a URI path (relative to BaseURL) to match a and the URI paths specified in the filter could be different. See :ref:`upgrade-447-filter-paths` for details. -Any place you can use a URI path (relative to BaseURL) in the filter settings, you can use a regular expression or, like in this example, use +Any place you can use a URI path (relative to BaseURL) in the filter settings, you can use a regular expression or, like in this example above, use an asterisk (``*``) for a wildcard that will match all characters after that. In this example, any URI path starting with ``api/`` would be exempted from CSRF protection, but the site's forms would all be protected. @@ -186,17 +190,19 @@ $methods because :ref:`auto-routing-legacy` permits any HTTP method to access a controller. Accessing the controller with a method you don't expect could bypass the filter. -You can apply filters to all requests of a certain HTTP method, like POST, GET, PUT, etc. In this array, you would -specify the method name in **lowercase**. It's value would be an array of filters to run: +You can apply filters to all requests of a certain HTTP method, like ``POST``, ``GET``, ``PUT``, etc. +It's value would be an array of filters to run: .. literalinclude:: filters/008.php .. note:: Unlike the ``$globals`` or the ``$filters`` properties, these will only run as before filters. -In addition to the standard HTTP methods, this also supports one special case: ``cli``. The ``cli`` method would apply to +In addition to the standard HTTP methods, this also supports one special case: ``CLI``. The ``CLI`` method would apply to all requests that were run from the command line. +.. note:: Prior to v4.5.0, due to a bug, you needed to specify the HTTP method names in **lowercase**. + $filters -------- diff --git a/user_guide_src/source/incoming/routing.rst b/user_guide_src/source/incoming/routing.rst index 867983437acd..90c99ad73b61 100644 --- a/user_guide_src/source/incoming/routing.rst +++ b/user_guide_src/source/incoming/routing.rst @@ -388,12 +388,13 @@ Applying Filters ================ You can alter the behavior of specific routes by supplying filters to run before or after the controller. This is especially handy during authentication or api logging. + The value for the filter can be a string or an array of strings: * matching the aliases defined in **app/Config/Filters.php**. * filter classnames -See :doc:`Controller Filters ` for more information on setting up filters. +See :ref:`Controller Filters ` for more information on defining aliases. .. Warning:: If you set filters to routes in **app/Config/Routes.php** (not in **app/Config/Filters.php**), it is recommended to disable Auto Routing (Legacy). @@ -405,7 +406,7 @@ See :doc:`Controller Filters ` for more information on setting up filte Alias Filter ------------ -You specify an alias defined in **app/Config/Filters.php** for the filter value: +You specify an alias :ref:`defined in app/Config/Filters.php ` for the filter value: .. literalinclude:: routing/034.php @@ -418,7 +419,7 @@ Classname Filter .. versionadded:: 4.1.5 -You specify a filter classname for the filter value: +You can specify a filter classname for the filter value: .. literalinclude:: routing/036.php @@ -435,7 +436,7 @@ Multiple Filters :ref:`Upgrading from 4.1.4 to 4.1.5 ` for the details. -You specify an array for the filter value: +You can specify an array for the filter value: .. literalinclude:: routing/037.php @@ -563,6 +564,9 @@ run the filter before or after the controller. This is especially handy during a The value for the filter must match one of the aliases defined within **app/Config/Filters.php**. +.. note:: Prior to v4.5.4, due to a bug, filters passed to the ``group()`` were + not merged into the filters passed to the inner routes. + Setting Other Options ===================== diff --git a/user_guide_src/source/installation/upgrade_420.rst b/user_guide_src/source/installation/upgrade_420.rst index 03aebabb015c..d86800f58dbc 100644 --- a/user_guide_src/source/installation/upgrade_420.rst +++ b/user_guide_src/source/installation/upgrade_420.rst @@ -43,6 +43,8 @@ The constants ``EVENT_PRIORITY_LOW``, ``EVENT_PRIORITY_NORMAL`` and ``EVENT_PRIO composer.json ============= +.. note:: This procedure is not required in v4.5.0 or later. + If you use Composer, when you installed CodeIgniter v4.1.9 or before, and if there are ``App\\`` and ``Config\\`` namespaces in your ``/composer.json``'s ``autoload.psr-4`` like the following, you need to remove these lines, and run ``composer dump-autoload``. diff --git a/user_guide_src/source/installation/upgrade_454.rst b/user_guide_src/source/installation/upgrade_454.rst new file mode 100644 index 000000000000..2e460eed3a16 --- /dev/null +++ b/user_guide_src/source/installation/upgrade_454.rst @@ -0,0 +1,33 @@ +############################# +Upgrading from 4.5.3 to 4.5.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 + +************* +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 `_. + +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: + +- app/Config/Events.php +- composer.json diff --git a/user_guide_src/source/installation/upgrading.rst b/user_guide_src/source/installation/upgrading.rst index 2615c49fe10a..0daa9309cddc 100644 --- a/user_guide_src/source/installation/upgrading.rst +++ b/user_guide_src/source/installation/upgrading.rst @@ -16,6 +16,7 @@ See also :doc:`./backward_compatibility_notes`. backward_compatibility_notes + upgrade_454 upgrade_453 upgrade_452 upgrade_451 diff --git a/user_guide_src/source/libraries/email.rst b/user_guide_src/source/libraries/email.rst index c506a9044757..ec21b79ddcf2 100644 --- a/user_guide_src/source/libraries/email.rst +++ b/user_guide_src/source/libraries/email.rst @@ -113,35 +113,37 @@ sending email. =================== =================== ============================ ======================================================================= Preference Default Value Options Description =================== =================== ============================ ======================================================================= -**userAgent** CodeIgniter None The "user agent". +**fromEmail** The email address to be set in the "from" header. +**fromName** The name to be set in the "from" header. +**userAgent** CodeIgniter The "user agent". **protocol** mail ``mail``, ``sendmail``, The mail sending protocol. or ``smtp`` -**mailPath** /usr/sbin/sendmail None The server path to Sendmail. -**SMTPHost** No Default None SMTP Server Hostname. -**SMTPUser** No Default None SMTP Username. -**SMTPPass** No Default None SMTP Password. -**SMTPPort** 25 None SMTP Port. (If set to ``465``, TLS will be used for the connection +**mailPath** /usr/sbin/sendmail The server path to Sendmail. +**SMTPHost** SMTP Server Hostname. +**SMTPUser** SMTP Username. +**SMTPPass** SMTP Password. +**SMTPPort** 25 SMTP Port. (If set to ``465``, TLS will be used for the connection regardless of ``SMTPCrypto`` setting.) -**SMTPTimeout** 5 None SMTP Timeout (in seconds). -**SMTPKeepAlive** false ``true``/``false`` (boolean) Enable persistent SMTP connections. +**SMTPTimeout** 5 SMTP Timeout (in seconds). +**SMTPKeepAlive** false ``true``/``false`` Enable persistent SMTP connections. **SMTPCrypto** tls ``tls``, ``ssl``, or SMTP Encryption. Setting this to ``ssl`` will create a secure empty string (``''``) channel to the server using SSL, and ``tls`` will issue a ``STARTTLS`` command to the server. Connection on port ``465`` should set this to an empty string (``''``). See also :ref:`email-ssl-tls-for-smtp`. -**wordWrap** true ``true``/``false`` (boolean) Enable word-wrap. +**wordWrap** true ``true``/``false`` Enable word-wrap. **wrapChars** 76 Character count to wrap at. **mailType** text ``text`` or ``html`` Type of mail. If you send HTML email you must send it as a complete web page. Make sure you don't have any relative links or relative image paths otherwise they will not work. -**charset** utf-8 Character set (``utf-8``, ``iso-8859-1``, etc.). -**validate** true ``true``/``false`` (boolean) Whether to validate the email address. +**charset** UTF-8 Character set (``utf-8``, ``iso-8859-1``, etc.). +**validate** true ``true``/``false`` Whether to validate the email address. **priority** 3 1, 2, 3, 4, 5 Email Priority. ``1`` = highest. ``5`` = lowest. ``3`` = normal. -**CRLF** \\n ``\r\n`` or ``\n`` or ``\r`` Newline character. (Use ``\r\n`` to comply with RFC 822). -**newline** \\n ``\r\n`` or ``\n`` or ``\r`` Newline character. (Use ``\r\n`` to comply with RFC 822). -**BCCBatchMode** false ``true``/``false`` (boolean) Enable BCC Batch Mode. -**BCCBatchSize** 200 None Number of emails in each BCC batch. -**DSN** false ``true``/``false`` (boolean) Enable notify message from server. +**CRLF** \\r\\n ``\r\n``, ``\n`` or ``\r`` Newline character. (Use ``\r\n`` to comply with RFC 822). +**newline** \\r\\n ``\r\n``, ``\n`` or ``\r`` Newline character. (Use ``\r\n`` to comply with RFC 822). +**BCCBatchMode** false ``true``/``false`` Enable BCC Batch Mode. +**BCCBatchSize** 200 Number of emails in each BCC batch. +**DSN** false ``true``/``false`` Enable notify message from server. =================== =================== ============================ ======================================================================= Overriding Word Wrapping @@ -173,9 +175,9 @@ Class Reference .. php:method:: setFrom($from[, $name = ''[, $returnPath = null]]) - :param string $from: "From" e-mail address + :param string $from: "From" email address :param string $name: "From" display name - :param string $returnPath: Optional email address to redirect undelivered e-mail to + :param string $returnPath: Optional email address to redirect undelivered email to :returns: CodeIgniter\\Email\\Email instance (method chaining) :rtype: CodeIgniter\\Email\\Email @@ -192,8 +194,8 @@ Class Reference .. php:method:: setReplyTo($replyto[, $name = '']) - :param string $replyto: E-mail address for replies - :param string $name: Display name for the reply-to e-mail address + :param string $replyto: Email address for replies + :param string $name: Display name for the reply-to email address :returns: CodeIgniter\\Email\\Email instance (method chaining) :rtype: CodeIgniter\\Email\\Email @@ -204,12 +206,12 @@ Class Reference .. php:method:: setTo($to) - :param mixed $to: Comma-delimited string or an array of e-mail addresses + :param mixed $to: Comma separated string or an array of email addresses :returns: CodeIgniter\\Email\\Email instance (method chaining) :rtype: CodeIgniter\\Email\\Email - Sets the email address(s) of the recipient(s). Can be a single e-mail, - a comma-delimited list or an array: + Sets the email address(es) of the recipient(s). Can be a single email, + a comma separated list or an array: .. literalinclude:: email/006.php @@ -219,22 +221,22 @@ Class Reference .. php:method:: setCC($cc) - :param mixed $cc: Comma-delimited string or an array of e-mail addresses + :param mixed $cc: Comma separated string or an array of email addresses :returns: CodeIgniter\\Email\\Email instance (method chaining) :rtype: CodeIgniter\\Email\\Email - Sets the CC email address(s). Just like the "to", can be a single e-mail, - a comma-delimited list or an array. + Sets the CC email address(es). Just like the "to", can be a single email, + a comma separated list or an array. .. php:method:: setBCC($bcc[, $limit = '']) - :param mixed $bcc: Comma-delimited string or an array of e-mail addresses - :param int $limit: Maximum number of e-mails to send per batch + :param mixed $bcc: Comma separated string or an array of email addresses + :param int $limit: Maximum number of emails to send per batch :returns: CodeIgniter\\Email\\Email instance (method chaining) :rtype: CodeIgniter\\Email\\Email - Sets the BCC email address(s). Just like the ``setTo()`` method, can be a single - e-mail, a comma-delimited list or an array. + Sets the BCC email address(es). Just like the ``setTo()`` method, can be a single + email, a comma separated list or an array. If ``$limit`` is set, "batch mode" will be enabled, which will send the emails to batches, with each batch not exceeding the specified @@ -242,7 +244,7 @@ Class Reference .. php:method:: setSubject($subject) - :param string $subject: E-mail subject line + :param string $subject: Email subject line :returns: CodeIgniter\\Email\\Email instance (method chaining) :rtype: CodeIgniter\\Email\\Email @@ -252,21 +254,21 @@ Class Reference .. php:method:: setMessage($body) - :param string $body: E-mail message body + :param string $body: Email message body :returns: CodeIgniter\\Email\\Email instance (method chaining) :rtype: CodeIgniter\\Email\\Email - Sets the e-mail message body: + Sets the email message body: .. literalinclude:: email/010.php .. php:method:: setAltMessage($str) - :param string $str: Alternative e-mail message body + :param string $str: Alternative email message body :returns: CodeIgniter\\Email\\Email instance (method chaining) :rtype: CodeIgniter\\Email\\Email - Sets the alternative e-mail message body: + Sets the alternative email message body: .. literalinclude:: email/011.php @@ -284,7 +286,7 @@ Class Reference :returns: CodeIgniter\\Email\\Email instance (method chaining) :rtype: CodeIgniter\\Email\\Email - Appends additional headers to the e-mail: + Appends additional headers to the email: .. literalinclude:: email/012.php @@ -311,7 +313,7 @@ Class Reference :returns: true on success, false on failure :rtype: bool - The e-mail sending method. Returns boolean true or false based on + The email sending method. Returns boolean true or false based on success or failure, enabling it to be used conditionally: .. literalinclude:: email/015.php @@ -334,7 +336,7 @@ Class Reference :param string $disposition: 'disposition' of the attachment. Most email clients make their own decision regardless of the MIME specification used here. https://www.iana.org/assignments/cont-disp/cont-disp.xhtml - :param string $newname: Custom file name to use in the e-mail + :param string $newname: Custom file name to use in the email :param string $mime: MIME type to use (useful for buffered data) :returns: CodeIgniter\\Email\\Email instance (method chaining) :rtype: CodeIgniter\\Email\\Email @@ -375,7 +377,7 @@ Class Reference .. literalinclude:: email/022.php - .. note:: Content-ID for each e-mail must be re-created for it to be unique. + .. note:: Content-ID for each email must be re-created for it to be unique. .. php:method:: printDebugger($include = ['headers', 'subject', 'body']) diff --git a/user_guide_src/source/libraries/files.rst b/user_guide_src/source/libraries/files.rst index 0b55982692c2..1622c3345c51 100644 --- a/user_guide_src/source/libraries/files.rst +++ b/user_guide_src/source/libraries/files.rst @@ -14,10 +14,12 @@ Getting a File instance *********************** You create a new File instance by passing in the path to the file in the constructor. -By default, the file does not need to exist. However, you can pass an additional argument of "true" -to check that the file exists and throw ``FileNotFoundException()`` if it does not. .. literalinclude:: files/001.php + :lines: 2- + +By default, the file does not need to exist. However, you can pass an additional argument of ``true`` +to check that the file exists and throw ``FileNotFoundException()`` if it does not. Taking Advantage of Spl *********************** @@ -25,6 +27,7 @@ Taking Advantage of Spl Once you have an instance, you have the full power of the SplFileInfo class at the ready, including: .. literalinclude:: files/002.php + :lines: 2- New Features ************ @@ -38,21 +41,28 @@ You can generate a cryptographically secure random filename, with the current ti method. This is especially useful to rename files when moving it so that the filename is unguessable: .. literalinclude:: files/003.php + :lines: 2- getSize() ========= -Returns the size of the uploaded file in bytes: +Returns the size of the file in bytes: .. literalinclude:: files/004.php + :lines: 2- + +A ``RuntimeException`` will be thrown if the file does not exist or an error occurs. getSizeByUnit() =============== -Returns the size of the uploaded file default in bytes. You can pass in either 'kb' or 'mb' as the first parameter to get +Returns the size of the file default in bytes. You can pass in either ``'kb'`` or ``'mb'`` as the first parameter to get the results in kilobytes or megabytes, respectively: .. literalinclude:: files/005.php + :lines: 2- + +A ``RuntimeException`` will be thrown if the file does not exist or an error occurs. getMimeType() ============= @@ -61,6 +71,7 @@ Retrieve the media type (mime type) of the file. Uses methods that are considere the type of file: .. literalinclude:: files/006.php + :lines: 2- guessExtension() ================ @@ -70,6 +81,7 @@ will return null. This is often a more trusted source than simply using the exte the values in **app/Config/Mimes.php** to determine extension: .. literalinclude:: files/007.php + :lines: 2- Moving Files ============ @@ -78,12 +90,15 @@ Each file can be moved to its new location with the aptly named ``move()`` metho the file to as the first parameter: .. literalinclude:: files/008.php + :lines: 2- By default, the original filename was used. You can specify a new filename by passing it as the second parameter: .. literalinclude:: files/009.php + :lines: 2- -The move() method returns a new File instance that for the relocated file, so you must capture the result if the +The ``move()`` method returns a new File instance that for the relocated file, so you must capture the result if the resulting location is needed: .. literalinclude:: files/010.php + :lines: 2- diff --git a/user_guide_src/source/libraries/user_agent/005.php b/user_guide_src/source/libraries/user_agent/005.php index 944c70975971..80aad0f42c59 100644 --- a/user_guide_src/source/libraries/user_agent/005.php +++ b/user_guide_src/source/libraries/user_agent/005.php @@ -1,5 +1,5 @@ isReferral()) { - echo $agent->referrer(); + echo $agent->getReferrer(); } diff --git a/user_guide_src/source/libraries/validation.rst b/user_guide_src/source/libraries/validation.rst index 899389d2d004..7b87954653e1 100644 --- a/user_guide_src/source/libraries/validation.rst +++ b/user_guide_src/source/libraries/validation.rst @@ -897,15 +897,11 @@ Rule Parameter Description ======================= ========== ============================================= =================================================== alpha No Fails if field has anything other than alphabetic characters in ASCII. -alpha_space No Fails if field contains anything other than - alphabetic characters or spaces in ASCII. alpha_dash No Fails if field contains anything other than alphanumeric characters, underscores or dashes in ASCII. alpha_numeric No Fails if field contains anything other than alphanumeric characters in ASCII. -alpha_numeric_space No Fails if field contains anything other than - alphanumeric or space characters in ASCII. alpha_numeric_punct No Fails if field contains anything other than alphanumeric, space, or this limited set of punctuation characters: ``~`` (tilde), @@ -916,6 +912,10 @@ alpha_numeric_punct No Fails if field contains anything other than ``_`` (underscore), ``+`` (plus), ``=`` (equals), ``|`` (vertical bar), ``:`` (colon), ``.`` (period). +alpha_numeric_space No Fails if field contains anything other than + alphanumeric or space characters in ASCII. +alpha_space No Fails if field contains anything other than + alphabetic characters or spaces in ASCII. decimal No Fails if field contains anything other than a decimal number. Also accepts a ``+`` or ``-`` sign for the number. @@ -940,9 +940,11 @@ in_list Yes Fails if field is not within a predetermined integer No Fails if field contains anything other than an integer. is_natural No Fails if field contains anything other than - a natural number: 0, 1, 2, 3, etc. + a natural number: ``0``, ``1``, ``2``, ``3`` + , etc. is_natural_no_zero No Fails if field contains anything other than - a natural number, except zero: 1, 2, 3, etc. + a natural number, except zero: ``1``, ``2``, + ``3``, etc. is_not_unique Yes Checks the database to see if the given value ``is_not_unique[table.field,where_field,where_value]`` exists. Can ignore records by field/value to filter (currently accept only one filter). @@ -964,47 +966,22 @@ not_in_list Yes Fails if field is within a predetermined list. numeric No Fails if field contains anything other than numeric characters. -regex_match Yes Fails if field does not match the regular ``regex_match[/regex/]`` - expression. permit_empty No Allows the field to receive an empty array, empty string, null or false. +regex_match Yes Fails if field does not match the regular ``regex_match[/regex/]`` + expression. required No Fails if the field is an empty array, empty string, null or false. required_with Yes The field is required when any of the other ``required_with[field1,field2]`` fields is not `empty()`_ in the data. required_without Yes The field is required when any of the other ``required_without[field1,field2]`` fields is `empty()`_ in the data. -string No A generic alternative to the alpha* rules +string No A generic alternative to the **alpha*** rules that confirms the element is a string timezone No Fails if field does not match a timezone per `timezone_identifiers_list()`_ valid_base64 No Fails if field contains anything other than valid Base64 characters. -valid_json No Fails if field does not contain a valid JSON - string. -valid_email No Fails if field does not contain a valid - email address. -valid_emails No Fails if any value provided in a comma - separated list is not a valid email. -valid_ip Yes Fails if the supplied IP is not valid. ``valid_ip[ipv6]`` - Accepts an optional parameter of ``ipv4`` or - ``ipv6`` to specify an IP format. -valid_url No Fails if field does not contain (loosely) a - URL. Includes simple strings that could be - hostnames, like "codeigniter". - **Normally,** ``valid_url_strict`` **should - be used.** -valid_url_strict Yes Fails if field does not contain a valid URL. ``valid_url_strict[https]`` - You can optionally specify a list of valid - schemas. If not specified, ``http,https`` - are valid. This rule uses PHP's - ``FILTER_VALIDATE_URL``. -valid_date Yes Fails if field does not contain a valid date. ``valid_date[d/m/Y]`` - Any string that `strtotime()`_ accepts is - valid if you don't specify an optional - parameter that matches a date format. - **So it is usually necessary to specify - the parameter.** valid_cc_number Yes Verifies that the credit card number matches ``valid_cc_number[amex]`` the format used by the specified provider. Current supported providers are: @@ -1025,11 +1002,37 @@ valid_cc_number Yes Verifies that the credit card number matches Scotiabank Scotia Card (``scotia``), BMO ABM Card (``bmoabm``), HSBC Canada Card (``hsbc``) +valid_date Yes Fails if field does not contain a valid date. ``valid_date[d/m/Y]`` + Any string that `strtotime()`_ accepts is + valid if you don't specify an optional + parameter that matches a date format. + **So it is usually necessary to specify + the parameter.** +valid_email No Fails if field does not contain a valid + email address. +valid_emails No Fails if any value provided in a comma + separated list is not a valid email. +valid_ip Yes Fails if the supplied IP is not valid. ``valid_ip[ipv6]`` + Accepts an optional parameter of ``ipv4`` or + ``ipv6`` to specify an IP format. +valid_json No Fails if field does not contain a valid JSON + string. +valid_url No Fails if field does not contain (loosely) a + URL. Includes simple strings that could be + hostnames, like "codeigniter". + **Normally,** ``valid_url_strict`` **should + be used.** +valid_url_strict Yes Fails if field does not contain a valid URL. ``valid_url_strict[https]`` + You can optionally specify a list of valid + schemas. If not specified, ``http,https`` + are valid. This rule uses PHP's + ``FILTER_VALIDATE_URL``. ======================= ========== ============================================= =================================================== .. note:: You can also use any native PHP functions that return boolean and permit at least one parameter, the field data to validate. - The Validation library **never alters the data** to validate. + +.. important:: The Validation library **never alters the data** to validate. .. _timezone_identifiers_list(): https://www.php.net/manual/en/function.timezone-identifiers-list.php .. _strtotime(): https://www.php.net/manual/en/function.strtotime.php diff --git a/utils/composer.json b/utils/composer.json new file mode 100644 index 000000000000..65137621e0ba --- /dev/null +++ b/utils/composer.json @@ -0,0 +1,24 @@ +{ + "require": { + "php": "^8.1", + "codeigniter/coding-standard": "^1.7", + "ergebnis/composer-normalize": "^2.28", + "friendsofphp/php-cs-fixer": "^3.47.1", + "nexusphp/cs-config": "^3.6", + "phpmetrics/phpmetrics": "^2.8 || ^3.0rc6", + "vimeo/psalm": "^5.0" + }, + "autoload": { + "psr-4": { + "Utils\\": "src/" + } + }, + "config": { + "allow-plugins": { + "ergebnis/composer-normalize": true + }, + "optimize-autoloader": true, + "preferred-install": "dist", + "sort-packages": true + } +} diff --git a/utils/PHPStan/CheckUseStatementsAfterLicenseRule.php b/utils/src/PHPStan/CheckUseStatementsAfterLicenseRule.php similarity index 79% rename from utils/PHPStan/CheckUseStatementsAfterLicenseRule.php rename to utils/src/PHPStan/CheckUseStatementsAfterLicenseRule.php index a3ccaf215d6e..4b23d3204ca1 100644 --- a/utils/PHPStan/CheckUseStatementsAfterLicenseRule.php +++ b/utils/src/PHPStan/CheckUseStatementsAfterLicenseRule.php @@ -18,8 +18,13 @@ use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\Use_; use PHPStan\Analyser\Scope; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; +/** + * @implements Rule + */ final class CheckUseStatementsAfterLicenseRule implements Rule { private const ERROR_MESSAGE = 'Use statement must be located after license docblock'; @@ -32,6 +37,8 @@ public function getNodeType(): string /** * @param Stmt $node + * + * @return list */ public function processNode(Node $node, Scope $scope): array { @@ -54,7 +61,11 @@ public function processNode(Node $node, Scope $scope): array while ($previous) { if ($previous instanceof Use_) { - return [self::ERROR_MESSAGE]; + return [ + RuleErrorBuilder::message(self::ERROR_MESSAGE) + ->identifier('codeigniter.useStmtAfterLicense') + ->build(), + ]; } $previous = $previous->getAttribute('previous'); diff --git a/utils/Rector/PassStrictParameterToFunctionParameterRector.php b/utils/src/Rector/PassStrictParameterToFunctionParameterRector.php similarity index 100% rename from utils/Rector/PassStrictParameterToFunctionParameterRector.php rename to utils/src/Rector/PassStrictParameterToFunctionParameterRector.php diff --git a/utils/Rector/RemoveErrorSuppressInTryCatchStmtsRector.php b/utils/src/Rector/RemoveErrorSuppressInTryCatchStmtsRector.php similarity index 100% rename from utils/Rector/RemoveErrorSuppressInTryCatchStmtsRector.php rename to utils/src/Rector/RemoveErrorSuppressInTryCatchStmtsRector.php diff --git a/utils/Rector/UnderscoreToCamelCaseVariableNameRector.php b/utils/src/Rector/UnderscoreToCamelCaseVariableNameRector.php similarity index 100% rename from utils/Rector/UnderscoreToCamelCaseVariableNameRector.php rename to utils/src/Rector/UnderscoreToCamelCaseVariableNameRector.php