diff --git a/.github/workflows/test-rector.yml b/.github/workflows/test-rector.yml index bf4146edd3df..9db9ea63af58 100644 --- a/.github/workflows/test-rector.yml +++ b/.github/workflows/test-rector.yml @@ -14,6 +14,7 @@ on: - 'utils/**.php' - '.github/workflows/test-rector.yml' - composer.json + - rector.php push: branches: @@ -26,6 +27,7 @@ on: - 'utils/**.php' - '.github/workflows/test-rector.yml' - composer.json + - rector.php jobs: build: diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index a0502a6d9cae..0b83b6051d99 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -14,7 +14,6 @@ use CodeIgniter\CodingStandard\CodeIgniter4; use Nexus\CsConfig\Factory; use Nexus\CsConfig\Fixer\Comment\NoCodeSeparatorCommentFixer; -use Nexus\CsConfig\Fixer\Comment\SpaceAfterCommentStartFixer; use Nexus\CsConfig\FixerGenerator; use PhpCsFixer\Finder; @@ -25,7 +24,11 @@ __DIR__ . '/tests', __DIR__ . '/utils', ]) - ->exclude(['ThirdParty']) + ->exclude([ + 'Pager/Views', + 'ThirdParty', + 'Validation/Views', + ]) ->notName('#Foobar.php$#') ->append([ __FILE__, @@ -35,7 +38,44 @@ __DIR__ . '/spark', ]); -$overrides = []; +$overrides = [ + // <<<<<<<<<<<<<<<<<<<<<<<< @TODO TO BE REMOVED ONCE LIVE IN CODING-STANDARD + 'blank_line_between_import_groups' => true, + 'class_definition' => [ + 'multi_line_extends_each_single_line' => true, + 'single_item_single_line' => true, + 'single_line' => true, + 'space_before_parenthesis' => true, + 'inline_constructor_arguments' => true, + ], + 'control_structure_braces' => true, + 'no_multiple_statements_per_line' => true, + 'no_trailing_comma_in_singleline' => [ + 'elements' => [ + 'arguments', + 'array_destructuring', + 'array', + 'group_import', + ], + ], + 'no_useless_nullsafe_operator' => true, + 'phpdoc_separation' => [ + 'groups' => [ + ['immutable', 'psalm-immutable'], + ['param', 'phpstan-param', 'psalm-param'], + ['phpstan-pure', 'psalm-pure'], + ['readonly', 'psalm-readonly'], + ['return', 'phpstan-return', 'psalm-return'], + ['template', 'phpstan-template', 'psalm-template'], + ['template-covariant', 'phpstan-template-covariant', 'psalm-template-covariant'], + ['phpstan-type', 'psalm-type'], + ['var', 'phpstan-var', 'psalm-var'], + ], + ], + 'single_line_comment_spacing' => true, + 'statement_indentation' => true, + // >>>>>>>>>>>>>>>>>>>>>>>>> +]; $options = [ 'cacheFile' => 'build/.php-cs-fixer.cache', @@ -43,7 +83,6 @@ 'customFixers' => FixerGenerator::create('vendor/nexusphp/cs-config/src/Fixer', 'Nexus\\CsConfig\\Fixer'), 'customRules' => [ NoCodeSeparatorCommentFixer::name() => true, - SpaceAfterCommentStartFixer::name() => true, ], ]; diff --git a/.php-cs-fixer.no-header.php b/.php-cs-fixer.no-header.php index 6c136cd4b164..8e9f3351953b 100644 --- a/.php-cs-fixer.no-header.php +++ b/.php-cs-fixer.no-header.php @@ -14,7 +14,6 @@ use CodeIgniter\CodingStandard\CodeIgniter4; use Nexus\CsConfig\Factory; use Nexus\CsConfig\Fixer\Comment\NoCodeSeparatorCommentFixer; -use Nexus\CsConfig\Fixer\Comment\SpaceAfterCommentStartFixer; use Nexus\CsConfig\FixerGenerator; use PhpCsFixer\Finder; @@ -25,12 +24,50 @@ __DIR__ . '/app', __DIR__ . '/public', ]) + ->exclude(['Views/errors/html']) ->notName('#Logger\.php$#') ->append([ __DIR__ . '/admin/starter/builds', ]); -$overrides = []; +$overrides = [ + // <<<<<<<<<<<<<<<<<<<<<<<< @TODO TO BE REMOVED ONCE LIVE IN CODING-STANDARD + 'blank_line_between_import_groups' => true, + 'class_definition' => [ + 'multi_line_extends_each_single_line' => true, + 'single_item_single_line' => true, + 'single_line' => true, + 'space_before_parenthesis' => true, + 'inline_constructor_arguments' => true, + ], + 'control_structure_braces' => true, + 'no_multiple_statements_per_line' => true, + 'no_trailing_comma_in_singleline' => [ + 'elements' => [ + 'arguments', + 'array_destructuring', + 'array', + 'group_import', + ], + ], + 'no_useless_nullsafe_operator' => true, + 'phpdoc_separation' => [ + 'groups' => [ + ['immutable', 'psalm-immutable'], + ['param', 'phpstan-param', 'psalm-param'], + ['phpstan-pure', 'psalm-pure'], + ['readonly', 'psalm-readonly'], + ['return', 'phpstan-return', 'psalm-return'], + ['template', 'phpstan-template', 'psalm-template'], + ['template-covariant', 'phpstan-template-covariant', 'psalm-template-covariant'], + ['phpstan-type', 'psalm-type'], + ['var', 'phpstan-var', 'psalm-var'], + ], + ], + 'single_line_comment_spacing' => true, + 'statement_indentation' => true, + // >>>>>>>>>>>>>>>>>>>>>>>>> +]; $options = [ 'cacheFile' => 'build/.php-cs-fixer.no-header.cache', @@ -38,7 +75,6 @@ 'customFixers' => FixerGenerator::create('vendor/nexusphp/cs-config/src/Fixer', 'Nexus\\CsConfig\\Fixer'), 'customRules' => [ NoCodeSeparatorCommentFixer::name() => true, - SpaceAfterCommentStartFixer::name() => true, ], ]; diff --git a/.php-cs-fixer.user-guide.php b/.php-cs-fixer.user-guide.php index 8081d73698ca..be38def156f6 100644 --- a/.php-cs-fixer.user-guide.php +++ b/.php-cs-fixer.user-guide.php @@ -14,7 +14,6 @@ use CodeIgniter\CodingStandard\CodeIgniter4; use Nexus\CsConfig\Factory; use Nexus\CsConfig\Fixer\Comment\NoCodeSeparatorCommentFixer; -use Nexus\CsConfig\Fixer\Comment\SpaceAfterCommentStartFixer; use Nexus\CsConfig\FixerGenerator; use PhpCsFixer\Finder; @@ -34,6 +33,42 @@ 'php_unit_internal_class' => false, 'no_unused_imports' => false, 'class_attributes_separation' => false, + // <<<<<<<<<<<<<<<<<<<<<<<< @TODO TO BE REMOVED ONCE LIVE IN CODING-STANDARD + 'blank_line_between_import_groups' => true, + 'class_definition' => [ + 'multi_line_extends_each_single_line' => true, + 'single_item_single_line' => true, + 'single_line' => true, + 'space_before_parenthesis' => true, + 'inline_constructor_arguments' => true, + ], + 'control_structure_braces' => true, + 'no_multiple_statements_per_line' => true, + 'no_trailing_comma_in_singleline' => [ + 'elements' => [ + 'arguments', + 'array_destructuring', + 'array', + 'group_import', + ], + ], + 'no_useless_nullsafe_operator' => true, + 'phpdoc_separation' => [ + 'groups' => [ + ['immutable', 'psalm-immutable'], + ['param', 'phpstan-param', 'psalm-param'], + ['phpstan-pure', 'psalm-pure'], + ['readonly', 'psalm-readonly'], + ['return', 'phpstan-return', 'psalm-return'], + ['template', 'phpstan-template', 'psalm-template'], + ['template-covariant', 'phpstan-template-covariant', 'psalm-template-covariant'], + ['phpstan-type', 'psalm-type'], + ['var', 'phpstan-var', 'psalm-var'], + ], + ], + 'single_line_comment_spacing' => true, + 'statement_indentation' => true, + // >>>>>>>>>>>>>>>>>>>>>>>>> ]; $options = [ @@ -42,7 +77,6 @@ 'customFixers' => FixerGenerator::create('vendor/nexusphp/cs-config/src/Fixer', 'Nexus\\CsConfig\\Fixer'), 'customRules' => [ NoCodeSeparatorCommentFixer::name() => true, - SpaceAfterCommentStartFixer::name() => true, ], ]; diff --git a/CHANGELOG.md b/CHANGELOG.md index 174689cf5a2e..569960c8842d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,47 @@ # Changelog +## [v4.2.6](https://github.com/codeigniter4/CodeIgniter4/tree/v4.2.6) (2022-09-04) +[Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/v4.2.5...v4.2.6) + +### Fixed Bugs +* fix: AssertionError occurs when using Validation in CLI by @daycry in https://github.com/codeigniter4/CodeIgniter4/pull/6452 +* fix: [Validation] JSON data may cause "Array to string conversion" error by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6467 +* Fix fatal error gets turned to `0` severity on shutdown handler by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/6472 +* Fix redis cache increment/decrement methods by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/6473 +* Fix broken caching system when array of allowed parameters used by @JavaDeveloperKiev in https://github.com/codeigniter4/CodeIgniter4/pull/6475 +* fix: Strict Validation Rules greater_than/less_than by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6492 + +### Refactoring +* refactor: fix PHPStan errors by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6470 +* Bump `friendsofphp/php-cs-fixer` to `~3.11.0` by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/6471 +* Fix overlooked coding style violations by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/6491 + +## [v4.2.5](https://github.com/codeigniter4/CodeIgniter4/tree/v4.2.5) (2022-08-28) +[Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/v4.2.4...v4.2.5) + +### Breaking Changes +* Add $cached param to BaseConnection::tableExists() by @sclubricants in https://github.com/codeigniter4/CodeIgniter4/pull/6364 +* Fix validation custom error asterisk field by @ping-yee in https://github.com/codeigniter4/CodeIgniter4/pull/6378 + +### Fixed Bugs +* fix: Email class may not log an error when it fails to send by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6362 +* fix: Response::download() causes TypeError by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6361 +* fix: command usages by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6402 +* Fix: The subquery adds a prefix for the table alias. by @iRedds in https://github.com/codeigniter4/CodeIgniter4/pull/6390 +* Fix Sqlite Table::createTable() by @sclubricants in https://github.com/codeigniter4/CodeIgniter4/pull/6396 +* docs: add missing `@method` `groupBy()` in Model by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6433 +* fix: CLIRequest Erros in CLI by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6421 +* fix: Call to undefined method CodeIgniter\HTTP\CLIRequest::getLocale() by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6442 + +### Enhancements +* chore: update Kint to 4.2.0 by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6436 + +### Refactoring +* refactor: add test for DownloadResponse by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6375 +* refactor: ValidationTest by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6382 +* refactor: remove unused `_parent_name` in BaseBuilder::objectToArray() by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6427 +* Remove unneeded abstract `handle()` method by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/6434 + ## [v4.2.4](https://github.com/codeigniter4/CodeIgniter4/tree/v4.2.4) (2022-08-13) [Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/v4.2.3...v4.2.4) diff --git a/admin/framework/composer.json b/admin/framework/composer.json index 95ea60802e1c..7532fd47303f 100644 --- a/admin/framework/composer.json +++ b/admin/framework/composer.json @@ -10,21 +10,32 @@ "ext-intl": "*", "ext-json": "*", "ext-mbstring": "*", - "kint-php/kint": "^4.1.1", "laminas/laminas-escaper": "^2.9", "psr/log": "^1.1" }, "require-dev": { + "kint-php/kint": "^4.2", "codeigniter/coding-standard": "^1.1", "fakerphp/faker": "^1.9", - "friendsofphp/php-cs-fixer": "3.6.*", + "friendsofphp/php-cs-fixer": "~3.11.0", "mikey179/vfsstream": "^1.6", "nexusphp/cs-config": "^3.3", "phpunit/phpunit": "^9.1", "predis/predis": "^1.1 || ^2.0" }, "suggest": { - "ext-fileinfo": "Improves mime type detection for files" + "ext-imagick": "If you use Image class ImageMagickHandler", + "ext-simplexml": "If you format XML", + "ext-mysqli": "If you use MySQL", + "ext-oci8": "If you use Oracle Database", + "ext-pgsql": "If you use PostgreSQL", + "ext-sqlsrv": "If you use SQL Server", + "ext-sqlite3": "If you use SQLite3", + "ext-memcache": "If you use Cache class MemcachedHandler with Memcache", + "ext-memcached": "If you use Cache class MemcachedHandler with Memcached", + "ext-redis": "If you use Cache class RedisHandler", + "ext-fileinfo": "Improves mime type detection for files", + "ext-readline": "Improves CLI::input() usability" }, "autoload": { "psr-4": { diff --git a/app/Config/App.php b/app/Config/App.php index 47bd6c2dd4b1..b97dfede2550 100644 --- a/app/Config/App.php +++ b/app/Config/App.php @@ -369,7 +369,7 @@ class App extends BaseConfig * * @deprecated Use `Config\Security` $redirect property instead of using this property. */ - public bool $CSRFRedirect = true; + public bool $CSRFRedirect = false; /** * -------------------------------------------------------------------------- @@ -385,6 +385,7 @@ class App extends BaseConfig * Defaults to `Lax` as recommended in this link: * * @see https://portswigger.net/web-security/csrf/samesite-cookies + * * @deprecated `Config\Cookie` $samesite property is used. */ public string $CSRFSameSite = 'Lax'; diff --git a/app/Config/Autoload.php b/app/Config/Autoload.php index ee27e3b2ce18..914377aad9dd 100644 --- a/app/Config/Autoload.php +++ b/app/Config/Autoload.php @@ -31,12 +31,10 @@ class Autoload extends AutoloadConfig * else you will need to modify all of those classes for this to work. * * Prototype: - *``` * $psr4 = [ * 'CodeIgniter' => SYSTEMPATH, - * 'App' => APPPATH + * 'App' => APPPATH * ]; - *``` * * @var array */ @@ -56,11 +54,9 @@ class Autoload extends AutoloadConfig * were being autoloaded through a namespace. * * Prototype: - *``` * $classmap = [ * 'MyClass' => '/path/to/class/file.php' * ]; - *``` * * @var array */ @@ -75,13 +71,26 @@ class Autoload extends AutoloadConfig * or for loading functions. * * Prototype: - * ``` - * $files = [ - * '/path/to/my/file.php', - * ]; - * ``` + * $files = [ + * '/path/to/my/file.php', + * ]; * - * @var array + * @var string[] + * @phpstan-var list */ public $files = []; + + /** + * ------------------------------------------------------------------- + * Helpers + * ------------------------------------------------------------------- + * Prototype: + * $helpers = [ + * 'form', + * ]; + * + * @var string[] + * @phpstan-var list + */ + public $helpers = []; } diff --git a/app/Config/ContentSecurityPolicy.php b/app/Config/ContentSecurityPolicy.php index 6558a6a47eda..18612e15cc37 100644 --- a/app/Config/ContentSecurityPolicy.php +++ b/app/Config/ContentSecurityPolicy.php @@ -15,9 +15,10 @@ */ class ContentSecurityPolicy extends BaseConfig { - //------------------------------------------------------------------------- + // ------------------------------------------------------------------------- // Broadbrush CSP management - //------------------------------------------------------------------------- + // ------------------------------------------------------------------------- + /** * Default CSP report context */ @@ -36,10 +37,10 @@ class ContentSecurityPolicy extends BaseConfig */ public bool $upgradeInsecureRequests = false; - //------------------------------------------------------------------------- + // ------------------------------------------------------------------------- // Sources allowed // Note: once you set a policy to 'none', it cannot be further restricted - //------------------------------------------------------------------------- + // ------------------------------------------------------------------------- /** * Will default to self if not overridden diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 71b7a86de469..c251ec22c4b4 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -19,7 +19,7 @@ // where controller filters or CSRF protection are bypassed. // If you don't want to define all routes, please use the Auto Routing (Improved). // Set `$autoRoutesImproved` to true in `app/Config/Feature.php` and set the following to true. -//$routes->setAutoRoute(false); +// $routes->setAutoRoute(false); /* * -------------------------------------------------------------------- diff --git a/app/Config/Security.php b/app/Config/Security.php index 1474404be9e4..57be4ee41541 100644 --- a/app/Config/Security.php +++ b/app/Config/Security.php @@ -80,7 +80,7 @@ class Security extends BaseConfig * * Redirect to previous page with error on failure. */ - public bool $redirect = true; + public bool $redirect = false; /** * -------------------------------------------------------------------------- @@ -94,6 +94,7 @@ class Security extends BaseConfig * Defaults to `Lax` as recommended in this link: * * @see https://portswigger.net/web-security/csrf/samesite-cookies + * * @deprecated `Config\Cookie` $samesite property is used. */ public string $samesite = 'Lax'; diff --git a/app/Config/Validation.php b/app/Config/Validation.php index 373615cfb16b..f7d1b9f49f5f 100644 --- a/app/Config/Validation.php +++ b/app/Config/Validation.php @@ -10,9 +10,9 @@ class Validation extends BaseConfig { - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Setup - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Stores the classes that contain the @@ -38,7 +38,7 @@ class Validation extends BaseConfig 'single' => 'CodeIgniter\Validation\Views\single', ]; - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Rules - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- } diff --git a/composer.json b/composer.json index 080fc5974605..105c07acc662 100644 --- a/composer.json +++ b/composer.json @@ -10,21 +10,22 @@ "ext-intl": "*", "ext-json": "*", "ext-mbstring": "*", - "kint-php/kint": "^4.1.1", "laminas/laminas-escaper": "^2.9", "psr/log": "^1.1" }, "require-dev": { + "kint-php/kint": "^4.2", "codeigniter/coding-standard": "^1.1", "fakerphp/faker": "^1.9", - "friendsofphp/php-cs-fixer": "3.6.*", + "friendsofphp/php-cs-fixer": "~3.11.0", "mikey179/vfsstream": "^1.6", "nexusphp/cs-config": "^3.3", "nexusphp/tachycardia": "^1.0", "phpstan/phpstan": "^1.7.1", "phpunit/phpunit": "^9.1", "predis/predis": "^1.1 || ^2.0", - "rector/rector": "0.14.0" + "rector/rector": "0.14.1", + "vimeo/psalm": "^4.26" }, "suggest": { "ext-imagick": "If you use Image class ImageMagickHandler", @@ -70,7 +71,11 @@ "CodeIgniter\\ComposerScripts::postUpdate", "bash -c \"if [ -f admin/setup.sh ]; then bash admin/setup.sh; fi\"" ], - "analyze": "phpstan analyse", + "analyze": [ + "phpstan analyze", + "rector process --dry-run" + ], + "sa": "@analyze", "test": "phpunit", "cs": [ "php-cs-fixer fix --ansi --verbose --dry-run --diff --config=.php-cs-fixer.user-guide.php", @@ -81,7 +86,8 @@ "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" - ] + ], + "style": "@cs-fix" }, "scripts-descriptions": { "analyze": "Run static analysis", diff --git a/contributing/pull_request.md b/contributing/pull_request.md index 628d5593da5a..f1e29304e305 100644 --- a/contributing/pull_request.md +++ b/contributing/pull_request.md @@ -249,18 +249,16 @@ The best way to contribute is to fork the CodeIgniter4 repository, and "clone" t 7. Fix existing bugs on the [Issue tracker](https://github.com/codeigniter4/CodeIgniter4/issues) after confirming that no one else is working on them. 8. [Commit](https://help.github.com/en/desktop/contributing-to-projects/committing-and-reviewing-changes-to-your-project) the changed files in your contribution branch. - `> git commit` - - Commit messages are expected to be descriptive of what you changed specifically. Commit messages like "Fixes #1234" would be asked by the reviewer to be revised. -9. If there are intermediate commits that are not meaningful to the overall PR, such as "Fixed error on style guide", "Fixed phpstan error", "Fixing mistake in code", and other related commits, it is advised to squash your commits so that we can have a clean commit history. -10. If you have touched PHP code, run static analysis. + - Commit messages are expected to be descriptive of why and what you changed specifically. Commit messages like "Fixes #1234" would be asked by the reviewer to be revised. [Atomic commit](https://en.wikipedia.org/wiki/Atomic_commit#Atomic_commit_convention) is recommended. See [Contribution Workflow](./workflow.md#commit-messages) for details. +9. If you have touched PHP code, run static analysis. - `> composer analyze` - - `> vendor/bin/rector process` -11. Run unit tests on the specific file you modified. If there are no existing tests yet, please create one. +10. Run unit tests on the specific file you modified. If there are no existing tests yet, please create one. - `> vendor/bin/phpunit tests/system/path/to/file/you/modified` - Make sure the tests pass to have a higher chance of merging. -12. [Push](https://docs.github.com/en/github/using-git/pushing-commits-to-a-remote-repository) your contribution branch to your fork. +11. [Push](https://docs.github.com/en/github/using-git/pushing-commits-to-a-remote-repository) your contribution branch to your fork. - `> git push origin ` -13. Send a [pull request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork). -14. Label your pull request with the appropriate label if you can. +12. Send a [pull request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork). +13. Label your pull request with the appropriate label if you can. See [Contribution workflow](./workflow.md) for Git workflow details. diff --git a/contributing/signing.md b/contributing/signing.md index ca84ef79a9a7..d312d3fc217f 100644 --- a/contributing/signing.md +++ b/contributing/signing.md @@ -52,12 +52,4 @@ bash shell to use the **-S** option to force the secure signing. ## Commit Messages Regardless of how you sign a commit, commit messages are important too. -They communicate the intent of a specific change, concisely. They make -it easier to review code, and to find out why a change was made if the -code history is examined later. - -The audience for your commit messages will be the codebase maintainers, -any code reviewers, and debuggers trying to figure out when a bug might -have been introduced. - -Make your commit messages meaningful. +See [Contribution Workflow](./workflow.md#commit-messages) for details. diff --git a/contributing/workflow.md b/contributing/workflow.md index 03e567d7fe29..cde1f2eb5de6 100644 --- a/contributing/workflow.md +++ b/contributing/workflow.md @@ -66,7 +66,7 @@ Clone your repository, leaving a local folder for you to work with: > git clone ORIGIN_URL ``` -## Syncing your repository +## Syncing Your Repository Within your local repository, Git will have created an alias, **origin**, for the GitHub repository it is bound to. You want to create @@ -83,8 +83,8 @@ is normally done locally, so that you can resolve any merge conflicts. For instance, to synchronize **develop** branches: ```console -> git switch develop > git fetch upstream +> git switch develop > git merge upstream/develop > git push origin develop ``` @@ -139,16 +139,52 @@ Your local changes need to be *committed* to save them in your local repository. This is where [contribution signing](./signing.md) comes in. +Now we don't have detailed rules on commits and its messages. But +[atomic commit](https://en.wikipedia.org/wiki/Atomic_commit#Atomic_commit_convention) is recommended. +Keep your commits atomic. One commit for one change. + +There are some references for writing good commit messages: + +- [Git Best Practices — AFTER Technique - DZone DevOps](https://dzone.com/articles/git-best-practices-after-technique-1) +- [Semantic Commit Messages](https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716) +- [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) + +If there are intermediate commits that are not meaningful to the overall PR, +such as "Fix error on style guide", "Fix phpstan error", "Fix mistake in code", +and other related commits, you can squash your commits so that we can have a clean commit history. +But it is not a must. + +### Commit Messages + +Commit messages are important. They communicate the intent of a specific change, concisely. +They make it easier to review code, and to find out why a change was made +if the code history is examined later. + +The audience for your commit messages will be the codebase maintainers, +any code reviewers, and debuggers trying to figure out when a bug might +have been introduced. + +Make your commit messages meaningful. + +Commit messages are expected to be descriptive of **why** and what you changed specifically. +Commit messages like "Fixes #1234" would be asked by the reviewer to be revised. + You can have as many commits in a branch as you need to "get it right". For instance, to commit your work from a debugging session: ```console > git add . -> git commit -S -m "Find and fix the broken reference problem" +> git commit -S -m "Fix the broken reference problem" ``` Just make sure that your commits in a feature branch are all related. +### Changing a Commit Message + +See . + +### When You Work on Two Features + If you are working on two features at a time, then you will want to switch between them to keep the contributions separate. For instance: @@ -164,7 +200,7 @@ switch between them to keep the contributions separate. For instance: > git switch develop ``` -The last checkout makes sure that you end up in your *develop* branch as +The last switch makes sure that you end up in your *develop* branch as a starting point for your next session working with your repository. This is a good practice, as it is not always obvious which branch you are working in. @@ -181,8 +217,8 @@ It is a lot easier to resolve conflicts at this stage. Synchronize your repository: ```console -> git switch develop > git fetch upstream +> git switch develop > git merge upstream/develop > git push origin develop ``` @@ -253,8 +289,8 @@ do the following: Synchronize your repository: ```console -> git switch develop > git fetch upstream +> git switch develop > git merge upstream/develop > git push origin develop ``` @@ -282,7 +318,7 @@ And finally push your local branch to your GitHub repository: > git push --force-with-lease origin fix/problem123 ``` -## If you sent to the wrong branch +## If You Sent to the Wrong Branch If you have sent a PR to the wrong branch, you need to create a new PR branch. @@ -298,8 +334,8 @@ Copy the IDs of any commits you made that you want to keep: Update your `4.3` branch: ```console -> git switch 4.3 > git fetch upstream +> git switch 4.3 > git merge upstream/4.3 > git push origin 4.3 ``` diff --git a/env b/env index cc0681c67711..fc796609639f 100644 --- a/env +++ b/env @@ -48,7 +48,7 @@ # database.default.port = 3306 # database.tests.hostname = localhost -# database.tests.database = ci4 +# database.tests.database = ci4_test # database.tests.username = root # database.tests.password = root # database.tests.DBDriver = MySQLi @@ -124,7 +124,7 @@ # security.cookieName = 'csrf_cookie_name' # security.expires = 7200 # security.regenerate = true -# security.redirect = true +# security.redirect = false # security.samesite = 'Lax' #-------------------------------------------------------------------- diff --git a/phpstan-baseline.neon.dist b/phpstan-baseline.neon.dist index 4540e21e703e..1a696ccc5422 100644 --- a/phpstan-baseline.neon.dist +++ b/phpstan-baseline.neon.dist @@ -35,16 +35,6 @@ parameters: count: 1 path: system/Cache/Handlers/FileHandler.php - - - message: "#^Method MemcachePool\\:\\:decrement\\(\\) invoked with 4 parameters, 1\\-2 required\\.$#" - count: 1 - path: system/Cache/Handlers/MemcachedHandler.php - - - - message: "#^Method MemcachePool\\:\\:increment\\(\\) invoked with 4 parameters, 1\\-2 required\\.$#" - count: 1 - path: system/Cache/Handlers/MemcachedHandler.php - - message: "#^Unreachable statement \\- code above always terminates\\.$#" count: 1 @@ -470,11 +460,6 @@ parameters: count: 1 path: system/HTTP/Request.php - - - message: "#^Cannot unset offset 'path' on array{host: non-empty-string}\\.$#" - count: 1 - path: system/HTTP/URI.php - - message: "#^Property CodeIgniter\\\\HTTP\\\\URI\\:\\:\\$fragment \\(string\\) on left side of \\?\\? is not nullable\\.$#" count: 1 @@ -505,11 +490,6 @@ parameters: count: 1 path: system/Helpers/number_helper.php - - - message: "#^Variable \\$pool might not be defined\\.$#" - count: 2 - path: system/Helpers/text_helper.php - - message: "#^Variable \\$count might not be defined\\.$#" count: 1 @@ -655,11 +635,6 @@ parameters: count: 1 path: system/Throttle/Throttler.php - - - message: "#^Property CodeIgniter\\\\Validation\\\\Validation\\:\\:\\$errors \\(array\\) on left side of \\?\\? is not nullable\\.$#" - count: 1 - path: system/Validation/Validation.php - - message: "#^Variable \\$error on left side of \\?\\? always exists and is always null\\.$#" count: 1 diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 000000000000..33137421006b --- /dev/null +++ b/psalm.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + diff --git a/psalm_autoload.php b/psalm_autoload.php new file mode 100644 index 000000000000..3adf90abd9e4 --- /dev/null +++ b/psalm_autoload.php @@ -0,0 +1,24 @@ + [ - __DIR__ . '/system/CodeIgniter.php', __DIR__ . '/system/Autoloader/Autoloader.php', ], diff --git a/system/API/ResponseTrait.php b/system/API/ResponseTrait.php index 5f6a52c497e5..6ae0e3a361bd 100644 --- a/system/API/ResponseTrait.php +++ b/system/API/ResponseTrait.php @@ -136,9 +136,9 @@ protected function fail($messages, int $status = 400, ?string $code = null, stri return $this->respond($response, $status, $customMessage); } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Response Helpers - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Used after successfully creating a new resource. @@ -290,9 +290,9 @@ protected function failServerError(string $description = 'Internal Server Error' return $this->fail($description, $this->codes['server_error'], $code, $message); } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Utility Methods - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Handles formatting a response. Currently makes some heavy assumptions diff --git a/system/Autoloader/Autoloader.php b/system/Autoloader/Autoloader.php index 9f02ea5327a0..053756417279 100644 --- a/system/Autoloader/Autoloader.php +++ b/system/Autoloader/Autoloader.php @@ -71,10 +71,20 @@ class Autoloader /** * Stores files as a list. * - * @var array + * @var string[] + * @phpstan-var list */ protected $files = []; + /** + * Stores helper list. + * Always load the URL helper, it should be used in most apps. + * + * @var string[] + * @phpstan-var list + */ + protected $helpers = ['url']; + /** * Reads in the configuration array (described above) and stores * the valid parts that we'll need. @@ -105,6 +115,10 @@ public function initialize(Autoload $config, Modules $modules) $this->files = $config->files; } + if (isset($config->helpers)) { // @phpstan-ignore-line + $this->helpers = [...$this->helpers, ...$config->helpers]; + } + if (is_file(COMPOSER_PATH)) { $this->loadComposerInfo($modules); } @@ -146,6 +160,17 @@ public function register() } } + /** + * Unregister autoloader. + * + * This method is for testing. + */ + public function unregister(): void + { + spl_autoload_unregister([$this, 'loadClass']); + spl_autoload_unregister([$this, 'loadClassmap']); + } + /** * Registers namespaces with the autoloader. * @@ -396,4 +421,12 @@ protected function discoverComposerNamespaces() $this->prefixes = array_merge($this->prefixes, $newPaths); $this->classmap = array_merge($this->classmap, $classes); } + + /** + * Loads helpers + */ + public function loadHelpers(): void + { + helper($this->helpers); + } } diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index 4b86d19c3f54..5279bcf374db 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -362,9 +362,9 @@ public static function promptByMultipleKeys(string $text, array $options): array return $input; } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Utility for promptBy... - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Validation for $options in promptByKey() and promptByMultipleKeys(). Return an error if $options is an empty array. @@ -390,9 +390,9 @@ private static function printKeysAndValues(array $options): void } } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // End Utility for promptBy... - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Validate one prompt "field" at a time @@ -849,9 +849,9 @@ public static function wrap(?string $string = null, int $max = 0, int $padLeft = return $lines; } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Command-Line 'URI' support - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Parses the command line it was called from and collects all diff --git a/system/Cache/Handlers/RedisHandler.php b/system/Cache/Handlers/RedisHandler.php index df179e7455e6..b84c0115b1f8 100644 --- a/system/Cache/Handlers/RedisHandler.php +++ b/system/Cache/Handlers/RedisHandler.php @@ -200,7 +200,7 @@ public function increment(string $key, int $offset = 1) { $key = static::validateKey($key, $this->prefix); - return $this->redis->hIncrBy($key, 'data', $offset); + return $this->redis->hIncrBy($key, '__ci_value', $offset); } /** @@ -208,9 +208,7 @@ public function increment(string $key, int $offset = 1) */ public function decrement(string $key, int $offset = 1) { - $key = static::validateKey($key, $this->prefix); - - return $this->redis->hIncrBy($key, 'data', -$offset); + return $this->increment($key, -$offset); } /** diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 02019676a980..4e426a8e1e9a 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -47,7 +47,7 @@ class CodeIgniter /** * The current version of CodeIgniter Framework */ - public const CI_VERSION = '4.2.4'; + public const CI_VERSION = '4.2.6'; /** * App startup time. @@ -183,10 +183,6 @@ public function initialize() date_default_timezone_set($this->config->appTimezone ?? 'UTC'); $this->initializeKint(); - - if (! CI_DEBUG) { - Kint::$enabled_mode = false; // @codeCoverageIgnore - } } /** @@ -223,6 +219,20 @@ protected function resolvePlatformExtensions() * Initializes Kint */ protected function initializeKint() + { + if (CI_DEBUG) { + $this->autoloadKint(); + $this->configureKint(); + } elseif (class_exists(Kint::class)) { + // In case that Kint is already loaded via Composer. + Kint::$enabled_mode = false; + // @codeCoverageIgnore + } + + helper('kint'); + } + + private function autoloadKint(): void { // If we have KINT_DIR it means it's already loaded via composer if (! defined('KINT_DIR')) { @@ -242,7 +252,10 @@ protected function initializeKint() require_once SYSTEMPATH . 'ThirdParty/Kint/init.php'; } + } + private function configureKint(): void + { /** @var \Config\Kint $config */ $config = config(KintConfig::class); @@ -706,9 +719,12 @@ protected function generateCacheName(Cache $config): string } $uri = $this->request->getUri(); - if ($config->cacheQueryString) { - $name = URI::createURIString($uri->getScheme(), $uri->getAuthority(), $uri->getPath(), $uri->getQuery()); + if (is_array($config->cacheQueryString)) { + $name = URI::createURIString($uri->getScheme(), $uri->getAuthority(), $uri->getPath(), $uri->getQuery(['only' => $config->cacheQueryString])); + } else { + $name = URI::createURIString($uri->getScheme(), $uri->getAuthority(), $uri->getPath(), $uri->getQuery()); + } } else { $name = URI::createURIString($uri->getScheme(), $uri->getAuthority(), $uri->getPath()); } @@ -910,11 +926,9 @@ protected function display404errors(PageNotFoundException $e) $this->response->setStatusCode($e->getCode()); if (ENVIRONMENT !== 'testing') { - // @codeCoverageIgnoreStart if (ob_get_level() > 0) { - ob_end_flush(); + ob_end_flush(); // @codeCoverageIgnore } - // @codeCoverageIgnoreEnd } // When testing, one is for phpunit, another is for test case. elseif (ob_get_level() > 2) { diff --git a/system/Commands/Cache/ClearCache.php b/system/Commands/Cache/ClearCache.php index bb98177a0c0d..648d99e42ecc 100644 --- a/system/Commands/Cache/ClearCache.php +++ b/system/Commands/Cache/ClearCache.php @@ -46,7 +46,7 @@ class ClearCache extends BaseCommand * * @var string */ - protected $usage = 'cache:clear [driver]'; + protected $usage = 'cache:clear []'; /** * the Command's Arguments diff --git a/system/Commands/Help.php b/system/Commands/Help.php index 4dbc2df6d34d..74dcbeab855a 100644 --- a/system/Commands/Help.php +++ b/system/Commands/Help.php @@ -48,7 +48,7 @@ class Help extends BaseCommand * * @var string */ - protected $usage = 'help command_name'; + protected $usage = 'help []'; /** * the Command's Arguments diff --git a/system/Common.php b/system/Common.php index 4e3a67e62dff..5dec233b6c8a 100644 --- a/system/Common.php +++ b/system/Common.php @@ -19,6 +19,7 @@ use CodeIgniter\Debug\Timer; use CodeIgniter\Files\Exceptions\FileNotFoundException; use CodeIgniter\HTTP\Exceptions\HTTPException; +use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; @@ -346,25 +347,6 @@ function db_connect($db = null, bool $getShared = true) } } -if (! function_exists('dd')) { - /** - * Prints a Kint debug report and exits. - * - * @param array ...$vars - * - * @codeCoverageIgnore Can't be tested ... exits - */ - function dd(...$vars) - { - // @codeCoverageIgnoreStart - Kint::$aliases[] = 'dd'; - Kint::dump(...$vars); - - exit; - // @codeCoverageIgnoreEnd - } -} - if (! function_exists('env')) { /** * Allows user to retrieve values from the environment @@ -487,6 +469,10 @@ function force_https(int $duration = 31_536_000, ?RequestInterface $request = nu $response = Services::response(null, true); } + if (! $request instanceof IncomingRequest) { + return; + } + if ((ENVIRONMENT !== 'testing' && (is_cli() || $request->isSecure())) || (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'test')) { // @codeCoverageIgnoreStart return; @@ -906,7 +892,8 @@ function remove_invisible_characters(string $str, bool $urlEncoded = true): stri * have a route defined in the routes Config file. * * @param string $method Named route or Controller::method - * @param int|string ...$params One or more parameters to be passed to the route + * @param int|string ...$params One or more parameters to be passed to the route. + * The last parameter allows you to set the locale. * * @return false|string */ @@ -1103,17 +1090,6 @@ function timer(?string $name = null, ?callable $callable = null) } } -if (! function_exists('trace')) { - /** - * Provides a backtrace to the current execution point, from Kint. - */ - function trace() - { - Kint::$aliases[] = 'trace'; - Kint::trace(); - } -} - if (! function_exists('view')) { /** * Grabs the current RendererInterface-compatible class diff --git a/system/ComposerScripts.php b/system/ComposerScripts.php index 62e5d828e1ce..a2727755a3dd 100644 --- a/system/ComposerScripts.php +++ b/system/ComposerScripts.php @@ -69,8 +69,14 @@ public static function postUpdate() { self::recursiveDelete(self::$path); - foreach (self::$dependencies as $dependency) { + foreach (self::$dependencies as $key => $dependency) { + // Kint may be removed. + if (! is_dir($dependency['from']) && strpos($key, 'kint') === 0) { + continue; + } + self::recursiveMirror($dependency['from'], $dependency['to']); + if (isset($dependency['license'])) { $license = basename($dependency['license']); copy($dependency['license'], $dependency['to'] . '/' . $license); diff --git a/system/Config/BaseService.php b/system/Config/BaseService.php index 5213f426bca7..07738a74e38a 100644 --- a/system/Config/BaseService.php +++ b/system/Config/BaseService.php @@ -92,43 +92,43 @@ * @see http://blog.ircmaxell.com/2015/11/simple-easy-risk-and-change.html * @see http://www.infoq.com/presentations/Simple-Made-Easy * - * @method static CacheInterface cache(Cache $config = null, $getShared = true) - * @method static CLIRequest clirequest(App $config = null, $getShared = true) - * @method static CodeIgniter codeigniter(App $config = null, $getShared = true) - * @method static Commands commands($getShared = true) - * @method static void createRequest(App $config, bool $isCli = false) - * @method static ContentSecurityPolicy csp(CSPConfig $config = null, $getShared = true) - * @method static CURLRequest curlrequest($options = [], ResponseInterface $response = null, App $config = null, $getShared = true) - * @method static Email email($config = null, $getShared = true) - * @method static EncrypterInterface encrypter(Encryption $config = null, $getShared = false) - * @method static Exceptions exceptions(ConfigExceptions $config = null, IncomingRequest $request = null, Response $response = null, $getShared = true) - * @method static Filters filters(ConfigFilters $config = null, $getShared = true) - * @method static Format format(ConfigFormat $config = null, $getShared = true) - * @method static Honeypot honeypot(ConfigHoneyPot $config = null, $getShared = true) - * @method static BaseHandler image($handler = null, Images $config = null, $getShared = true) - * @method static IncomingRequest incomingrequest(?App $config = null, bool $getShared = true) - * @method static Iterator iterator($getShared = true) - * @method static Language language($locale = null, $getShared = true) - * @method static Logger logger($getShared = true) - * @method static MigrationRunner migrations(Migrations $config = null, ConnectionInterface $db = null, $getShared = true) - * @method static Negotiate negotiator(RequestInterface $request = null, $getShared = true) - * @method static Pager pager(ConfigPager $config = null, RendererInterface $view = null, $getShared = true) - * @method static Parser parser($viewPath = null, ConfigView $config = null, $getShared = true) - * @method static RedirectResponse redirectresponse(App $config = null, $getShared = true) - * @method static View renderer($viewPath = null, ConfigView $config = null, $getShared = true) + * @method static CacheInterface cache(Cache $config = null, $getShared = true) + * @method static CLIRequest clirequest(App $config = null, $getShared = true) + * @method static CodeIgniter codeigniter(App $config = null, $getShared = true) + * @method static Commands commands($getShared = true) + * @method static void createRequest(App $config, bool $isCli = false) + * @method static ContentSecurityPolicy csp(CSPConfig $config = null, $getShared = true) + * @method static CURLRequest curlrequest($options = [], ResponseInterface $response = null, App $config = null, $getShared = true) + * @method static Email email($config = null, $getShared = true) + * @method static EncrypterInterface encrypter(Encryption $config = null, $getShared = false) + * @method static Exceptions exceptions(ConfigExceptions $config = null, IncomingRequest $request = null, Response $response = null, $getShared = true) + * @method static Filters filters(ConfigFilters $config = null, $getShared = true) + * @method static Format format(ConfigFormat $config = null, $getShared = true) + * @method static Honeypot honeypot(ConfigHoneyPot $config = null, $getShared = true) + * @method static BaseHandler image($handler = null, Images $config = null, $getShared = true) + * @method static IncomingRequest incomingrequest(?App $config = null, bool $getShared = true) + * @method static Iterator iterator($getShared = true) + * @method static Language language($locale = null, $getShared = true) + * @method static Logger logger($getShared = true) + * @method static MigrationRunner migrations(Migrations $config = null, ConnectionInterface $db = null, $getShared = true) + * @method static Negotiate negotiator(RequestInterface $request = null, $getShared = true) + * @method static Pager pager(ConfigPager $config = null, RendererInterface $view = null, $getShared = true) + * @method static Parser parser($viewPath = null, ConfigView $config = null, $getShared = true) + * @method static RedirectResponse redirectresponse(App $config = null, $getShared = true) + * @method static View renderer($viewPath = null, ConfigView $config = null, $getShared = true) * @method static IncomingRequest|CLIRequest request(App $config = null, $getShared = true) - * @method static Response response(App $config = null, $getShared = true) - * @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 Throttler throttler($getShared = true) - * @method static Timer timer($getShared = true) - * @method static Toolbar toolbar(ConfigToolbar $config = null, $getShared = true) - * @method static Typography typography($getShared = true) - * @method static URI uri($uri = null, $getShared = true) - * @method static Validation validation(ConfigValidation $config = null, $getShared = true) - * @method static Cell viewcell($getShared = true) + * @method static Response response(App $config = null, $getShared = true) + * @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 Throttler throttler($getShared = true) + * @method static Timer timer($getShared = true) + * @method static Toolbar toolbar(ConfigToolbar $config = null, $getShared = true) + * @method static Typography typography($getShared = true) + * @method static URI uri($uri = null, $getShared = true) + * @method static Validation validation(ConfigValidation $config = null, $getShared = true) + * @method static Cell viewcell($getShared = true) */ class BaseService { diff --git a/system/Cookie/CloneableCookieInterface.php b/system/Cookie/CloneableCookieInterface.php index 508c1359970e..93f6031e2010 100644 --- a/system/Cookie/CloneableCookieInterface.php +++ b/system/Cookie/CloneableCookieInterface.php @@ -60,6 +60,8 @@ public function withExpired(); * Creates a new Cookie that will virtually never expire from the browser. * * @return static + * + * @deprecated See https://github.com/codeigniter4/CodeIgniter4/pull/6413 */ public function withNeverExpiring(); diff --git a/system/Cookie/Cookie.php b/system/Cookie/Cookie.php index 22c01a7daa3c..72188451d3e2 100644 --- a/system/Cookie/Cookie.php +++ b/system/Cookie/Cookie.php @@ -152,9 +152,9 @@ public static function setDefaults($config = []) return $oldDefaults; } - //========================================================================= + // ========================================================================= // CONSTRUCTORS - //========================================================================= + // ========================================================================= /** * Create a new Cookie instance from a `Set-Cookie` header. @@ -238,9 +238,9 @@ final public function __construct(string $name, string $value = '', array $optio $this->raw = $raw; } - //========================================================================= + // ========================================================================= // GETTERS - //========================================================================= + // ========================================================================= /** * {@inheritDoc} @@ -391,9 +391,9 @@ public function getOptions(): array ]; } - //========================================================================= + // ========================================================================= // CLONING - //========================================================================= + // ========================================================================= /** * {@inheritDoc} @@ -460,7 +460,7 @@ public function withExpired() } /** - * {@inheritDoc} + * @deprecated See https://github.com/codeigniter4/CodeIgniter4/pull/6413 */ public function withNeverExpiring() { @@ -556,9 +556,9 @@ public function withRaw(bool $raw = true) return $cookie; } - //========================================================================= + // ========================================================================= // ARRAY ACCESS FOR BC - //========================================================================= + // ========================================================================= /** * Whether an offset exists. @@ -614,9 +614,9 @@ public function offsetUnset($offset): void throw new LogicException(sprintf('Cannot unset values of properties of %s as it is immutable.', static::class)); } - //========================================================================= + // ========================================================================= // CONVERTERS - //========================================================================= + // ========================================================================= /** * {@inheritDoc} @@ -716,9 +716,9 @@ protected static function convertExpiresTimestamp($expires = 0): int return $expires > 0 ? (int) $expires : 0; } - //========================================================================= + // ========================================================================= // VALIDATION - //========================================================================= + // ========================================================================= /** * Validates the cookie name per RFC 2616. diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index 2310c1ba3509..14eada695f83 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -577,7 +577,7 @@ public function fromSubquery(BaseBuilder $from, string $alias): self { $table = $this->buildSubquery($from, true, $alias); - $this->trackAliases($table); + $this->db->addTableAlias($alias); $this->QBFrom[] = $table; return $this; @@ -1920,6 +1920,7 @@ public function insert($set = null, ?bool $escape = null) * @internal This is a temporary solution. * * @see https://github.com/codeigniter4/CodeIgniter4/pull/5376 + * * @TODO Fix a root cause, and this method should be removed. */ protected function removeAlias(string $from): string @@ -2422,6 +2423,22 @@ public function decrement(string $column, int $value = 1) return true; } + /** + * Only runs the query when $condition evaluates to true + * + * @param array|bool|float|int|object|resource|string|null $condition + */ + public function when($condition, Closure $callback, ?Closure $defaultCallback = null): self + { + if ($condition) { + $callback($this, $condition); + } elseif ($defaultCallback) { + $defaultCallback($this); + } + + return $this; + } + /** * Generates a platform-specific delete string from the supplied data */ @@ -2697,8 +2714,7 @@ protected function objectToArray($object) $array = []; foreach (get_object_vars($object) as $key => $val) { - // There are some built in keys we need to ignore for this conversion - if (! is_object($val) && ! is_array($val) && $key !== '_parent_name') { + if ((! is_object($val) || $val instanceof RawSql) && ! is_array($val)) { $array[$key] = $val; } } @@ -2724,13 +2740,10 @@ protected function batchObjectToArray($object) $fields = array_keys($out); foreach ($fields as $val) { - // There are some built in keys we need to ignore for this conversion - if ($val !== '_parent_name') { - $i = 0; + $i = 0; - foreach ($out[$val] as $data) { - $array[$i++][$val] = $data; - } + foreach ($out[$val] as $data) { + $array[$i++][$val] = $data; } } @@ -2944,7 +2957,7 @@ protected function buildSubquery($builder, bool $wrapped = false, string $alias throw new DatabaseException('The subquery cannot be the same object as the main query object.'); } - $subquery = strtr($builder->getCompiledSelect(), "\n", ' '); + $subquery = strtr($builder->getCompiledSelect(false), "\n", ' '); if ($wrapped) { $subquery = '(' . $subquery . ')'; diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index 1887a8208fd6..deaa9443c851 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -559,7 +559,7 @@ public function addTableAlias(string $table) /** * Executes the query against the database. * - * @return mixed + * @return bool|object|resource */ abstract protected function execute(string $sql); @@ -883,7 +883,12 @@ public function table($tableName) */ public function newQuery(): BaseBuilder { - return $this->table(',')->from([], true); + // save table aliases + $tempAliases = $this->aliasedTables; + $builder = $this->table(',')->from([], true); + $this->aliasedTables = $tempAliases; + + return $builder; } /** @@ -1238,6 +1243,10 @@ public function escape($str) } if (is_string($str) || (is_object($str) && method_exists($str, '__toString'))) { + if ($str instanceof RawSql) { + return $str->__toString(); + } + return "'" . $this->escapeString($str) . "'"; } @@ -1348,9 +1357,9 @@ protected function getDriverFunctionPrefix(): string return strtolower($this->DBDriver) . '_'; } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // META Methods - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Returns an array of table names diff --git a/system/Database/Forge.php b/system/Database/Forge.php index 458e60b22442..5c13d27feb34 100644 --- a/system/Database/Forge.php +++ b/system/Database/Forge.php @@ -776,7 +776,7 @@ public function modifyColumn(string $table, $field): bool } /** - * @param mixed $fields + * @param array|string $fields * * @return false|string|string[] */ diff --git a/system/Database/MySQLi/Connection.php b/system/Database/MySQLi/Connection.php index fcbc03bdbbe1..c9361e5c795b 100644 --- a/system/Database/MySQLi/Connection.php +++ b/system/Database/MySQLi/Connection.php @@ -277,7 +277,7 @@ public function getVersion(): string /** * Executes the query against the database. * - * @return mixed + * @return bool|object */ protected function execute(string $sql) { diff --git a/system/Database/MySQLi/Forge.php b/system/Database/MySQLi/Forge.php index d00c26dd1ca7..0bee007c6341 100644 --- a/system/Database/MySQLi/Forge.php +++ b/system/Database/MySQLi/Forge.php @@ -128,9 +128,9 @@ protected function _createTableAttributes(array $attributes): string /** * ALTER TABLE * - * @param string $alterType ALTER type - * @param string $table Table name - * @param mixed $field Column definition + * @param string $alterType ALTER type + * @param string $table Table name + * @param array|string $field Column definition * * @return string|string[] */ diff --git a/system/Database/OCI8/Connection.php b/system/Database/OCI8/Connection.php index 72c971e76167..b2d5b808440c 100644 --- a/system/Database/OCI8/Connection.php +++ b/system/Database/OCI8/Connection.php @@ -184,7 +184,7 @@ public function getVersion(): string /** * Executes the query against the database. * - * @return false|resource + * @return bool */ protected function execute(string $sql) { diff --git a/system/Database/OCI8/Forge.php b/system/Database/OCI8/Forge.php index 42393cd63aa9..add01540177a 100644 --- a/system/Database/OCI8/Forge.php +++ b/system/Database/OCI8/Forge.php @@ -86,9 +86,9 @@ class Forge extends BaseForge /** * ALTER TABLE * - * @param string $alterType ALTER type - * @param string $table Table name - * @param mixed $field Column definition + * @param string $alterType ALTER type + * @param string $table Table name + * @param array|string $field Column definition * * @return string|string[] */ diff --git a/system/Database/Postgre/Connection.php b/system/Database/Postgre/Connection.php index 9296b3277db0..5edb24a0abce 100644 --- a/system/Database/Postgre/Connection.php +++ b/system/Database/Postgre/Connection.php @@ -13,6 +13,7 @@ use CodeIgniter\Database\BaseConnection; use CodeIgniter\Database\Exceptions\DatabaseException; +use CodeIgniter\Database\RawSql; use ErrorException; use stdClass; @@ -132,7 +133,7 @@ public function getVersion(): string /** * Executes the query against the database. * - * @return mixed + * @return false|resource */ protected function execute(string $sql) { @@ -181,6 +182,10 @@ public function escape($str) } if (is_string($str) || (is_object($str) && method_exists($str, '__toString'))) { + if ($str instanceof RawSql) { + return $str->__toString(); + } + return pg_escape_literal($this->connID, $str); } diff --git a/system/Database/Postgre/Forge.php b/system/Database/Postgre/Forge.php index a050c6888e38..6791da6169fa 100644 --- a/system/Database/Postgre/Forge.php +++ b/system/Database/Postgre/Forge.php @@ -76,7 +76,7 @@ protected function _createTableAttributes(array $attributes): string } /** - * @param mixed $field + * @param array|string $field * * @return array|bool|string */ @@ -130,7 +130,7 @@ protected function _alterTable(string $alterType, string $table, $field) protected function _processColumn(array $field): string { return $this->db->escapeIdentifiers($field['name']) - . ' ' . $field['type'] . $field['length'] + . ' ' . $field['type'] . ($field['type'] === 'text' ? '' : $field['length']) . $field['default'] . $field['null'] . $field['auto_increment'] diff --git a/system/Database/SQLSRV/Connection.php b/system/Database/SQLSRV/Connection.php index 6a029b238257..7d89305ef6fb 100755 --- a/system/Database/SQLSRV/Connection.php +++ b/system/Database/SQLSRV/Connection.php @@ -461,7 +461,7 @@ public function setDatabase(?string $databaseName = null) /** * Executes the query against the database. * - * @return mixed + * @return false|resource */ protected function execute(string $sql) { diff --git a/system/Database/SQLSRV/Forge.php b/system/Database/SQLSRV/Forge.php index 95ec9ff0b601..7f8a0d8199e2 100755 --- a/system/Database/SQLSRV/Forge.php +++ b/system/Database/SQLSRV/Forge.php @@ -119,13 +119,12 @@ protected function _createTableAttributes(array $attributes): string } /** - * @param mixed $field + * @param array|string $field * * @return false|string|string[] */ protected function _alterTable(string $alterType, string $table, $field) { - // Handle DROP here if ($alterType === 'DROP') { // check if fields are part of any indexes @@ -144,7 +143,25 @@ protected function _alterTable(string $alterType, string $table, $field) } } - $sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table) . ' DROP '; + $fullTable = $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table); + + // Drop default constraints + $fields = implode(',', $this->db->escape((array) $field)); + + $sql = <<db->query($sql)->getResultArray() as $index) { + $this->db->query('ALTER TABLE ' . $fullTable . ' DROP CONSTRAINT ' . $index['name'] . ''); + } + + $sql = 'ALTER TABLE ' . $fullTable . ' DROP '; $fields = array_map(static fn ($item) => 'COLUMN [' . trim($item) . ']', (array) $field); @@ -263,7 +280,7 @@ protected function _processColumn(array $field): string { return $this->db->escapeIdentifiers($field['name']) . (empty($field['new_name']) ? '' : ' ' . $this->db->escapeIdentifiers($field['new_name'])) - . ' ' . $field['type'] . $field['length'] + . ' ' . $field['type'] . ($field['type'] === 'text' ? '' : $field['length']) . $field['default'] . $field['null'] . $field['auto_increment'] diff --git a/system/Database/SQLite3/Connection.php b/system/Database/SQLite3/Connection.php index 5e0d96791ec6..0d8d026c472d 100644 --- a/system/Database/SQLite3/Connection.php +++ b/system/Database/SQLite3/Connection.php @@ -16,6 +16,7 @@ use ErrorException; use Exception; use SQLite3; +use SQLite3Result; use stdClass; /** @@ -120,7 +121,7 @@ public function getVersion(): string /** * Execute the query * - * @return mixed \SQLite3Result object or bool + * @return bool|SQLite3Result */ protected function execute(string $sql) { diff --git a/system/Database/SQLite3/Forge.php b/system/Database/SQLite3/Forge.php index 154c9f97b554..9175dd2bd978 100644 --- a/system/Database/SQLite3/Forge.php +++ b/system/Database/SQLite3/Forge.php @@ -109,7 +109,7 @@ public function dropDatabase(string $dbName): bool } /** - * @param mixed $field + * @param array|string $field * * @return array|string|null */ diff --git a/system/Database/SQLite3/Table.php b/system/Database/SQLite3/Table.php index 6c3328557578..0834212d2c2c 100644 --- a/system/Database/SQLite3/Table.php +++ b/system/Database/SQLite3/Table.php @@ -247,6 +247,13 @@ protected function createTable() $this->forge->addField($fields); + $fieldNames = array_keys($fields); + + $this->keys = array_filter( + $this->keys, + static fn ($index) => count(array_intersect($index['fields'], $fieldNames)) === count($index['fields']) + ); + // Unique/Index keys if (is_array($this->keys)) { foreach ($this->keys as $key) { diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php index 5198152ffd13..01eca101d887 100644 --- a/system/Debug/Exceptions.php +++ b/system/Debug/Exceptions.php @@ -179,7 +179,7 @@ public function shutdownHandler() ['type' => $type, 'message' => $message, 'file' => $file, 'line' => $line] = $error; if (in_array($type, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE], true)) { - $this->exceptionHandler(new ErrorException($message, $type, 0, $file, $line)); + $this->exceptionHandler(new ErrorException($message, 0, $type, $file, $line)); } } @@ -328,9 +328,9 @@ protected function determineCodes(Throwable $exception): array return [$statusCode, $exitStatus]; } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Display Methods - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * This makes nicer looking paths for the error output. diff --git a/system/Entity/Entity.php b/system/Entity/Entity.php index e9a4e938a7ed..32b741057b56 100644 --- a/system/Entity/Entity.php +++ b/system/Entity/Entity.php @@ -26,6 +26,7 @@ use CodeIgniter\Entity\Cast\URICast; use CodeIgniter\Entity\Exceptions\CastException; use CodeIgniter\I18n\Time; +use DateTime; use Exception; use JsonSerializable; use ReturnTypeWillChange; @@ -149,7 +150,7 @@ public function fill(?array $data = null) * * @param bool $onlyChanged If true, only return values that have changed since object creation * @param bool $cast If true, properties will be cast. - * @param bool $recursive If true, inner entities will be casted as array as well. + * @param bool $recursive If true, inner entities will be cast as array as well. */ public function toArray(bool $onlyChanged = false, bool $cast = true, bool $recursive = false): array { @@ -191,7 +192,7 @@ public function toArray(bool $onlyChanged = false, bool $cast = true, bool $recu * Returns the raw values of the current attributes. * * @param bool $onlyChanged If true, only return values that have changed since object creation - * @param bool $recursive If true, inner entities will be casted as array as well. + * @param bool $recursive If true, inner entities will be cast as array as well. */ public function toRawArray(bool $onlyChanged = false, bool $recursive = false): array { @@ -249,7 +250,7 @@ public function syncOriginal() * was created. Or, without a parameter, checks if any * properties have changed. * - * @param string $key + * @param string|null $key class property */ public function hasChanged(?string $key = null): bool { @@ -310,11 +311,11 @@ protected function mapProperty(string $key) * Converts the given string|timestamp|DateTime|Time instance * into the "CodeIgniter\I18n\Time" object. * - * @param mixed $value + * @param DateTime|float|int|string|Time $value * * @throws Exception * - * @return mixed|Time + * @return Time */ protected function mutateDate($value) { @@ -326,13 +327,13 @@ protected function mutateDate($value) * Add ? at the beginning of $type (i.e. ?string) to get NULL * instead of casting $value if $value === null * - * @param mixed $value Attribute value - * @param string $attribute Attribute name - * @param string $method Allowed to "get" and "set" + * @param bool|float|int|string|null $value Attribute value + * @param string $attribute Attribute name + * @param string $method Allowed to "get" and "set" * * @throws CastException * - * @return mixed + * @return array|bool|float|int|object|string|null */ protected function castAs($value, string $attribute, string $method = 'get') { @@ -426,7 +427,7 @@ public function cast(?bool $cast = null) * $this->my_property = $p; * $this->setMyProperty() = $p; * - * @param mixed|null $value + * @param array|bool|float|int|object|string|null $value * * @throws Exception * @@ -473,7 +474,9 @@ public function __set(string $key, $value = null) * * @throws Exception * - * @return mixed + * @params string $key class property + * + * @return array|bool|float|int|object|string|null */ public function __get(string $key) { diff --git a/system/Files/FileCollection.php b/system/Files/FileCollection.php index 20b208fded43..ee1d0477d34c 100644 --- a/system/Files/FileCollection.php +++ b/system/Files/FileCollection.php @@ -33,9 +33,9 @@ class FileCollection implements Countable, IteratorAggregate */ protected $files = []; - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Support Methods - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Resolves a full path and verifies it is an actual directory. @@ -106,9 +106,9 @@ final protected static function matchFiles(array $files, string $pattern): array return array_filter($files, static fn ($value) => (bool) preg_match($pattern, basename($value))); } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Class Core - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Loads the Filesystem helper and adds any initial files. @@ -189,9 +189,9 @@ public function add($paths, bool $recursive = true) return $this; } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // File Handling - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Verifies and adds files to the list. @@ -245,9 +245,9 @@ public function removeFile(string $file) return $this->removeFiles([$file]); } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Directory Handling - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Verifies and adds files from each @@ -287,9 +287,9 @@ public function addDirectory(string $directory, bool $recursive = false) return $this; } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Filtering - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Removes any files from the list that match the supplied pattern @@ -335,9 +335,9 @@ public function retainPattern(string $pattern, ?string $scope = null) return $this->removeFiles(array_diff($files, self::matchFiles($files, $pattern))); } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Interface Methods - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Returns the current number of files in the collection. diff --git a/system/Filters/CSRF.php b/system/Filters/CSRF.php index 9cce85fb7ada..6bc83405b7f0 100644 --- a/system/Filters/CSRF.php +++ b/system/Filters/CSRF.php @@ -11,6 +11,7 @@ namespace CodeIgniter\Filters; +use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; @@ -44,7 +45,7 @@ class CSRF implements FilterInterface */ public function before(RequestInterface $request, $arguments = null) { - if ($request->isCLI()) { + if (! $request instanceof IncomingRequest) { return; } diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index d5d5e3aae42e..ca2b8a4b5ba8 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -134,7 +134,7 @@ private function discoverFilters() $className = $locator->getClassname($file); // Don't include our main Filter config again... - if ($className === 'Config\\Filters') { + if ($className === FiltersConfig::class) { continue; } @@ -382,9 +382,9 @@ public function getArguments(?string $key = null) return $key === null ? $this->arguments : $this->arguments[$key]; } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Processors - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Add any applicable (not excluded) global filter settings to the mix. diff --git a/system/Filters/Honeypot.php b/system/Filters/Honeypot.php index 539513e7cb8a..419c2b47e75a 100644 --- a/system/Filters/Honeypot.php +++ b/system/Filters/Honeypot.php @@ -12,6 +12,7 @@ namespace CodeIgniter\Filters; use CodeIgniter\Honeypot\Exceptions\HoneypotException; +use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; use Config\Services; @@ -31,6 +32,10 @@ class Honeypot implements FilterInterface */ public function before(RequestInterface $request, $arguments = null) { + if (! $request instanceof IncomingRequest) { + return; + } + if (Services::honeypot()->hasContent($request)) { throw HoneypotException::isBot(); } diff --git a/system/Filters/InvalidChars.php b/system/Filters/InvalidChars.php index 4b1d8f7f9b6b..42aa15b19675 100644 --- a/system/Filters/InvalidChars.php +++ b/system/Filters/InvalidChars.php @@ -11,6 +11,7 @@ namespace CodeIgniter\Filters; +use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\Security\Exceptions\SecurityException; @@ -48,7 +49,7 @@ class InvalidChars implements FilterInterface */ public function before(RequestInterface $request, $arguments = null) { - if ($request->isCLI()) { + if (! $request instanceof IncomingRequest) { return; } diff --git a/system/HTTP/CLIRequest.php b/system/HTTP/CLIRequest.php index 5f2c70b19ff7..e81ae9cabbd9 100644 --- a/system/HTTP/CLIRequest.php +++ b/system/HTTP/CLIRequest.php @@ -12,6 +12,7 @@ namespace CodeIgniter\HTTP; use Config\App; +use Locale; use RuntimeException; /** @@ -214,4 +215,79 @@ public function isCLI(): bool { return true; } + + /** + * Fetch an item from GET data. + * + * @param array|string|null $index Index for item to fetch from $_GET. + * @param int|null $filter A filter name to apply. + * @param mixed|null $flags + * + * @return null + */ + public function getGet($index = null, $filter = null, $flags = null) + { + return $this->returnNullOrEmptyArray($index); + } + + /** + * Fetch an item from POST. + * + * @param array|string|null $index Index for item to fetch from $_POST. + * @param int|null $filter A filter name to apply + * @param mixed $flags + * + * @return null + */ + public function getPost($index = null, $filter = null, $flags = null) + { + return $this->returnNullOrEmptyArray($index); + } + + /** + * Fetch an item from POST data with fallback to GET. + * + * @param array|string|null $index Index for item to fetch from $_POST or $_GET + * @param int|null $filter A filter name to apply + * @param mixed $flags + * + * @return null + */ + public function getPostGet($index = null, $filter = null, $flags = null) + { + return $this->returnNullOrEmptyArray($index); + } + + /** + * Fetch an item from GET data with fallback to POST. + * + * @param array|string|null $index Index for item to be fetched from $_GET or $_POST + * @param int|null $filter A filter name to apply + * @param mixed $flags + * + * @return null + */ + public function getGetPost($index = null, $filter = null, $flags = null) + { + return $this->returnNullOrEmptyArray($index); + } + + /** + * @param array|string|null $index + * + * @return array|null + */ + private function returnNullOrEmptyArray($index) + { + return ($index === null || is_array($index)) ? [] : null; + } + + /** + * Gets the current locale, with a fallback to the default + * locale if none is set. + */ + public function getLocale(): string + { + return Locale::getDefault(); + } } diff --git a/system/HTTP/MessageTrait.php b/system/HTTP/MessageTrait.php index 90e36ad5dccc..daa70875f548 100644 --- a/system/HTTP/MessageTrait.php +++ b/system/HTTP/MessageTrait.php @@ -38,9 +38,9 @@ trait MessageTrait */ protected $headerMap = []; - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Body - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Sets the body of the current message. @@ -70,9 +70,9 @@ public function appendBody($data): self return $this; } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Headers - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Populates the $headers array with any headers the server knows about. diff --git a/system/HTTP/Negotiate.php b/system/HTTP/Negotiate.php index dfb9dcedf9b4..7b0fdc2d54b4 100644 --- a/system/HTTP/Negotiate.php +++ b/system/HTTP/Negotiate.php @@ -27,7 +27,7 @@ class Negotiate /** * Request * - * @var IncomingRequest|RequestInterface + * @var IncomingRequest */ protected $request; @@ -37,6 +37,8 @@ class Negotiate public function __construct(?RequestInterface $request = null) { if ($request !== null) { + assert($request instanceof IncomingRequest); + $this->request = $request; } } @@ -48,6 +50,8 @@ public function __construct(?RequestInterface $request = null) */ public function setRequest(RequestInterface $request) { + assert($request instanceof IncomingRequest); + $this->request = $request; return $this; @@ -118,9 +122,9 @@ public function language(array $supported): string return $this->getBestMatch($supported, $this->request->getHeaderLine('accept-language'), false, false, true); } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Utility Methods - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Does the grunt work of comparing any of the app-supported values diff --git a/system/HTTP/RedirectResponse.php b/system/HTTP/RedirectResponse.php index 1040c1578628..f75bda850a95 100644 --- a/system/HTTP/RedirectResponse.php +++ b/system/HTTP/RedirectResponse.php @@ -79,8 +79,8 @@ public function back(?int $code = null, string $method = 'auto') } /** - * Specifies that the current $_GET and $_POST arrays should be - * packaged up with the response. + * Sets the current $_GET and $_POST arrays in the session. + * This also saves the validation errors. * * It will then be available via the 'old()' helper function. * @@ -94,22 +94,18 @@ public function withInput() 'post' => $_POST ?? [], ]); - // @TODO Remove this in the future. - // See https://github.com/codeigniter4/CodeIgniter4/issues/5839#issuecomment-1086624600 $this->withErrors(); return $this; } /** - * Set validation errors in the session. + * Sets validation errors in the session. * * If the validation has any errors, transmit those back * so they can be displayed when the validation is handled * within a method different than displaying the form. * - * @TODO Make this method public when removing $this->withErrors() in withInput(). - * * @return $this */ private function withErrors(): self @@ -118,7 +114,7 @@ private function withErrors(): self if ($validation->getErrors()) { $session = Services::session(); - $session->setFlashdata('_ci_validation_errors', serialize($validation->getErrors())); + $session->setFlashdata('_ci_validation_errors', $validation->getErrors()); } return $this; diff --git a/system/HTTP/RequestInterface.php b/system/HTTP/RequestInterface.php index e7bc14a3f361..ca3229db904c 100644 --- a/system/HTTP/RequestInterface.php +++ b/system/HTTP/RequestInterface.php @@ -13,10 +13,6 @@ /** * Expected behavior of an HTTP request - * - * @mixin IncomingRequest - * @mixin CLIRequest - * @mixin CURLRequest */ interface RequestInterface { diff --git a/system/HTTP/Response.php b/system/HTTP/Response.php index bcaffa49fbe8..c0f3a2e8120a 100644 --- a/system/HTTP/Response.php +++ b/system/HTTP/Response.php @@ -222,6 +222,7 @@ public function getStatusCode(): int * * @see http://tools.ietf.org/html/rfc7231#section-6 * @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + * * @deprecated Use getReasonPhrase() * * @codeCoverageIgnore diff --git a/system/HTTP/ResponseInterface.php b/system/HTTP/ResponseInterface.php index 9cf729bb841e..e8bfde68dde2 100644 --- a/system/HTTP/ResponseInterface.php +++ b/system/HTTP/ResponseInterface.php @@ -141,13 +141,14 @@ public function setStatusCode(int $code, string $reason = ''); * * @see http://tools.ietf.org/html/rfc7231#section-6 * @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + * * @deprecated Use getReasonPhrase() */ public function getReason(): string; - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Convenience Methods - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Sets the date header @@ -185,9 +186,9 @@ public function setLink(PagerInterface $pager); */ public function setContentType(string $mime, string $charset = 'UTF-8'); - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Formatter Methods - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Converts the $body into JSON and sets the Content Type header. @@ -225,11 +226,11 @@ public function setXML($body); */ public function getXML(); - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Cache Control Methods // // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9 - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Sets the appropriate headers to ensure this response @@ -265,9 +266,9 @@ public function noCache(); */ public function setCache(array $options = []); - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Output Methods - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Sends the output to the browser. @@ -290,9 +291,9 @@ public function sendHeaders(); */ public function sendBody(); - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Cookie Methods - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Set a cookie @@ -350,9 +351,9 @@ public function deleteCookie(string $name = '', string $domain = '', string $pat */ public function getCookies(); - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Response Methods - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Perform a redirect to a new URL, in two flavors: header or location. diff --git a/system/HTTP/ResponseTrait.php b/system/HTTP/ResponseTrait.php index 66c7ce263fed..2b96d6a083aa 100644 --- a/system/HTTP/ResponseTrait.php +++ b/system/HTTP/ResponseTrait.php @@ -165,9 +165,9 @@ public function setStatusCode(int $code, string $reason = '') return $this; } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Convenience Methods - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Sets the date header @@ -323,11 +323,11 @@ protected function formatBody($body, string $format) return $body; } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Cache Control Methods // // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9 - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Sets the appropriate headers to ensure this response @@ -422,9 +422,9 @@ public function setLastModified($date) return $this; } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Output Methods - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Sends the output to the browser. diff --git a/system/Helpers/cookie_helper.php b/system/Helpers/cookie_helper.php index 5537de9c3899..bb355f70224a 100755 --- a/system/Helpers/cookie_helper.php +++ b/system/Helpers/cookie_helper.php @@ -13,9 +13,9 @@ use Config\Cookie; use Config\Services; -//============================================================================= +// ============================================================================= // CodeIgniter Cookie Helpers -//============================================================================= +// ============================================================================= if (! function_exists('set_cookie')) { /** diff --git a/system/Helpers/form_helper.php b/system/Helpers/form_helper.php index aaab2a46e0be..e20c7ba0818e 100644 --- a/system/Helpers/form_helper.php +++ b/system/Helpers/form_helper.php @@ -9,6 +9,7 @@ * the LICENSE file that was distributed with this source code. */ +use CodeIgniter\Validation\Exceptions\ValidationException; use Config\App; use Config\Services; @@ -680,12 +681,91 @@ function set_radio(string $field, string $value = '', bool $default = false): st } } +if (! function_exists('validation_errors')) { + /** + * Returns the validation errors. + * + * First, checks the validation errors that are stored in the session. + * To store the errors in the session, you need to use `withInput()` with `redirect()`. + * + * The returned array should be in the following format: + * [ + * 'field1' => 'error message', + * 'field2' => 'error message', + * ] + * + * @return array + */ + function validation_errors() + { + session(); + + // Check the session to see if any were + // passed along from a redirect withErrors() request. + if (isset($_SESSION['_ci_validation_errors']) && (ENVIRONMENT === 'testing' || ! is_cli())) { + return $_SESSION['_ci_validation_errors']; + } + + $validation = Services::validation(); + + return $validation->getErrors(); + } +} + +if (! function_exists('validation_list_errors')) { + /** + * Returns the rendered HTML of the validation errors. + * + * See Validation::listErrors() + */ + function validation_list_errors(string $template = 'list'): string + { + $config = config('Validation'); + $view = Services::renderer(); + + if (! array_key_exists($template, $config->templates)) { + throw ValidationException::forInvalidTemplate($template); + } + + return $view->setVar('errors', validation_errors()) + ->render($config->templates[$template]); + } +} + +if (! function_exists('validation_show_error')) { + /** + * Returns a single error for the specified field in formatted HTML. + * + * See Validation::showError() + */ + function validation_show_error(string $field, string $template = 'single'): string + { + $config = config('Validation'); + $view = Services::renderer(); + + $errors = validation_errors(); + + if (! array_key_exists($field, $errors)) { + return ''; + } + + if (! array_key_exists($template, $config->templates)) { + throw ValidationException::forInvalidTemplate($template); + } + + return $view->setVar('error', $errors[$field]) + ->render($config->templates[$template]); + } +} + if (! function_exists('parse_form_attributes')) { /** * Parse the form attributes * * Helper function used by some of the form helpers * + * @internal + * * @param array|string $attributes List of attributes * @param array $default Default values */ diff --git a/system/Helpers/kint_helper.php b/system/Helpers/kint_helper.php new file mode 100644 index 000000000000..10c4f34e938d --- /dev/null +++ b/system/Helpers/kint_helper.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +// This helper is autoloaded by CodeIgniter. + +if (! function_exists('dd')) { + if (class_exists(Kint::class)) { + /** + * Prints a Kint debug report and exits. + * + * @param array ...$vars + * + * @codeCoverageIgnore Can't be tested ... exits + */ + function dd(...$vars) + { + // @codeCoverageIgnoreStart + Kint::$aliases[] = 'dd'; + Kint::dump(...$vars); + + exit; + // @codeCoverageIgnoreEnd + } + } else { + // In case that Kint is not loaded. + function dd(...$vars) + { + return 0; + } + } +} + +if (! function_exists('d') && ! class_exists(Kint::class)) { + // In case that Kint is not loaded. + function d(...$vars) + { + return 0; + } +} + +if (! function_exists('trace')) { + if (class_exists(Kint::class)) { + /** + * Provides a backtrace to the current execution point, from Kint. + */ + function trace() + { + Kint::$aliases[] = 'trace'; + Kint::trace(); + } + } else { + // In case that Kint is not loaded. + function trace() + { + return 0; + } + } +} diff --git a/system/Honeypot/Honeypot.php b/system/Honeypot/Honeypot.php index 178f56621618..fdd8fe95168b 100644 --- a/system/Honeypot/Honeypot.php +++ b/system/Honeypot/Honeypot.php @@ -12,6 +12,7 @@ namespace CodeIgniter\Honeypot; use CodeIgniter\Honeypot\Exceptions\HoneypotException; +use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; use Config\Honeypot as HoneypotConfig; @@ -59,6 +60,8 @@ public function __construct(HoneypotConfig $config) */ public function hasContent(RequestInterface $request) { + assert($request instanceof IncomingRequest); + return ! empty($request->getPost($this->config->name)); } diff --git a/system/I18n/Time.php b/system/I18n/Time.php index dd26979af13d..300ae851c155 100644 --- a/system/I18n/Time.php +++ b/system/I18n/Time.php @@ -62,9 +62,9 @@ class Time extends DateTime */ protected static $testNow; - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Constructors - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Time constructor. @@ -285,6 +285,7 @@ public static function createFromInstance(DateTimeInterface $dateTime, ?string $ * @return Time * * @deprecated Use createFromInstance() instead + * * @codeCoverageIgnore */ public static function instance(DateTime $dateTime, ?string $locale = null) @@ -307,9 +308,9 @@ public function toDateTime() return $dateTime; } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // For Testing - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Creates an instance of Time that will be returned during testing @@ -347,9 +348,9 @@ public static function hasTestNow(): bool return static::$testNow !== null; } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Getters - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Returns the localized Year @@ -509,9 +510,9 @@ public function getTimezoneName(): string return $this->timezone->getName(); } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Setters - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Sets the current year for this instance. @@ -688,9 +689,9 @@ public function setTimestamp($timestamp) return self::parse($time, $this->timezone, $this->locale); } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Add/Subtract - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Returns a new Time instance with $seconds added to the time. @@ -836,9 +837,9 @@ public function subYears(int $years) return $time->sub(DateInterval::createFromDateString("{$years} years")); } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Formatters - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Returns the localized value of the date in the format 'Y-m-d H:i:s' @@ -895,7 +896,7 @@ public function toTimeString() * * @throws Exception * - * @return bool|string + * @return false|string */ public function toLocalizedString(?string $format = null) { @@ -904,9 +905,9 @@ public function toLocalizedString(?string $format = null) return IntlDateFormatter::formatObject($this->toDateTime(), $format, $this->locale); } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Comparison - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Determines if the datetime passed in is equal to the current instance. @@ -984,9 +985,9 @@ public function isAfter($testTime, ?string $timezone = null): bool return $ourTime > $testTime; } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Differences - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Returns a text string that is easily readable that describes @@ -1060,9 +1061,9 @@ public function difference($testTime, ?string $timezone = null) return new TimeDifference($ourTime, $testTime); } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Utilities - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Returns a Time instance with the timezone converted to UTC. @@ -1179,4 +1180,12 @@ public function __wakeup(): void $this->timezone = new DateTimeZone($timezone); parent::__construct($this->date, $this->timezone); } + + /** + * Returns the datetime string ('Y-m-d H:i:s') that is safe to databases regardless of locale. + */ + public function toDatabase(): string + { + return $this->format('Y-m-d H:i:s'); + } } diff --git a/system/Language/en/CLI.php b/system/Language/en/CLI.php index c8f7ecae081d..29b96fd2c74a 100644 --- a/system/Language/en/CLI.php +++ b/system/Language/en/CLI.php @@ -31,9 +31,9 @@ 'commandType' => 'Command type', 'databaseGroup' => 'Database group', 'fileCreate' => 'File created: {0}', - 'fileError' => 'Error while creating file: {0}', - 'fileExist' => 'File exists: {0}', - 'fileOverwrite' => 'File overwritten: {0}', + 'fileError' => 'Error while creating file: "{0}"', + 'fileExist' => 'File exists: "{0}"', + 'fileOverwrite' => 'File overwritten: "{0}"', 'parentClass' => 'Parent class', 'returnType' => 'Return type', 'tableName' => 'Table name', @@ -43,6 +43,6 @@ 'helpDescription' => 'Description:', 'helpOptions' => 'Options:', 'helpUsage' => 'Usage:', - 'invalidColor' => 'Invalid {0} color: {1}.', + 'invalidColor' => 'Invalid "{0}" color: "{1}".', 'namespaceNotDefined' => 'Namespace "{0}" is not defined.', ]; diff --git a/system/Language/en/Cache.php b/system/Language/en/Cache.php index d119223a837f..d2abc9bedbbc 100644 --- a/system/Language/en/Cache.php +++ b/system/Language/en/Cache.php @@ -11,7 +11,7 @@ // Cache language settings return [ - 'unableToWrite' => 'Cache unable to write to {0}.', + 'unableToWrite' => 'Cache unable to write to "{0}".', 'invalidHandlers' => 'Cache config must have an array of $validHandlers.', 'noBackup' => 'Cache config must have a handler and backupHandler set.', 'handlerNotFound' => 'Cache config has an invalid handler or backup handler specified.', diff --git a/system/Language/en/Core.php b/system/Language/en/Core.php index f292bb833a01..9ab549a4c8fe 100644 --- a/system/Language/en/Core.php +++ b/system/Language/en/Core.php @@ -11,10 +11,10 @@ // Core language settings return [ - 'copyError' => 'An error was encountered while attempting to replace the file ({0}). Please make sure your file directory is writable.', + 'copyError' => 'An error was encountered while attempting to replace the file "{0}". Please make sure your file directory is writable.', 'enabledZlibOutputCompression' => 'Your zlib.output_compression ini directive is turned on. This will not work well with output buffers.', - 'invalidFile' => 'Invalid file: {0}', + 'invalidFile' => 'Invalid file: "{0}"', 'invalidPhpVersion' => 'Your PHP version must be {0} or higher to run CodeIgniter. Current version: {1}', - 'missingExtension' => 'The framework needs the following extension(s) installed and loaded: {0}.', - 'noHandlers' => '{0} must provide at least one Handler.', + 'missingExtension' => 'The framework needs the following extension(s) installed and loaded: "{0}".', + 'noHandlers' => '"{0}" must provide at least one Handler.', ]; diff --git a/system/Language/en/Database.php b/system/Language/en/Database.php index 63a18498e9f4..f0410e744aa1 100644 --- a/system/Language/en/Database.php +++ b/system/Language/en/Database.php @@ -11,9 +11,9 @@ // Database language settings return [ - 'invalidEvent' => '{0} is not a valid Model Event callback.', - 'invalidArgument' => 'You must provide a valid {0}.', - 'invalidAllowedFields' => 'Allowed fields must be specified for model: {0}', + 'invalidEvent' => '"{0}" is not a valid Model Event callback.', + 'invalidArgument' => 'You must provide a valid "{0}".', + 'invalidAllowedFields' => 'Allowed fields must be specified for model: "{0}"', 'emptyDataset' => 'There is no data to {0}.', 'emptyPrimaryKey' => 'There is no primary key defined when trying to make {0}.', 'failGetFieldData' => 'Failed to get field data from database.', @@ -21,11 +21,11 @@ 'failGetForeignKeyData' => 'Failed to get foreign key data from database.', 'parseStringFail' => 'Parsing key string failed.', 'featureUnavailable' => 'This feature is not available for the database you are using.', - 'tableNotFound' => 'Table `{0}` was not found in the current database.', - 'noPrimaryKey' => '`{0}` model class does not specify a Primary Key.', - 'noDateFormat' => '`{0}` model class does not have a valid dateFormat.', - 'fieldNotExists' => 'Field `{0}` not found.', - 'forEmptyInputGiven' => 'Empty statement is given for the field `{0}`', + 'tableNotFound' => 'Table "{0}" was not found in the current database.', + 'noPrimaryKey' => '"{0}" model class does not specify a Primary Key.', + 'noDateFormat' => '"{0}" model class does not have a valid dateFormat.', + 'fieldNotExists' => 'Field "{0}" not found.', + 'forEmptyInputGiven' => 'Empty statement is given for the field "{0}"', 'forFindColumnHaveMultipleColumns' => 'Only single column allowed in Column name.', - 'methodNotAvailable' => 'You cannot use `{1}` in `{0}`. This is a method of the `Query Builder` class.', + 'methodNotAvailable' => 'You cannot use "{1}" in "{0}". This is a method of the Query Builder Class.', ]; diff --git a/system/Language/en/Email.php b/system/Language/en/Email.php index 16ff832ee8a8..133f327058da 100644 --- a/system/Language/en/Email.php +++ b/system/Language/en/Email.php @@ -12,9 +12,9 @@ // Email language settings return [ 'mustBeArray' => 'The email validation method must be passed an array.', - 'invalidAddress' => 'Invalid email address: {0}', - 'attachmentMissing' => 'Unable to locate the following email attachment: {0}', - 'attachmentUnreadable' => 'Unable to open this attachment: {0}', + 'invalidAddress' => 'Invalid email address: "{0}"', + 'attachmentMissing' => 'Unable to locate the following email attachment: "{0}"', + 'attachmentUnreadable' => 'Unable to open this attachment: "{0}"', 'noFrom' => 'Cannot send mail with no "From" header.', 'noRecipients' => 'You must include recipients: To, Cc, or Bcc', 'sendFailurePHPMail' => 'Unable to send email using PHP mail(). Your server might not be configured to send mail using this method.', diff --git a/system/Language/en/Encryption.php b/system/Language/en/Encryption.php index 269974fc885d..c0e7cb8276d1 100644 --- a/system/Language/en/Encryption.php +++ b/system/Language/en/Encryption.php @@ -12,7 +12,7 @@ // Encryption language settings return [ 'noDriverRequested' => 'No driver requested; Miss Daisy will be so upset!', - 'noHandlerAvailable' => 'Unable to find an available {0} encryption handler.', + 'noHandlerAvailable' => 'Unable to find an available "{0}" encryption handler.', 'unKnownHandler' => '"{0}" cannot be configured.', 'starterKeyNeeded' => 'Encrypter needs a starter key.', 'authenticationFailed' => 'Decrypting: authentication failed.', diff --git a/system/Language/en/Fabricator.php b/system/Language/en/Fabricator.php index 94140af339b2..bfedec747d91 100644 --- a/system/Language/en/Fabricator.php +++ b/system/Language/en/Fabricator.php @@ -13,5 +13,5 @@ return [ 'invalidModel' => 'Invalid model supplied for fabrication.', 'missingFormatters' => 'No valid formatters defined.', - 'createFailed' => 'Fabricator failed to insert on table {0}: {1}', + 'createFailed' => 'Fabricator failed to insert on table "{0}": {1}', ]; diff --git a/system/Language/en/Files.php b/system/Language/en/Files.php index 03fa776e4822..4506abe68690 100644 --- a/system/Language/en/Files.php +++ b/system/Language/en/Files.php @@ -11,8 +11,8 @@ // Files language settings return [ - 'fileNotFound' => 'File not found: {0}', - 'cannotMove' => 'Could not move file {0} to {1} ({2}).', + 'fileNotFound' => 'File not found: "{0}"', + 'cannotMove' => 'Could not move file "{0}" to "{1}". Reason: {2}', 'expectedDirectory' => '{0} expects a valid directory.', 'expectedFile' => '{0} expects a valid file.', ]; diff --git a/system/Language/en/Filters.php b/system/Language/en/Filters.php index fc59eaf8d005..aca478cef9bf 100644 --- a/system/Language/en/Filters.php +++ b/system/Language/en/Filters.php @@ -11,6 +11,6 @@ // Filters language settings return [ - 'noFilter' => '{0} filter must have a matching alias defined.', - 'incorrectInterface' => '{0} must implement CodeIgniter\Filters\FilterInterface.', + 'noFilter' => '"{0}" filter must have a matching alias defined.', + 'incorrectInterface' => '"{0}" must implement CodeIgniter\Filters\FilterInterface.', ]; diff --git a/system/Language/en/Format.php b/system/Language/en/Format.php index 8c06207e80d4..7c54534270aa 100644 --- a/system/Language/en/Format.php +++ b/system/Language/en/Format.php @@ -12,7 +12,7 @@ // Format language settings return [ 'invalidFormatter' => '"{0}" is not a valid Formatter class.', - 'invalidJSON' => 'Failed to parse json string, error: "{0}".', + 'invalidJSON' => 'Failed to parse JSON string. Error: {0}', 'invalidMime' => 'No Formatter defined for mime type: "{0}".', 'missingExtension' => 'The SimpleXML extension is required to format XML.', ]; diff --git a/system/Language/en/HTTP.php b/system/Language/en/HTTP.php index de34953a77d0..4467a0be1f7e 100644 --- a/system/Language/en/HTTP.php +++ b/system/Language/en/HTTP.php @@ -13,12 +13,12 @@ return [ // CurlRequest 'missingCurl' => 'CURL must be enabled to use the CURLRequest class.', - 'invalidSSLKey' => 'Cannot set SSL Key. {0} is not a valid file.', - 'sslCertNotFound' => 'SSL certificate not found at: {0}', + 'invalidSSLKey' => 'Cannot set SSL Key. "{0}" is not a valid file.', + 'sslCertNotFound' => 'SSL certificate not found at: "{0}"', 'curlError' => '{0} : {1}', // IncomingRequest - 'invalidNegotiationType' => '{0} is not a valid negotiation type. Must be one of: media, charset, encoding, language.', + 'invalidNegotiationType' => '"{0}" is not a valid negotiation type. Must be one of: media, charset, encoding, language.', // Message 'invalidHTTPProtocol' => 'Invalid HTTP Protocol Version. Must be one of: {0}', @@ -31,7 +31,7 @@ // DownloadResponse 'cannotSetBinary' => 'When setting filepath cannot set binary.', - 'cannotSetFilepath' => 'When setting binary cannot set filepath: {0}', + 'cannotSetFilepath' => 'When setting binary cannot set filepath: "{0}"', 'notFoundDownloadSource' => 'Not found download body source.', 'cannotSetCache' => 'It does not support caching for downloading.', 'cannotSetStatusCode' => 'It does not support change status code for downloading. code: {0}, reason: {1}', @@ -42,8 +42,8 @@ 'unknownStatusCode' => 'Unknown HTTP status code provided with no message: {0}', // URI - 'cannotParseURI' => 'Unable to parse URI: {0}', - 'segmentOutOfRange' => 'Request URI segment is out of range: {0}', + 'cannotParseURI' => 'Unable to parse URI: "{0}"', + 'segmentOutOfRange' => 'Request URI segment is out of range: "{0}"', 'invalidPort' => 'Ports must be between 0 and 65535. Given: {0}', 'malformedQueryString' => 'Query strings may not include URI fragments.', @@ -51,7 +51,7 @@ 'pageNotFound' => 'Page Not Found', 'emptyController' => 'No Controller specified.', 'controllerNotFound' => 'Controller or its method is not found: {0}::{1}', - 'methodNotFound' => 'Controller method is not found: {0}', + 'methodNotFound' => 'Controller method is not found: "{0}"', 'localeNotSupported' => 'Locale is not supported: {0}', // CSRF @@ -61,7 +61,7 @@ // Uploaded file moving 'alreadyMoved' => 'The uploaded file has already been moved.', 'invalidFile' => 'The original file is not a valid file.', - 'moveFailed' => 'Could not move file {0} to {1} ({2})', + 'moveFailed' => 'Could not move file "{0}" to "{1}". Reason: {2}', 'uploadErrOk' => 'The file uploaded with success.', 'uploadErrIniSize' => 'The file "%s" exceeds your upload_max_filesize ini directive.', diff --git a/system/Language/en/Images.php b/system/Language/en/Images.php index e31804ab4159..be4d52903ce4 100644 --- a/system/Language/en/Images.php +++ b/system/Language/en/Images.php @@ -22,13 +22,13 @@ 'unsupportedImageCreate' => 'Your server does not support the GD function required to process this type of image.', 'jpgOrPngRequired' => 'The image resize protocol specified in your preferences only works with JPEG or PNG image types.', 'rotateUnsupported' => 'Image rotation does not appear to be supported by your server.', - 'libPathInvalid' => 'The path to your image library is not correct. Please set the correct path in your image preferences. {0}', + 'libPathInvalid' => 'The path to your image library is not correct. Please set the correct path in your image preferences. "{0}"', 'imageProcessFailed' => 'Image processing failed. Please verify that your server supports the chosen protocol and that the path to your image library is correct.', 'rotationAngleRequired' => 'An angle of rotation is required to rotate the image.', 'invalidPath' => 'The path to the image is not correct.', 'copyFailed' => 'The image copy routine failed.', 'missingFont' => 'Unable to find a font to use.', 'saveFailed' => 'Unable to save the image. Please make sure the image and file directory are writable.', - 'invalidDirection' => 'Flip direction can be only `vertical` or `horizontal`. Given: {0}', + 'invalidDirection' => 'Flip direction can be only "vertical" or "horizontal". Given: "{0}"', 'exifNotSupported' => 'Reading EXIF data is not supported by this PHP installation.', ]; diff --git a/system/Language/en/Log.php b/system/Language/en/Log.php index 154d402b6c95..d6ee779a5fe9 100644 --- a/system/Language/en/Log.php +++ b/system/Language/en/Log.php @@ -11,6 +11,6 @@ // Log language settings return [ - 'invalidLogLevel' => '{0} is an invalid log level.', + 'invalidLogLevel' => '"{0}" is an invalid log level.', 'invalidMessageType' => 'The given message type "{0}" is not supported.', ]; diff --git a/system/Language/en/Pager.php b/system/Language/en/Pager.php index 81e6d2627cf5..884ea8e05aab 100644 --- a/system/Language/en/Pager.php +++ b/system/Language/en/Pager.php @@ -18,6 +18,6 @@ 'last' => 'Last', 'older' => 'Older', 'newer' => 'Newer', - 'invalidTemplate' => '{0} is not a valid Pager template.', - 'invalidPaginationGroup' => '{0} is not a valid Pagination group.', + 'invalidTemplate' => '"{0}" is not a valid Pager template.', + 'invalidPaginationGroup' => '"{0}" is not a valid Pagination group.', ]; diff --git a/system/Language/en/Publisher.php b/system/Language/en/Publisher.php index f335b98423f7..3eb74a3f202f 100644 --- a/system/Language/en/Publisher.php +++ b/system/Language/en/Publisher.php @@ -11,12 +11,12 @@ // Publisher language settings return [ - 'collision' => 'Publisher encountered an unexpected {0} while copying {1} to {2}.', - 'destinationNotAllowed' => 'Destination is not on the allowed list of Publisher directories: {0}', - 'fileNotAllowed' => '{0} fails the following restriction for {1}: {2}', + 'collision' => 'Publisher encountered an unexpected "{0}" while copying "{1}" to "{2}".', + 'destinationNotAllowed' => 'Destination is not on the allowed list of Publisher directories: "{0}"', + 'fileNotAllowed' => '"{0}" fails the following restriction for "{1}": {2}', // Publish Command 'publishMissing' => 'No Publisher classes detected in {0} across all namespaces.', - 'publishSuccess' => '{0} published {1} file(s) to {2}.', - 'publishFailure' => '{0} failed to publish to {1}!', + 'publishSuccess' => '"{0}" published {1} file(s) to "{2}".', + 'publishFailure' => '"{0}" failed to publish to "{1}".', ]; diff --git a/system/Language/en/Router.php b/system/Language/en/Router.php index 7d0b5cc99e25..cb90612abdd2 100644 --- a/system/Language/en/Router.php +++ b/system/Language/en/Router.php @@ -13,6 +13,6 @@ return [ 'invalidParameter' => 'A parameter does not match the expected type.', 'missingDefaultRoute' => 'Unable to determine what should be displayed. A default route has not been specified in the routing file.', - 'invalidDynamicController' => 'A dynamic controller is not allowed for security reasons. Route handler: {0}', - 'invalidControllerName' => 'The namespace delimiter is a backslash (\), not a slash (/). Route handler: {0}', + 'invalidDynamicController' => 'A dynamic controller is not allowed for security reasons. Route handler: "{0}"', + 'invalidControllerName' => 'The namespace delimiter is a backslash (\), not a slash (/). Route handler: "{0}"', ]; diff --git a/system/Language/en/Security.php b/system/Language/en/Security.php index 5abec93db367..d7f8a652e0b6 100644 --- a/system/Language/en/Security.php +++ b/system/Language/en/Security.php @@ -14,5 +14,5 @@ 'disallowedAction' => 'The action you requested is not allowed.', // @deprecated - 'invalidSameSite' => 'The SameSite value must be None, Lax, Strict, or a blank string. Given: {0}', + 'invalidSameSite' => 'The SameSite value must be None, Lax, Strict, or a blank string. Given: "{0}"', ]; diff --git a/system/Language/en/Session.php b/system/Language/en/Session.php index 527d2e9c7cea..03705fce2c3d 100644 --- a/system/Language/en/Session.php +++ b/system/Language/en/Session.php @@ -15,8 +15,8 @@ 'invalidSavePath' => 'Session: Configured save path "{0}" is not a directory, does not exist or cannot be created.', 'writeProtectedSavePath' => 'Session: Configured save path "{0}" is not writable by the PHP process.', 'emptySavePath' => 'Session: No save path configured.', - 'invalidSavePathFormat' => 'Session: Invalid Redis save path format: {0}', + 'invalidSavePathFormat' => 'Session: Invalid Redis save path format: "{0}"', // @deprecated - 'invalidSameSiteSetting' => 'Session: The SameSite setting must be None, Lax, Strict, or a blank string. Given: {0}', + 'invalidSameSiteSetting' => 'Session: The SameSite setting must be None, Lax, Strict, or a blank string. Given: "{0}"', ]; diff --git a/system/Language/en/Test.php b/system/Language/en/Test.php index a3f5d7ef6f02..d54b9188a668 100644 --- a/system/Language/en/Test.php +++ b/system/Language/en/Test.php @@ -11,5 +11,5 @@ // Testing language settings return [ - 'invalidMockClass' => '{0} is not a valid Mock class', + 'invalidMockClass' => '"{0}" is not a valid Mock class', ]; diff --git a/system/Language/en/Validation.php b/system/Language/en/Validation.php index 2b870eb4085f..76535c80f085 100644 --- a/system/Language/en/Validation.php +++ b/system/Language/en/Validation.php @@ -12,11 +12,11 @@ // Validation language settings return [ // Core Messages - 'noRuleSets' => 'No rulesets specified in Validation configuration.', - 'ruleNotFound' => '{0} is not a valid rule.', - 'groupNotFound' => '{0} is not a validation rules group.', - 'groupNotArray' => '{0} rule group must be an array.', - 'invalidTemplate' => '{0} is not a valid Validation template.', + 'noRuleSets' => 'No rule sets specified in Validation configuration.', + 'ruleNotFound' => '"{0}" is not a valid rule.', + 'groupNotFound' => '"{0}" is not a validation rules group.', + 'groupNotArray' => '"{0}" rule group must be an array.', + 'invalidTemplate' => '"{0}" is not a valid Validation template.', // Rule Messages 'alpha' => 'The {field} field may only contain alphabetical characters.', @@ -31,7 +31,7 @@ 'exact_length' => 'The {field} field must be exactly {param} characters in length.', 'greater_than' => 'The {field} field must contain a number greater than {param}.', 'greater_than_equal_to' => 'The {field} field must contain a number greater than or equal to {param}.', - 'hex' => 'The {field} field may only contain hexidecimal characters.', + 'hex' => 'The {field} field may only contain hexadecimal characters.', 'in_list' => 'The {field} field must be one of: {param}.', 'integer' => 'The {field} field must contain an integer.', 'is_natural' => 'The {field} field must only contain digits.', diff --git a/system/Language/en/View.php b/system/Language/en/View.php index ac94be50fd82..cd6b6c294ade 100644 --- a/system/Language/en/View.php +++ b/system/Language/en/View.php @@ -13,9 +13,9 @@ return [ 'invalidCellMethod' => '{class}::{method} is not a valid method.', 'missingCellParameters' => '{class}::{method} has no params.', - 'invalidCellParameter' => '{0} is not a valid param name.', + 'invalidCellParameter' => '"{0}" is not a valid param name.', 'noCellClass' => 'No view cell class provided.', - 'invalidCellClass' => 'Unable to locate view cell class: {0}.', - 'tagSyntaxError' => 'You have a syntax error in your Parser tags: {0}', - 'invalidDecoratorClass' => '{0} is not a valid View Decorator.', + 'invalidCellClass' => 'Unable to locate view cell class: "{0}".', + 'tagSyntaxError' => 'You have a syntax error in your Parser tags: "{0}"', + 'invalidDecoratorClass' => '"{0}" is not a valid View Decorator.', ]; diff --git a/system/Log/Handlers/BaseHandler.php b/system/Log/Handlers/BaseHandler.php index 97d1281deb1a..c19ecb0b8066 100644 --- a/system/Log/Handlers/BaseHandler.php +++ b/system/Log/Handlers/BaseHandler.php @@ -47,17 +47,6 @@ public function canHandle(string $level): bool return in_array($level, $this->handles, true); } - /** - * Handles logging the message. - * If the handler returns false, then execution of handlers - * will stop. Any handlers that have not run, yet, will not - * be run. - * - * @param string $level - * @param string $message - */ - abstract public function handle($level, $message): bool; - /** * Stores the date format to use while logging messages. */ diff --git a/system/Model.php b/system/Model.php index c8012871ed0f..edd3294c390b 100644 --- a/system/Model.php +++ b/system/Model.php @@ -40,6 +40,7 @@ * * @property BaseConnection $db * + * @method $this groupBy($by, ?bool $escape = null) * @method $this havingIn(?string $key = null, $values = null, ?bool $escape = null) * @method $this havingLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false) * @method $this havingNotIn(?string $key = null, $values = null, ?bool $escape = null) @@ -680,7 +681,7 @@ public function insert($data = null, bool $returnID = true) { if (! empty($this->tempData['data'])) { if (empty($data)) { - $data = $this->tempData['data'] ?? null; + $data = $this->tempData['data']; } else { $data = $this->transformDataToArray($data, 'insert'); $data = array_merge($this->tempData['data'], $data); @@ -706,7 +707,7 @@ public function update($id = null, $data = null): bool { if (! empty($this->tempData['data'])) { if (empty($data)) { - $data = $this->tempData['data'] ?? null; + $data = $this->tempData['data']; } else { $data = $this->transformDataToArray($data, 'update'); $data = array_merge($this->tempData['data'], $data); diff --git a/system/Publisher/Publisher.php b/system/Publisher/Publisher.php index 502d3670ff12..88459153188f 100644 --- a/system/Publisher/Publisher.php +++ b/system/Publisher/Publisher.php @@ -87,9 +87,9 @@ class Publisher extends FileCollection */ protected $destination = FCPATH; - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Support Methods - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Discovers and returns all Publishers in the specified namespace directory. @@ -145,9 +145,9 @@ private static function wipeDirectory(string $directory): void } } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Class Core - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Loads the helper and verifies the source and destination directories. @@ -203,9 +203,9 @@ public function publish(): bool return $this->addPath('/')->merge(true); } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Property Accessors - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Returns the source directory. @@ -258,9 +258,9 @@ final public function getPublished(): array return $this->published; } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Additional Handlers - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Verifies and adds paths to the list. @@ -324,9 +324,9 @@ final public function addUri(string $uri) return $this->addFile($file); } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Write Methods - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Removes the destination and all its files and folders. diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 34989d54a171..8567c17f03b3 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -14,9 +14,11 @@ use Closure; use CodeIgniter\Autoloader\FileLocator; use CodeIgniter\Router\Exceptions\RouterException; +use Config\App; use Config\Modules; use Config\Services; use InvalidArgumentException; +use Locale; /** * @todo Implement nested resource routing (See CakePHP) @@ -1016,7 +1018,8 @@ public function environment(string $env, Closure $callback): RouteCollectionInte * reverseRoute('Controller::method', $param1, $param2); * * @param string $search Named route or Controller::method - * @param int|string ...$params One or more parameters to be passed to the route + * @param int|string ...$params One or more parameters to be passed to the route. + * The last parameter allows you to set the locale. * * @return false|string */ @@ -1025,9 +1028,7 @@ public function reverseRoute(string $search, ...$params) // Named routes get higher priority. foreach ($this->routes as $collection) { if (array_key_exists($search, $collection)) { - $route = $this->fillRouteParams(key($collection[$search]['route']), $params); - - return $this->localizeRoute($route); + return $this->buildReverseRoute(key($collection[$search]['route']), $params); } } @@ -1069,9 +1070,7 @@ public function reverseRoute(string $search, ...$params) continue; } - $route = $this->fillRouteParams($from, $params); - - return $this->localizeRoute($route); + return $this->buildReverseRoute($from, $params); } } @@ -1081,6 +1080,8 @@ public function reverseRoute(string $search, ...$params) /** * Replaces the {locale} tag with the current application locale + * + * @deprecated Unused. */ protected function localizeRoute(string $route): string { @@ -1145,6 +1146,8 @@ public function getFiltersForRoute(string $search, ?string $verb = null): array * Given a * * @throws RouterException + * + * @deprecated Unused. Now uses buildReverseRoute(). */ protected function fillRouteParams(string $from, ?array $params = null): string { @@ -1171,6 +1174,78 @@ protected function fillRouteParams(string $from, ?array $params = null): string return '/' . ltrim($from, '/'); } + /** + * Builds reverse route + * + * @param array $params One or more parameters to be passed to the route. + * The last parameter allows you to set the locale. + */ + protected function buildReverseRoute(string $from, array $params): string + { + $locale = null; + + // Find all of our back-references in the original route + preg_match_all('/\(([^)]+)\)/', $from, $matches); + + if (empty($matches[0])) { + if (strpos($from, '{locale}') !== false) { + $locale = $params[0] ?? null; + } + + $from = $this->replaceLocale($from, $locale); + + return '/' . ltrim($from, '/'); + } + + // Locale is passed? + $placeholderCount = count($matches[0]); + if (count($params) > $placeholderCount) { + $locale = $params[$placeholderCount]; + } + + // Build our resulting string, inserting the $params in + // the appropriate places. + foreach ($matches[0] as $index => $pattern) { + if (! preg_match('#^' . $pattern . '$#u', $params[$index])) { + throw RouterException::forInvalidParameterType(); + } + + // Ensure that the param we're inserting matches + // the expected param type. + $pos = strpos($from, $pattern); + $from = substr_replace($from, $params[$index], $pos, strlen($pattern)); + } + + $from = $this->replaceLocale($from, $locale); + + return '/' . ltrim($from, '/'); + } + + /** + * Replaces the {locale} tag with the locale + */ + private function replaceLocale(string $route, ?string $locale = null): string + { + if (strpos($route, '{locale}') === false) { + return $route; + } + + // Check invalid locale + if ($locale !== null) { + /** @var App $config */ + $config = config('App'); + if (! in_array($locale, $config->supportedLocales, true)) { + $locale = null; + } + } + + if ($locale === null) { + $locale = Services::request()->getLocale(); + } + + return strtr($route, ['{locale}' => $locale]); + } + /** * Does the heavy lifting of creating an actual route. You must specify * the request method(s) that this route will work for. They can be separated diff --git a/system/Router/Router.php b/system/Router/Router.php index c653608d08c3..e524fb33e26b 100644 --- a/system/Router/Router.php +++ b/system/Router/Router.php @@ -441,7 +441,6 @@ protected function checkRoutes(string $uri): bool if ($this->collection->shouldUseSupportedLocalesOnly() && ! in_array($matched['locale'], config('App')->supportedLocales, true)) { - // Throw exception to prevent the autorouter, if enabled, // from trying to find a route throw PageNotFoundException::forLocaleNotSupported($matched['locale']); @@ -467,7 +466,7 @@ protected function checkRoutes(string $uri): bool return true; } - [$controller, ] = explode('::', $handler); + [$controller] = explode('::', $handler); // Checks `/` in controller name if (strpos($controller, '/') !== false) { diff --git a/system/Security/Exceptions/SecurityException.php b/system/Security/Exceptions/SecurityException.php index ed118d95d5dd..68683c324730 100644 --- a/system/Security/Exceptions/SecurityException.php +++ b/system/Security/Exceptions/SecurityException.php @@ -12,8 +12,9 @@ namespace CodeIgniter\Security\Exceptions; use CodeIgniter\Exceptions\FrameworkException; +use CodeIgniter\Exceptions\HTTPExceptionInterface; -class SecurityException extends FrameworkException +class SecurityException extends FrameworkException implements HTTPExceptionInterface { public static function forDisallowedAction() { diff --git a/system/Security/Security.php b/system/Security/Security.php index 36f4a730da5f..a2e517a40843 100644 --- a/system/Security/Security.php +++ b/system/Security/Security.php @@ -13,6 +13,7 @@ use CodeIgniter\Cookie\Cookie; use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\Request; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\Response; use CodeIgniter\Security\Exceptions\SecurityException; @@ -123,7 +124,7 @@ class Security implements SecurityInterface * * @var bool */ - protected $redirect = true; + protected $redirect = false; /** * CSRF SameSite @@ -321,6 +322,8 @@ public function verify(RequestInterface $request) */ private function removeTokenInRequest(RequestInterface $request): void { + assert($request instanceof Request); + $json = json_decode($request->getBody() ?? ''); if (isset($_POST[$this->tokenName])) { @@ -336,6 +339,8 @@ private function removeTokenInRequest(RequestInterface $request): void private function getPostedToken(RequestInterface $request): ?string { + assert($request instanceof IncomingRequest); + // Does the token exist in POST, HEADER or optionally php:://input - json data. if ($request->hasHeader($this->headerName) && ! empty($request->header($this->headerName)->getValue())) { $tokenName = $request->header($this->headerName)->getValue(); @@ -580,6 +585,8 @@ private function saveHashInCookie(): void */ protected function sendCookie(RequestInterface $request) { + assert($request instanceof IncomingRequest); + if ($this->cookie->isSecure() && ! $request->isSecure()) { return false; } diff --git a/system/Test/CIUnitTestCase.php b/system/Test/CIUnitTestCase.php index b5a105c6886b..213bcf5886c6 100644 --- a/system/Test/CIUnitTestCase.php +++ b/system/Test/CIUnitTestCase.php @@ -71,9 +71,9 @@ abstract class CIUnitTestCase extends TestCase */ private ?array $traits = null; - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Database Properties - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Should run db migration? @@ -166,9 +166,9 @@ abstract class CIUnitTestCase extends TestCase */ protected $insertCache = []; - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Feature Properties - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * If present, will override application @@ -216,9 +216,9 @@ abstract class CIUnitTestCase extends TestCase */ protected $requestBody = ''; - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Staging - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Load the helpers. @@ -289,9 +289,9 @@ private function callTraitMethods(string $stage): void } } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Mocking - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Resets shared instanced for all Factories components @@ -338,9 +338,9 @@ protected function mockSession() Services::injectMock('session', $session); } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Assertions - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Custom function to hook into CodeIgniter's Logging mechanism @@ -496,9 +496,9 @@ public function assertCloseEnoughString($expected, $actual, string $message = '' } } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Utility - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Loads up an instance of CodeIgniter diff --git a/system/Test/DatabaseTestTrait.php b/system/Test/DatabaseTestTrait.php index 539627e29189..f7a6d532a1a6 100644 --- a/system/Test/DatabaseTestTrait.php +++ b/system/Test/DatabaseTestTrait.php @@ -42,9 +42,9 @@ trait DatabaseTestTrait */ private static $doneSeed = false; - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Staging - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Runs the trait set up methods. @@ -89,9 +89,9 @@ public function loadDependencies() } } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Migrations - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Migrate on setUp @@ -163,9 +163,9 @@ protected function migrateDatabase() } } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Seeds - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Seed on setUp @@ -205,9 +205,9 @@ public function seed(string $name) $this->seeder->call($name); } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Utility - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Reset $doneMigration and $doneSeed @@ -264,9 +264,9 @@ public function grabFromDatabase(string $table, string $column, array $where) return $query->{$column} ?? false; } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Assertions - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Asserts that records that match the conditions in $where DO diff --git a/system/Test/FilterTestTrait.php b/system/Test/FilterTestTrait.php index 465648f4f6b3..3bcd37962941 100644 --- a/system/Test/FilterTestTrait.php +++ b/system/Test/FilterTestTrait.php @@ -78,9 +78,9 @@ trait FilterTestTrait */ protected $collection; - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Staging - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Initializes dependencies once. @@ -108,9 +108,9 @@ protected function setUpFilterTestTrait(): void $this->doneFilterSetUp = true; } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Utility - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Returns a callable method for a filter position @@ -182,9 +182,9 @@ protected function getFiltersForRoute(string $route, string $position): array return $aliases[$position]; } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Assertions - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Asserts that the given route at position uses diff --git a/system/Test/Filters/CITestStreamFilter.php b/system/Test/Filters/CITestStreamFilter.php index 564ec6fb51d1..edcd4dbfd7d6 100644 --- a/system/Test/Filters/CITestStreamFilter.php +++ b/system/Test/Filters/CITestStreamFilter.php @@ -28,6 +28,16 @@ class CITestStreamFilter extends php_user_filter protected static bool $registered = false; + /** + * @var resource|null + */ + private static $err; + + /** + * @var resource|null + */ + private static $out; + /** * This method is called whenever data is read from or written to the * attached stream (such as with fread() or fwrite()). @@ -56,4 +66,37 @@ public static function registration(): void static::$buffer = ''; } + + public static function addErrorFilter(): void + { + self::removeFilter(self::$err); + self::$err = stream_filter_append(STDERR, 'CITestStreamFilter'); + } + + public static function addOutputFilter(): void + { + self::removeFilter(self::$out); + self::$out = stream_filter_append(STDOUT, 'CITestStreamFilter'); + } + + public static function removeErrorFilter(): void + { + self::removeFilter(self::$err); + } + + public static function removeOutputFilter(): void + { + self::removeFilter(self::$out); + } + + /** + * @param resource $stream + */ + protected static function removeFilter(&$stream): void + { + if (is_resource($stream)) { + stream_filter_remove($stream); + $stream = null; + } + } } diff --git a/system/Test/Mock/MockCache.php b/system/Test/Mock/MockCache.php index ebdacccbbef5..69afc0e47039 100644 --- a/system/Test/Mock/MockCache.php +++ b/system/Test/Mock/MockCache.php @@ -236,9 +236,9 @@ public function isSupported(): bool return true; } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Test Helpers - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Instructs the class to ignore all @@ -255,9 +255,9 @@ public function bypass(bool $bypass = true) return $this; } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Additional Assertions - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Asserts that the cache has an item named $key. diff --git a/system/Test/Mock/MockConnection.php b/system/Test/Mock/MockConnection.php index f515550569a9..16693f260308 100644 --- a/system/Test/Mock/MockConnection.php +++ b/system/Test/Mock/MockConnection.php @@ -139,7 +139,7 @@ public function getVersion(): string /** * Executes the query against the database. * - * @return mixed + * @return bool|object */ protected function execute(string $sql) { diff --git a/system/Test/StreamFilterTrait.php b/system/Test/StreamFilterTrait.php index 732c6afdcc78..873576ec1425 100644 --- a/system/Test/StreamFilterTrait.php +++ b/system/Test/StreamFilterTrait.php @@ -15,86 +15,17 @@ trait StreamFilterTrait { - /** - * @var resource|null - */ - private $outputStreamFilterResource; - - /** - * @var resource|null - */ - private $errorStreamFilterResource; - protected function setUpStreamFilterTrait(): void { - $this->registerStreamFilterClass() - ->appendOutputStreamFilter() - ->appendErrorStreamFilter(); + CITestStreamFilter::registration(); + CITestStreamFilter::addOutputFilter(); + CITestStreamFilter::addErrorFilter(); } protected function tearDownStreamFilterTrait(): void { - $this->removeOutputStreamFilter()->removeErrorStreamFilter(); - } - - /** - * @return $this - */ - protected function appendOutputStreamFilter() - { - $this->removeOutputStreamFilter(); - - $this->outputStreamFilterResource = stream_filter_append(STDOUT, 'CITestStreamFilter'); - - return $this; - } - - /** - * @return $this - */ - protected function appendErrorStreamFilter() - { - $this->removeErrorStreamFilter(); - - $this->errorStreamFilterResource = stream_filter_append(STDERR, 'CITestStreamFilter'); - - return $this; - } - - /** - * @return $this - */ - protected function removeOutputStreamFilter() - { - if (is_resource($this->outputStreamFilterResource)) { - stream_filter_remove($this->outputStreamFilterResource); - $this->outputStreamFilterResource = null; - } - - return $this; - } - - /** - * @return $this - */ - protected function removeErrorStreamFilter() - { - if (is_resource($this->errorStreamFilterResource)) { - stream_filter_remove($this->errorStreamFilterResource); - $this->errorStreamFilterResource = null; - } - - return $this; - } - - /** - * @return $this - */ - protected function registerStreamFilterClass() - { - CITestStreamFilter::registration(); - - return $this; + CITestStreamFilter::removeOutputFilter(); + CITestStreamFilter::removeErrorFilter(); } protected function getStreamFilterBuffer(): string diff --git a/system/Test/TestResponse.php b/system/Test/TestResponse.php index d91f00080b14..16a50f74d1e5 100644 --- a/system/Test/TestResponse.php +++ b/system/Test/TestResponse.php @@ -60,9 +60,9 @@ public function __construct(ResponseInterface $response) $this->setResponse($response); } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Getters / Setters - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Sets the request. @@ -114,9 +114,9 @@ public function response() return $this->response; } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Status Checks - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Boils down the possible responses into a boolean valid/not-valid @@ -165,9 +165,9 @@ public function assertNotOK() $this->assertFalse($this->isOK(), "{$this->response->getStatusCode()} is an unexpected successful status code, or the Response has body content."); } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Redirection - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Returns whether or not the Response was a redirect or RedirectResponse @@ -239,9 +239,9 @@ public function getRedirectUrl(): ?string return null; } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Session - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Asserts that an SESSION key has been set and, optionally, test it's value. @@ -275,9 +275,9 @@ public function assertSessionMissing(string $key) $this->assertArrayNotHasKey($key, $_SESSION, "'{$key}' should not be present in \$_SESSION."); } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Headers - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Asserts that the Response contains a specific header. @@ -305,9 +305,9 @@ public function assertHeaderMissing(string $key) $this->assertFalse($this->response->hasHeader($key), "'{$key}' should not be in the Response headers."); } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // Cookies - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Asserts that the response has the specified cookie. @@ -340,9 +340,9 @@ public function assertCookieExpired(string $key, string $prefix = '') $this->assertGreaterThan(time(), $this->response->getCookie($key, $prefix)->getExpiresTimestamp()); } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // JSON - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Returns the response's body as JSON @@ -401,9 +401,9 @@ public function assertJSONExact($test) $this->assertJsonStringEqualsJsonString($test, $json, 'Response does not contain matching JSON.'); } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // XML Methods - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Returns the response' body as XML @@ -415,9 +415,9 @@ public function getXML() return $this->response->getXML(); } - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- // DomParser - //-------------------------------------------------------------------- + // -------------------------------------------------------------------- /** * Assert that the desired text can be found in the result body. diff --git a/system/Test/bootstrap.php b/system/Test/bootstrap.php index 5eeaae6071ab..3145725afe96 100644 --- a/system/Test/bootstrap.php +++ b/system/Test/bootstrap.php @@ -28,9 +28,7 @@ defined('HOMEPATH') || define('HOMEPATH', realpath(rtrim(getcwd(), '\\/ ')) . DIRECTORY_SEPARATOR); $source = is_dir(HOMEPATH . 'app') ? HOMEPATH - : (is_dir('vendor/codeigniter4/framework/') - ? 'vendor/codeigniter4/framework/' - : 'vendor/codeigniter4/codeigniter4/'); + : (is_dir('vendor/codeigniter4/framework/') ? 'vendor/codeigniter4/framework/' : 'vendor/codeigniter4/codeigniter4/'); defined('CONFIGPATH') || define('CONFIGPATH', realpath($source . 'app/Config') . DIRECTORY_SEPARATOR); defined('PUBLICPATH') || define('PUBLICPATH', realpath($source . 'public') . DIRECTORY_SEPARATOR); unset($source); diff --git a/system/ThirdParty/Kint/Kint.php b/system/ThirdParty/Kint/Kint.php index e2145b062cf4..701283c2848d 100644 --- a/system/ThirdParty/Kint/Kint.php +++ b/system/ThirdParty/Kint/Kint.php @@ -150,6 +150,7 @@ class Kint 'Kint\\Parser\\ClosurePlugin', 'Kint\\Parser\\ColorPlugin', 'Kint\\Parser\\DateTimePlugin', + 'Kint\\Parser\\EnumPlugin', 'Kint\\Parser\\FsPathPlugin', 'Kint\\Parser\\IteratorPlugin', 'Kint\\Parser\\JsonPlugin', diff --git a/system/ThirdParty/Kint/Parser/ClassStaticsPlugin.php b/system/ThirdParty/Kint/Parser/ClassStaticsPlugin.php index 89601af2f9f7..7d2673f849d2 100644 --- a/system/ThirdParty/Kint/Parser/ClassStaticsPlugin.php +++ b/system/ThirdParty/Kint/Parser/ClassStaticsPlugin.php @@ -30,6 +30,7 @@ use Kint\Zval\Value; use ReflectionClass; use ReflectionProperty; +use UnitEnum; class ClassStaticsPlugin extends Plugin { @@ -56,6 +57,11 @@ public function parse(&$var, Value &$o, $trigger) $consts = []; foreach ($reflection->getConstants() as $name => $val) { + // Skip enum constants + if ($var instanceof UnitEnum && $val instanceof UnitEnum && $o->classname == \get_class($val)) { + continue; + } + $const = Value::blank($name, '\\'.$class.'::'.$name); $const->const = true; $const->depth = $o->depth + 1; diff --git a/system/ThirdParty/Kint/Parser/EnumPlugin.php b/system/ThirdParty/Kint/Parser/EnumPlugin.php new file mode 100644 index 000000000000..3fe25f4eb516 --- /dev/null +++ b/system/ThirdParty/Kint/Parser/EnumPlugin.php @@ -0,0 +1,86 @@ +contents = []; + + foreach ($var->cases() as $case) { + $base_obj = Value::blank($class.'::'.$case->name, '\\'.$class.'::'.$case->name); + $base_obj->depth = $o->depth + 1; + + if ($var instanceof BackedEnum) { + $c = $case->value; + $cases->contents[] = $this->parser->parse($c, $base_obj); + } else { + $cases->contents[] = $base_obj; + } + } + + self::$cache[$class] = $cases; + } + + $object = new EnumValue($var); + $object->transplant($o); + + $object->addRepresentation(self::$cache[$class], 0); + + $o = $object; + } +} diff --git a/system/ThirdParty/Kint/Renderer/Text/EnumPlugin.php b/system/ThirdParty/Kint/Renderer/Text/EnumPlugin.php new file mode 100644 index 000000000000..efb5ba5d193f --- /dev/null +++ b/system/ThirdParty/Kint/Renderer/Text/EnumPlugin.php @@ -0,0 +1,44 @@ +depth) { + $out .= $this->renderer->colorTitle($this->renderer->renderTitle($o)).PHP_EOL; + } + + $out .= $this->renderer->renderHeader($o).PHP_EOL; + + return $out; + } +} diff --git a/system/ThirdParty/Kint/Renderer/TextRenderer.php b/system/ThirdParty/Kint/Renderer/TextRenderer.php index 0cfba527c78b..3421e132b21e 100644 --- a/system/ThirdParty/Kint/Renderer/TextRenderer.php +++ b/system/ThirdParty/Kint/Renderer/TextRenderer.php @@ -42,6 +42,7 @@ class TextRenderer extends Renderer 'microtime' => 'Kint\\Renderer\\Text\\MicrotimePlugin', 'recursion' => 'Kint\\Renderer\\Text\\RecursionPlugin', 'trace' => 'Kint\\Renderer\\Text\\TracePlugin', + 'enum' => 'Kint\\Renderer\\Text\\EnumPlugin', ]; /** @@ -55,6 +56,7 @@ class TextRenderer extends Renderer 'Kint\\Parser\\MicrotimePlugin', 'Kint\\Parser\\StreamPlugin', 'Kint\\Parser\\TracePlugin', + 'Kint\\Parser\\EnumPlugin', ]; /** diff --git a/system/ThirdParty/Kint/Zval/EnumValue.php b/system/ThirdParty/Kint/Zval/EnumValue.php new file mode 100644 index 000000000000..018eb68f0541 --- /dev/null +++ b/system/ThirdParty/Kint/Zval/EnumValue.php @@ -0,0 +1,62 @@ +enumval = $enumval; + } + + public function getValueShort() + { + if ($this->enumval instanceof BackedEnum) { + if (\is_string($this->enumval->value)) { + return '"'.$this->enumval->value.'"'; + } + if (\is_int($this->enumval->value)) { + return (string) $this->enumval->value; + } + } + } + + public function getType() + { + return $this->classname.'::'.$this->enumval->name; + } + + public function getSize() + { + } +} diff --git a/system/ThirdParty/Kint/resources/compiled/rich.js b/system/ThirdParty/Kint/resources/compiled/rich.js index f15d38ff0616..2f0ef6a1d0e2 100644 --- a/system/ThirdParty/Kint/resources/compiled/rich.js +++ b/system/ThirdParty/Kint/resources/compiled/rich.js @@ -1 +1 @@ -void 0===window.kintRich&&(window.kintRich=function(){"use strict";var l={selectText:function(e){var t=window.getSelection(),a=document.createRange();a.selectNodeContents(e),t.removeAllRanges(),t.addRange(a)},toggle:function(e,t){var a=l.getChildren(e);a&&(e.classList.toggle("kint-show",t),1===a.childNodes.length&&(a=a.childNodes[0].childNodes[0])&&a.classList&&a.classList.contains("kint-parent")&&l.toggle(a,t))},toggleChildren:function(e,t){var a=l.getChildren(e);if(a){var o=a.getElementsByClassName("kint-parent"),n=o.length;for(void 0===t&&(t=e.classList.contains("kint-show"));n--;)l.toggle(o[n],t)}},switchTab:function(e){var t=e.previousSibling,a=0;for(e.parentNode.getElementsByClassName("kint-active-tab")[0].classList.remove("kint-active-tab"),e.classList.add("kint-active-tab");t;)1===t.nodeType&&a++,t=t.previousSibling;for(var o=e.parentNode.nextSibling.childNodes,n=0;n"},openInNewWindow:function(e){var t=window.open();t&&(t.document.open(),t.document.write(l.mktag("html")+l.mktag("head")+l.mktag("title")+"Kint ("+(new Date).toISOString()+")"+l.mktag("/title")+l.mktag('meta charset="utf-8"')+l.mktag('script class="kint-rich-script" nonce="'+l.script.nonce+'"')+l.script.innerHTML+l.mktag("/script")+l.mktag('style class="kint-rich-style" nonce="'+l.style.nonce+'"')+l.style.innerHTML+l.mktag("/style")+l.mktag("/head")+l.mktag("body")+'
'+e.parentNode.outerHTML+"
"+l.mktag("/body")),t.document.close())},sortTable:function(e,a){var t=e.tBodies[0];[].slice.call(e.tBodies[0].rows).sort(function(e,t){if(e=e.cells[a].textContent.trim().toLocaleLowerCase(),t=t.cells[a].textContent.trim().toLocaleLowerCase(),isNaN(e)||isNaN(t)){if(isNaN(e)&&!isNaN(t))return 1;if(isNaN(t)&&!isNaN(e))return-1}else e=parseFloat(e),t=parseFloat(t);return eli:not(.kint-active-tab)").forEach(function(e){l.isFolderOpen()&&!l.folder.contains(e)||0===e.offsetWidth&&0===e.offsetHeight||l.keyboardNav.targets.push(e)}),e&&-1!==l.keyboardNav.targets.indexOf(e)&&(l.keyboardNav.target=l.keyboardNav.targets.indexOf(e))},sync:function(e){var t=document.querySelector(".kint-focused");t&&t.classList.remove("kint-focused"),l.keyboardNav.active&&((t=l.keyboardNav.targets[l.keyboardNav.target]).classList.add("kint-focused"),e||l.keyboardNav.scroll(t))},scroll:function(e){var t,a;e!==l.folder.querySelector("dt > nav")&&(a=(t=function(e){return e.offsetTop+(e.offsetParent?t(e.offsetParent):0)})(e),l.isFolderOpen()?(e=l.folder.querySelector("dd.kint-foldout")).scrollTo(0,a-e.clientHeight/2):window.scrollTo(0,a-window.innerHeight/2))},moveCursor:function(e){for(l.keyboardNav.target+=e;l.keyboardNav.target<0;)l.keyboardNav.target+=l.keyboardNav.targets.length;for(;l.keyboardNav.target>=l.keyboardNav.targets.length;)l.keyboardNav.target-=l.keyboardNav.targets.length;l.keyboardNav.sync()},setCursor:function(e){if(l.isFolderOpen()&&!l.folder.contains(e))return!1;l.keyboardNav.fetchTargets();for(var t=0;t"},openInNewWindow:function(e){var t=window.open();t&&(t.document.open(),t.document.write(l.mktag("html")+l.mktag("head")+l.mktag("title")+"Kint ("+(new Date).toISOString()+")"+l.mktag("/title")+l.mktag('meta charset="utf-8"')+l.mktag('script class="kint-rich-script" nonce="'+l.script.nonce+'"')+l.script.innerHTML+l.mktag("/script")+l.mktag('style class="kint-rich-style" nonce="'+l.style.nonce+'"')+l.style.innerHTML+l.mktag("/style")+l.mktag("/head")+l.mktag("body")+'
'+e.parentNode.outerHTML+"
"+l.mktag("/body")),t.document.close())},sortTable:function(e,a){var t=e.tBodies[0];[].slice.call(e.tBodies[0].rows).sort(function(e,t){if(e=e.cells[a].textContent.trim().toLocaleLowerCase(),t=t.cells[a].textContent.trim().toLocaleLowerCase(),isNaN(e)||isNaN(t)){if(isNaN(e)&&!isNaN(t))return 1;if(isNaN(t)&&!isNaN(e))return-1}else e=parseFloat(e),t=parseFloat(t);return eli:not(.kint-active-tab)").forEach(function(e){l.isFolderOpen()&&!l.folder.contains(e)||0===e.offsetWidth&&0===e.offsetHeight||l.keyboardNav.targets.push(e)}),e&&-1!==l.keyboardNav.targets.indexOf(e)&&(l.keyboardNav.target=l.keyboardNav.targets.indexOf(e))},sync:function(e){var t=document.querySelector(".kint-focused");t&&t.classList.remove("kint-focused"),l.keyboardNav.active&&((t=l.keyboardNav.targets[l.keyboardNav.target]).classList.add("kint-focused"),e||l.keyboardNav.scroll(t))},scroll:function(e){var t,a;e!==l.folder.querySelector("dt > nav")&&(a=(t=function(e){return e.offsetTop+(e.offsetParent?t(e.offsetParent):0)})(e),l.isFolderOpen()?(e=l.folder.querySelector("dd.kint-foldout")).scrollTo(0,a-e.clientHeight/2):window.scrollTo(0,a-window.innerHeight/2))},moveCursor:function(e){for(l.keyboardNav.target+=e;l.keyboardNav.target<0;)l.keyboardNav.target+=l.keyboardNav.targets.length;for(;l.keyboardNav.target>=l.keyboardNav.targets.length;)l.keyboardNav.target-=l.keyboardNav.targets.length;l.keyboardNav.sync()},setCursor:function(e){if(l.isFolderOpen()&&!l.folder.contains(e))return!1;l.keyboardNav.fetchTargets();for(var t=0;trequest = $request; } diff --git a/system/Validation/StrictRules/Rules.php b/system/Validation/StrictRules/Rules.php index 8861ab727852..af8970122f5c 100644 --- a/system/Validation/StrictRules/Rules.php +++ b/system/Validation/StrictRules/Rules.php @@ -71,20 +71,36 @@ public function exact_length($str, string $val): bool /** * Greater than * - * @param mixed $str + * @param mixed $str expects int|string */ public function greater_than($str, string $min): bool { + if (is_int($str)) { + $str = (string) $str; + } + + if (! is_string($str)) { + return false; + } + return $this->nonStrictRules->greater_than($str, $min); } /** * Equal to or Greater than * - * @param mixed $str + * @param mixed $str expects int|string */ public function greater_than_equal_to($str, string $min): bool { + if (is_int($str)) { + $str = (string) $str; + } + + if (! is_string($str)) { + return false; + } + return $this->nonStrictRules->greater_than_equal_to($str, $min); } @@ -141,20 +157,36 @@ public function is_unique($str, string $field, array $data): bool /** * Less than * - * @param mixed $str + * @param mixed $str expects int|string */ public function less_than($str, string $max): bool { + if (is_int($str)) { + $str = (string) $str; + } + + if (! is_string($str)) { + return false; + } + return $this->nonStrictRules->less_than($str, $max); } /** * Equal to or Less than * - * @param mixed $str + * @param mixed $str expects int|string */ public function less_than_equal_to($str, string $max): bool { + if (is_int($str)) { + $str = (string) $str; + } + + if (! is_string($str)) { + return false; + } + return $this->nonStrictRules->less_than_equal_to($str, $max); } diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php index 7e4a55db55cf..1ff080185294 100644 --- a/system/Validation/Validation.php +++ b/system/Validation/Validation.php @@ -108,12 +108,6 @@ public function __construct($config, RendererInterface $view) */ public function run(?array $data = null, ?string $group = null, ?string $dbGroup = null): bool { - // If there are still validation errors for redirect_with_input request, remove them. - // See `getErrors()` method. - if (isset($_SESSION, $_SESSION['_ci_validation_errors'])) { - unset($_SESSION['_ci_validation_errors']); - } - $data ??= $this->data; // i.e. is_unique @@ -168,7 +162,7 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup if (strpos($field, '*') !== false) { // Process multiple fields foreach ($values as $dotField => $value) { - $this->processRules($dotField, $setup['label'] ?? $field, $value, $rules, $data); + $this->processRules($dotField, $setup['label'] ?? $field, $value, $rules, $data, $field); } } else { // Process single field @@ -201,10 +195,17 @@ public function check($value, string $rule, array $errors = []): bool * * @param array|string $value * @param array|null $rules - * @param array $data + * @param array $data The array of data to validate, with `DBGroup`. + * @param string|null $originalField The original asterisk field name like "foo.*.bar". */ - protected function processRules(string $field, ?string $label, $value, $rules = null, ?array $data = null): bool - { + protected function processRules( + string $field, + ?string $label, + $value, + $rules = null, + ?array $data = null, + ?string $originalField = null + ): bool { if ($data === null) { throw new InvalidArgumentException('You must supply the parameter: data.'); } @@ -321,7 +322,9 @@ protected function processRules(string $field, ?string $label, $value, $rules = if ($passed === false) { // if the $value is an array, convert it to as string representation if (is_array($value)) { - $value = '[' . implode(', ', $value) . ']'; + $value = $this->isStringList($value) + ? '[' . implode(', ', $value) . ']' + : json_encode($value); } elseif (is_object($value)) { $value = json_encode($value); } @@ -333,7 +336,8 @@ protected function processRules(string $field, ?string $label, $value, $rules = $field, $label, $param, - (string) $value + (string) $value, + $originalField ); return false; @@ -343,6 +347,32 @@ protected function processRules(string $field, ?string $label, $value, $rules = return true; } + /** + * Is the array a string list `list`? + */ + private function isStringList(array $array): bool + { + $expectedKey = 0; + + foreach ($array as $key => $val) { + // Note: also covers PHP array key conversion, e.g. '5' and 5.1 both become 5 + if (! is_int($key)) { + return false; + } + + if ($key !== $expectedKey) { + return false; + } + $expectedKey++; + + if (! is_string($val)) { + return false; + } + } + + return true; + } + /** * Takes a Request object and grabs the input data to use from its * array values. @@ -506,6 +536,8 @@ public function setRuleGroup(string $group) /** * Returns the rendered HTML of the errors as defined in $template. + * + * You can also use validation_list_errors() in Form helper. */ public function listErrors(string $template = 'list'): string { @@ -520,6 +552,8 @@ public function listErrors(string $template = 'list'): string /** * Displays a single error in formatted HTML as defined in the $template view. + * + * You can also use validation_show_error() in Form helper. */ public function showError(string $field, string $template = 'single'): string { @@ -681,14 +715,7 @@ public function getError(?string $field = null): string */ public function getErrors(): array { - // If we already have errors, we'll use those. - // If we don't, check the session to see if any were - // passed along from a redirect_with_input request. - if (empty($this->errors) && ! is_cli() && isset($_SESSION, $_SESSION['_ci_validation_errors'])) { - $this->errors = unserialize($_SESSION['_ci_validation_errors']); - } - - return $this->errors ?? []; + return $this->errors; } /** @@ -706,13 +733,21 @@ public function setError(string $field, string $error): ValidationInterface * * @param string|null $value The value that caused the validation to fail. */ - protected function getErrorMessage(string $rule, string $field, ?string $label = null, ?string $param = null, ?string $value = null): string - { + protected function getErrorMessage( + string $rule, + string $field, + ?string $label = null, + ?string $param = null, + ?string $value = null, + ?string $originalField = null + ): string { $param ??= ''; // Check if custom message has been defined by user if (isset($this->customErrors[$field][$rule])) { $message = lang($this->customErrors[$field][$rule]); + } elseif (null !== $originalField && isset($this->customErrors[$originalField][$rule])) { + $message = lang($this->customErrors[$originalField][$rule]); } else { // Try to grab a localized version of the message... // lang() will return the rule name back if not found, diff --git a/system/bootstrap.php b/system/bootstrap.php index 421b6ca6b7d6..41736590a927 100644 --- a/system/bootstrap.php +++ b/system/bootstrap.php @@ -103,6 +103,7 @@ // Initialize and register the loader with the SPL autoloader stack. Services::autoloader()->initialize(new Autoload(), new Modules())->register(); +Services::autoloader()->loadHelpers(); // Now load Composer's if it's available if (is_file(COMPOSER_PATH)) { @@ -117,6 +118,3 @@ require_once COMPOSER_PATH; } - -// Always load the URL helper, it should be used in most of apps. -helper('url'); diff --git a/tests/AutoReview/ComposerJsonTest.php b/tests/AutoReview/ComposerJsonTest.php index 2d205c5fc872..39763b079eca 100644 --- a/tests/AutoReview/ComposerJsonTest.php +++ b/tests/AutoReview/ComposerJsonTest.php @@ -20,29 +20,35 @@ * @internal * * @coversNothing + * * @group auto-review */ final class ComposerJsonTest extends TestCase { - public function testFrameworkRequireIsTheSameWithDevRequire(): void + private array $devComposer; + private array $frameworkComposer; + + protected function setUp(): void { - $devComposer = $this->getComposerJson(dirname(__DIR__, 2) . '/composer.json'); - $frameworkComposer = $this->getComposerJson(dirname(__DIR__, 2) . '/admin/framework/composer.json'); + parent::setUp(); + $this->devComposer = $this->getComposerJson(dirname(__DIR__, 2) . '/composer.json'); + $this->frameworkComposer = $this->getComposerJson(dirname(__DIR__, 2) . '/admin/framework/composer.json'); + } + + public function testFrameworkRequireIsTheSameWithDevRequire(): void + { $this->assertSame( - $devComposer['require'], - $frameworkComposer['require'], + $this->devComposer['require'], + $this->frameworkComposer['require'], 'The framework\'s "require" section is not updated with the main composer.json.' ); } public function testFrameworkRequireDevIsTheSameWithDevRequireDev(): void { - $devComposer = $this->getComposerJson(dirname(__DIR__, 2) . '/composer.json'); - $frameworkComposer = $this->getComposerJson(dirname(__DIR__, 2) . '/admin/framework/composer.json'); - - $devRequireDev = $devComposer['require-dev']; - $fwRequireDev = $frameworkComposer['require-dev']; + $devRequireDev = $this->devComposer['require-dev']; + $fwRequireDev = $this->frameworkComposer['require-dev']; foreach ($devRequireDev as $dependency => $expectedVersion) { if (! isset($fwRequireDev[$dependency])) { @@ -62,6 +68,15 @@ public function testFrameworkRequireDevIsTheSameWithDevRequireDev(): void } } + public function testFrameworkSuggestIsTheSameWithDevSuggest(): void + { + $this->assertSame( + $this->devComposer['suggest'], + $this->frameworkComposer['suggest'], + 'The framework\'s "suggest" section is not updated with the main composer.json.' + ); + } + private function getComposerJson(string $path): array { try { diff --git a/tests/README.md b/tests/README.md index 5a7ddbbbed72..6ea177ddb588 100644 --- a/tests/README.md +++ b/tests/README.md @@ -34,9 +34,24 @@ for code coverage to be calculated successfully. After installing `XDebug`, you ## Setting Up A number of the tests use a running database. -In order to set up the database edit the details for the `tests` group in -**app/Config/Database.php** or **phpunit.xml**. +The default configuration uses SQLite3 transient in-memory database, so it works if you can use SQLite3. + +In order to change the database for testing, edit the details for the `tests` group in +**app/Config/Database.php** or **phpunit.xml** or use **.env** file. + +E.g.: +``` +database.tests.hostname = localhost +database.tests.database = ci4_test +database.tests.username = root +database.tests.password = root +database.tests.DBDriver = MySQLi +database.tests.DBPrefix = db_ +database.default.port = 3306 +``` + Make sure that you provide a database engine that is currently running on your machine. + More details on a test database setup are in the [Testing Your Database](https://codeigniter4.github.io/CodeIgniter4/testing/database.html) section of the documentation. diff --git a/tests/_support/Validation/TestRules.php b/tests/_support/Validation/TestRules.php index 507d17ef89bf..e44b9d459fc7 100644 --- a/tests/_support/Validation/TestRules.php +++ b/tests/_support/Validation/TestRules.php @@ -32,4 +32,9 @@ public function check_object_rule(object $value, ?string $fields, array $data = return $find; } + + public function array_count($value, $count): bool + { + return is_array($value) && count($value) === (int) $count; + } } diff --git a/tests/system/API/ResponseTraitTest.php b/tests/system/API/ResponseTraitTest.php index d463f0bd4d03..0222fd0c0058 100644 --- a/tests/system/API/ResponseTraitTest.php +++ b/tests/system/API/ResponseTraitTest.php @@ -43,19 +43,19 @@ protected function makeController(array $userConfig = [], string $uri = 'http:// $config = new App(); foreach ([ - 'baseURL' => 'http://example.com/', - 'uriProtocol' => 'REQUEST_URI', - 'defaultLocale' => 'en', - 'negotiateLocale' => false, + 'baseURL' => 'http://example.com/', + 'uriProtocol' => 'REQUEST_URI', + 'defaultLocale' => 'en', + 'negotiateLocale' => false, 'supportedLocales' => ['en'], - 'CSPEnabled' => false, - 'cookiePrefix' => '', - 'cookieDomain' => '', - 'cookiePath' => '/', - 'cookieSecure' => false, - 'cookieHTTPOnly' => false, - 'proxyIPs' => [], - 'cookieSameSite' => 'Lax', + 'CSPEnabled' => false, + 'cookiePrefix' => '', + 'cookieDomain' => '', + 'cookiePath' => '/', + 'cookieSecure' => false, + 'cookieHTTPOnly' => false, + 'proxyIPs' => [], + 'cookieSameSite' => 'Lax', ] as $key => $value) { $config->{$key} = $value; } @@ -79,7 +79,7 @@ protected function makeController(array $userConfig = [], string $uri = 'http:// } // Create the controller class finally. - $controller = new class ($this->request, $this->response, $this->formatter) { + return new class ($this->request, $this->response, $this->formatter) { use ResponseTrait; protected $request; @@ -98,8 +98,6 @@ public function resetFormatter() $this->formatter = null; } }; - - return $controller; } public function testNoFormatterJSON() @@ -518,19 +516,19 @@ public function testFormatByRequestNegotiateIfFormatIsNotJsonOrXML() $config = new App(); foreach ([ - 'baseURL' => 'http://example.com/', - 'uriProtocol' => 'REQUEST_URI', - 'defaultLocale' => 'en', - 'negotiateLocale' => false, + 'baseURL' => 'http://example.com/', + 'uriProtocol' => 'REQUEST_URI', + 'defaultLocale' => 'en', + 'negotiateLocale' => false, 'supportedLocales' => ['en'], - 'CSPEnabled' => false, - 'cookiePrefix' => '', - 'cookieDomain' => '', - 'cookiePath' => '/', - 'cookieSecure' => false, - 'cookieHTTPOnly' => false, - 'proxyIPs' => [], - 'cookieSameSite' => 'Lax', + 'CSPEnabled' => false, + 'cookiePrefix' => '', + 'cookieDomain' => '', + 'cookiePath' => '/', + 'cookieSecure' => false, + 'cookieHTTPOnly' => false, + 'proxyIPs' => [], + 'cookieSameSite' => 'Lax', ] as $key => $value) { $config->{$key} = $value; } diff --git a/tests/system/Autoloader/AutoloaderTest.php b/tests/system/Autoloader/AutoloaderTest.php index 2e0224709e49..5ee59750bce6 100644 --- a/tests/system/Autoloader/AutoloaderTest.php +++ b/tests/system/Autoloader/AutoloaderTest.php @@ -49,6 +49,13 @@ protected function setUp(): void $this->loader->initialize($config, $modules)->register(); } + protected function tearDown(): void + { + $this->loader->unregister(); + + parent::tearDown(); + } + public function testLoadStoredClass() { $this->assertInstanceOf('UnnamespacedClass', new UnnamespacedClass()); @@ -98,10 +105,13 @@ public function testServiceAutoLoader() $autoloader = Services::autoloader(false); $autoloader->initialize(new Autoload(), new Modules()); $autoloader->register(); + // look for Home controller, as that should be in base repo $actual = $autoloader->loadClass(Home::class); $expected = APPPATH . 'Controllers' . DIRECTORY_SEPARATOR . 'Home.php'; $this->assertSame($expected, realpath($actual) ?: $actual); + + $autoloader->unregister(); } public function testExistingFile() @@ -252,10 +262,10 @@ public function testFindsComposerRoutes() $modules = new Modules(); $modules->discoverInComposer = true; - $this->loader = new Autoloader(); - $this->loader->initialize($config, $modules); + $loader = new Autoloader(); + $loader->initialize($config, $modules); - $namespaces = $this->loader->getNamespace(); + $namespaces = $loader->getNamespace(); $this->assertArrayHasKey('Laminas\\Escaper', $namespaces); } @@ -268,10 +278,10 @@ public function testComposerNamespaceDoesNotOverwriteConfigAutoloadPsr4() $modules = new Modules(); $modules->discoverInComposer = true; - $this->loader = new Autoloader(); - $this->loader->initialize($config, $modules); + $loader = new Autoloader(); + $loader->initialize($config, $modules); - $namespaces = $this->loader->getNamespace(); + $namespaces = $loader->getNamespace(); $this->assertSame('/Config/Autoload/Psr/Log/', $namespaces['Psr\Log'][0]); $this->assertStringContainsString(VENDORPATH, $namespaces['Psr\Log'][1]); } @@ -284,29 +294,50 @@ public function testFindsComposerRoutesWithComposerPathNotFound() $modules = new Modules(); $modules->discoverInComposer = true; - $this->loader = new Autoloader(); + $loader = new Autoloader(); rename(COMPOSER_PATH, COMPOSER_PATH . '.backup'); - $this->loader->initialize($config, $modules); + $loader->initialize($config, $modules); rename(COMPOSER_PATH . '.backup', $composerPath); - $namespaces = $this->loader->getNamespace(); + $namespaces = $loader->getNamespace(); $this->assertArrayNotHasKey('Laminas\\Escaper', $namespaces); } public function testAutoloaderLoadsNonClassFiles(): void { - $config = new Autoload(); - + $config = new Autoload(); $config->files[] = SUPPORTPATH . 'Autoloader/functions.php'; - $this->loader = new Autoloader(); - $this->loader->initialize($config, new Modules()); - $this->loader->register(); + $loader = new Autoloader(); + $loader->initialize($config, new Modules()); + $loader->register(); $this->assertTrue(function_exists('autoload_foo')); $this->assertSame('I am autoloaded by Autoloader through $files!', autoload_foo()); $this->assertTrue(defined('AUTOLOAD_CONSTANT')); $this->assertSame('foo', AUTOLOAD_CONSTANT); + + $loader->unregister(); + } + + /** + * @runInSeparateProcess + * + * @preserveGlobalState disabled + */ + public function testLoadHelpers(): void + { + $config = new Autoload(); + $config->helpers[] = 'form'; + + $loader = new Autoloader(); + $loader->initialize($config, new Modules()); + + $loader->loadHelpers(); + + $this->assertTrue(function_exists('form_open')); + + $loader->unregister(); } } diff --git a/tests/system/CLI/CLITest.php b/tests/system/CLI/CLITest.php index aec60c46aae0..3ae084557cee 100644 --- a/tests/system/CLI/CLITest.php +++ b/tests/system/CLI/CLITest.php @@ -126,7 +126,7 @@ public function testNewLine() public function testColorExceptionForeground() { $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Invalid foreground color: Foreground'); + $this->expectExceptionMessage('Invalid "foreground" color: "Foreground"'); CLI::color('test', 'Foreground'); } @@ -134,7 +134,7 @@ public function testColorExceptionForeground() public function testColorExceptionBackground() { $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Invalid background color: Background'); + $this->expectExceptionMessage('Invalid "background" color: "Background"'); CLI::color('test', 'white', 'Background'); } diff --git a/tests/system/Cache/Handlers/RedisHandlerTest.php b/tests/system/Cache/Handlers/RedisHandlerTest.php index a10d92171ec8..2c79be076eca 100644 --- a/tests/system/Cache/Handlers/RedisHandlerTest.php +++ b/tests/system/Cache/Handlers/RedisHandlerTest.php @@ -166,14 +166,22 @@ public function testDeleteMatchingSuffix() $this->assertSame('keys=90', $dbInfo[0]); } - // FIXME: I don't like all Hash logic very much. It's wasting memory. - // public function testIncrement() - // { - // } - - // public function testDecrement() - // { - // } + public function testIncrementAndDecrement() + { + $this->handler->save('counter', 100); + + foreach (range(1, 10) as $step) { + $this->handler->increment('counter', $step); + } + + $this->assertSame(155, $this->handler->get('counter')); + + $this->handler->decrement('counter', 20); + $this->assertSame(135, $this->handler->get('counter')); + + $this->handler->increment('counter', 5); + $this->assertSame(140, $this->handler->get('counter')); + } public function testClean() { diff --git a/tests/system/CodeIgniterTest.php b/tests/system/CodeIgniterTest.php index d1b6793e4e89..4b1938aa0293 100644 --- a/tests/system/CodeIgniterTest.php +++ b/tests/system/CodeIgniterTest.php @@ -18,6 +18,7 @@ use CodeIgniter\Test\Filters\CITestStreamFilter; use CodeIgniter\Test\Mock\MockCodeIgniter; use Config\App; +use Config\Cache; use Config\Filters; use Config\Modules; use Tests\Support\Filters\Customfilter; @@ -177,7 +178,7 @@ public function testControllersCanReturnResponseObject() $routes = Services::routes(); $routes->add('pages/(:segment)', static function ($segment) { $response = Services::response(); - $string = "You want to see 'about' page."; + $string = "You want to see 'about' page."; return $response->setBody($string); }); @@ -569,9 +570,9 @@ public function testSpoofRequestMethodCannotUseGET() public function testPageCacheSendSecureHeaders() { // Suppress command() output - CITestStreamFilter::$buffer = ''; - $outputStreamFilter = stream_filter_append(STDOUT, 'CITestStreamFilter'); - $errorStreamFilter = stream_filter_append(STDERR, 'CITestStreamFilter'); + CITestStreamFilter::registration(); + CITestStreamFilter::addErrorFilter(); + CITestStreamFilter::addOutputFilter(); // Clear Page cache command('cache:clear'); @@ -583,7 +584,7 @@ public function testPageCacheSendSecureHeaders() CodeIgniter::cache(3600); $response = Services::response(); - $string = 'This is a test page. Elapsed time: {elapsed_time}'; + $string = 'This is a test page. Elapsed time: {elapsed_time}'; return $response->setBody($string); }); @@ -618,8 +619,86 @@ public function testPageCacheSendSecureHeaders() // Clear Page cache command('cache:clear'); - // Remove stream fliters + // Remove stream filters + CITestStreamFilter::removeErrorFilter(); + CITestStreamFilter::removeOutputFilter(); + } + + /** + * @param array|bool $cacheQueryStringValue + * + * @dataProvider cacheQueryStringProvider + * + * @see https://github.com/codeigniter4/CodeIgniter4/pull/6410 + */ + public function testPageCacheWithCacheQueryString($cacheQueryStringValue, int $expectedPagesInCache, array $testingUrls) + { + // Suppress command() output + CITestStreamFilter::$buffer = ''; + $outputStreamFilter = stream_filter_append(STDOUT, 'CITestStreamFilter'); + $errorStreamFilter = stream_filter_append(STDERR, 'CITestStreamFilter'); + + // Create cache config with cacheQueryString value from the dataProvider + $cacheConfig = new Cache(); + $cacheConfig->cacheQueryString = $cacheQueryStringValue; + + // Clear cache before starting the test + command('cache:clear'); + + // Calculate amount of items in the cache before the test + $cache = \Config\Services::cache(); + $cacheStartCounter = count($cache->getCacheInfo()); + + // Generate request to each URL from the testing array + foreach ($testingUrls as $testingUrl) { + $_SERVER['REQUEST_URI'] = '/' . $testingUrl; + $routes = Services::routes(true); + $routes->add($testingUrl, static function () { + CodeIgniter::cache(0); // Dont cache the page in the run() function because CodeIgniter class will create default $cacheConfig and overwrite settings from the dataProvider + $response = Services::response(); + $string = 'This is a test page, to check cache configuration'; + + return $response->setBody($string); + }); + + // Inject router + $router = Services::router($routes, Services::request(null, false)); + Services::injectMock('router', $router); + + // Cache the page output using default caching function and $cacheConfig with value from the data provider + $this->codeigniter->useSafeOutput(true)->run(); + $this->codeigniter->cachePage($cacheConfig); // Cache the page using our own $cacheConfig confugration + } + + // Calculate how much cached items exist in the cache after the test requests + $cacheEndCounter = count($cache->getCacheInfo()); + $newPagesCached = $cacheEndCounter - $cacheStartCounter; + + // Clear cache after the test + command('cache:clear'); + + // Check that amount of new items created in the cache matching expected value from the data provider + $this->assertSame($expectedPagesInCache, $newPagesCached); + + // Remove stream filters stream_filter_remove($outputStreamFilter); stream_filter_remove($errorStreamFilter); } + + public function cacheQueryStringProvider(): array + { + $testingUrls = [ + 'test', // URL #1 + 'test?important_parameter=1', // URL #2 + 'test?important_parameter=2', // URL #3 + 'test?important_parameter=1¬_important_parameter=2', // URL #4 + 'test?important_parameter=1¬_important_parameter=2&another_not_important_parameter=3', // URL #5 + ]; + + return [ + '$cacheQueryString=false' => [false, 1, $testingUrls], // We expect only 1 page in the cache, because when cacheQueryString is set to false, all GET parameter should be ignored, and page URI will be absolutely same "/test" string for all 5 requests + '$cacheQueryString=true' => [true, 5, $testingUrls], // We expect all 5 pages in the cache, because when cacheQueryString is set to true, all GET parameter should be processed as unique requests + '$cacheQueryString=array' => [['important_parameter'], 3, $testingUrls], // We expect only 3 pages in the cache, because when cacheQueryString is set to array with important parameters, we should ignore all parameters thats not in the array. Only URL #1, URL #2 and URL #3 should be cached. URL #4 and URL #5 is duplication of URL #2 (with value ?important_parameter=1), so they should not be processed as new unique requests and application should return already cached page for URL #2 + ]; + } } diff --git a/tests/system/Commands/CommandTest.php b/tests/system/Commands/CommandTest.php index 7a8c39940dc0..d6ec8bea9ae6 100644 --- a/tests/system/Commands/CommandTest.php +++ b/tests/system/Commands/CommandTest.php @@ -81,7 +81,7 @@ public function testCommandCall() $command = new $commands['app:info']['class']($this->logger, $this->commands); $command->bomb(); - $this->assertStringContainsString('Invalid background color:', $this->getBuffer()); + $this->assertStringContainsString('Invalid "background" color:', $this->getBuffer()); } public function testAbstractCommand() diff --git a/tests/system/Commands/GenerateKeyTest.php b/tests/system/Commands/GenerateKeyTest.php index f69e7ad1afc9..2daff35caac1 100644 --- a/tests/system/Commands/GenerateKeyTest.php +++ b/tests/system/Commands/GenerateKeyTest.php @@ -81,6 +81,7 @@ public function testGenerateKeyShowsEncodedKey() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testGenerateKeyCreatesNewKey() diff --git a/tests/system/Commands/Utilities/NamespacesTest.php b/tests/system/Commands/Utilities/NamespacesTest.php index bd86522149d4..565fcc7f89dc 100644 --- a/tests/system/Commands/Utilities/NamespacesTest.php +++ b/tests/system/Commands/Utilities/NamespacesTest.php @@ -12,37 +12,30 @@ namespace CodeIgniter\Commands\Utilities; use CodeIgniter\Test\CIUnitTestCase; -use CodeIgniter\Test\Filters\CITestStreamFilter; +use CodeIgniter\Test\StreamFilterTrait; /** * @internal */ final class NamespacesTest extends CIUnitTestCase { - private $streamFilter; + use StreamFilterTrait; protected function setUp(): void { $this->resetServices(); parent::setUp(); - - CITestStreamFilter::$buffer = ''; - - $this->streamFilter = stream_filter_append(STDOUT, 'CITestStreamFilter'); - $this->streamFilter = stream_filter_append(STDERR, 'CITestStreamFilter'); } protected function tearDown(): void { - stream_filter_remove($this->streamFilter); - $this->resetServices(); } protected function getBuffer() { - return CITestStreamFilter::$buffer; + return $this->getStreamFilterBuffer(); } public function testNamespacesCommandCodeIgniterOnly() diff --git a/tests/system/CommonFunctionsSendTest.php b/tests/system/CommonFunctionsSendTest.php index 594a0faab4db..03e675cfbfa5 100644 --- a/tests/system/CommonFunctionsSendTest.php +++ b/tests/system/CommonFunctionsSendTest.php @@ -32,6 +32,7 @@ protected function setUp(): void * See https://github.com/codeigniter4/CodeIgniter4/issues/1393 * * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testRedirectResponseCookiesSent() diff --git a/tests/system/CommonFunctionsTest.php b/tests/system/CommonFunctionsTest.php index 73cc7e38e93a..faa261190125 100644 --- a/tests/system/CommonFunctionsTest.php +++ b/tests/system/CommonFunctionsTest.php @@ -179,6 +179,7 @@ public function testEscapeBadContext() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testSessionInstance() @@ -190,6 +191,7 @@ public function testSessionInstance() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testSessionVariable() @@ -203,6 +205,7 @@ public function testSessionVariable() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testSessionVariableNotThere() @@ -222,6 +225,39 @@ public function testRouteTo() $this->assertSame('/path/string/to/13', route_to('myController::goto', 'string', 13)); } + public function testRouteToInCliWithoutLocaleInRoute() + { + Services::createRequest(new App(), true); + $routes = service('routes'); + $routes->add('path/(:any)/to/(:num)', 'myController::goto/$1/$2'); + + $this->assertSame('/path/string/to/13', route_to('myController::goto', 'string', 13)); + } + + public function testRouteToInCliWithLocaleInRoute() + { + Services::createRequest(new App(), true); + $routes = service('routes'); + $routes->add('{locale}/path/(:any)/to/(:num)', 'myController::goto/$1/$2', ['as' => 'path-to']); + + $this->assertSame( + '/en/path/string/to/13', + route_to('path-to', 'string', 13, 'en') + ); + } + + public function testRouteToWithUnsupportedLocale() + { + Services::createRequest(new App(), false); + $routes = service('routes'); + $routes->add('{locale}/path/(:any)/to/(:num)', 'myController::goto/$1/$2', ['as' => 'path-to']); + + $this->assertSame( + '/en/path/string/to/13', + route_to('path-to', 'string', 13, 'invalid') + ); + } + public function testInvisible() { $this->assertSame('Javascript', remove_invisible_characters("Java\0script")); @@ -286,6 +322,7 @@ public function testModelExistsAbsoluteClassname() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testOldInput() @@ -321,6 +358,7 @@ public function testOldInput() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testOldInputSerializeData() @@ -354,7 +392,9 @@ public function testOldInputSerializeData() /** * @see https://github.com/codeigniter4/CodeIgniter4/issues/1492 + * * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testOldInputArray() @@ -468,6 +508,7 @@ public function testRedirectResponseCookies1() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testTrace() @@ -491,6 +532,7 @@ public function testViewNotSaveData() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testForceHttpsNullRequestAndResponse() @@ -571,6 +613,7 @@ public function testDWithCSP() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testTraceWithCSP() diff --git a/tests/system/Config/BaseConfigTest.php b/tests/system/Config/BaseConfigTest.php index d1de7532cb39..9434e0a2c557 100644 --- a/tests/system/Config/BaseConfigTest.php +++ b/tests/system/Config/BaseConfigTest.php @@ -80,6 +80,7 @@ public function testUseDefaultValueTypeStringValue() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testServerValues() @@ -176,6 +177,7 @@ public function testSetsDefaultValues() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testSetsDefaultValuesEncryptionUsingHex2Bin() @@ -191,6 +193,7 @@ public function testSetsDefaultValuesEncryptionUsingHex2Bin() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testSetDefaultValuesEncryptionUsingBase64() diff --git a/tests/system/Config/ConfigTest.php b/tests/system/Config/ConfigTest.php index e5d382527edb..af996652a92c 100644 --- a/tests/system/Config/ConfigTest.php +++ b/tests/system/Config/ConfigTest.php @@ -55,6 +55,7 @@ public function testCreateNonConfig() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testInjection() diff --git a/tests/system/Config/DotEnvTest.php b/tests/system/Config/DotEnvTest.php index aa718bd6dea6..0b9dcc984e5a 100644 --- a/tests/system/Config/DotEnvTest.php +++ b/tests/system/Config/DotEnvTest.php @@ -67,6 +67,7 @@ public function testLoadsVars() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testLoadsHex2Bin() @@ -81,6 +82,7 @@ public function testLoadsHex2Bin() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testLoadsBase64() diff --git a/tests/system/Config/ServicesTest.php b/tests/system/Config/ServicesTest.php index 96ce95089389..8f375d398484 100644 --- a/tests/system/Config/ServicesTest.php +++ b/tests/system/Config/ServicesTest.php @@ -238,6 +238,7 @@ public function testNewViewcell() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testNewSession() @@ -248,6 +249,7 @@ public function testNewSession() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testNewSessionWithNullConfig() @@ -258,6 +260,7 @@ public function testNewSessionWithNullConfig() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testCallStatic() @@ -272,6 +275,7 @@ public function testCallStatic() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testCallStaticDirectly() @@ -283,6 +287,7 @@ public function testCallStaticDirectly() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testMockInjection() @@ -305,6 +310,7 @@ public function testMockInjection() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testReset() @@ -324,6 +330,7 @@ public function testReset() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testResetSingle() diff --git a/tests/system/ControllerTest.php b/tests/system/ControllerTest.php index cc77c7507b15..b78f2c69cd2e 100644 --- a/tests/system/ControllerTest.php +++ b/tests/system/ControllerTest.php @@ -75,7 +75,7 @@ public function testConstructorHTTPS() $original = $_SERVER; $_SERVER = ['HTTPS' => 'on']; // make sure we can instantiate one - $this->controller = new class () extends Controller { + $this->controller = new class () extends Controller { protected $forceHTTPS = 1; }; $this->controller->initController($this->request, $this->response, $this->logger); @@ -182,7 +182,7 @@ public function testValidateData() public function testHelpers() { - $this->controller = new class () extends Controller { + $this->controller = new class () extends Controller { protected $helpers = [ 'cookie', 'text', diff --git a/tests/system/Database/BaseConnectionTest.php b/tests/system/Database/BaseConnectionTest.php index 3c1be631d73e..33f70bbe7521 100644 --- a/tests/system/Database/BaseConnectionTest.php +++ b/tests/system/Database/BaseConnectionTest.php @@ -106,7 +106,7 @@ public function testCanConnectToFailoverWhenNoConnectionAvailable() $options = $this->options; $options['failover'] = [$this->failoverOptions]; - $db = new class ($options) extends MockConnection { + $db = new class ($options) extends MockConnection { protected $returnValues = [ 'connect' => [false, 345], ]; diff --git a/tests/system/Database/Builder/InsertTest.php b/tests/system/Database/Builder/InsertTest.php index 774dd1082ed0..17f544965e1e 100644 --- a/tests/system/Database/Builder/InsertTest.php +++ b/tests/system/Database/Builder/InsertTest.php @@ -13,6 +13,7 @@ use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Database\Query; +use CodeIgniter\Database\RawSql; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Mock\MockConnection; @@ -85,6 +86,21 @@ public function testInsertObject() $this->assertSame($expectedBinds, $builder->getBinds()); } + public function testInsertObjectWithRawSql() + { + $builder = $this->db->table('jobs'); + + $insertData = (object) [ + 'id' => 1, + 'name' => new RawSql('CONCAT("id", \'Grocery Sales\')'), + ]; + $builder->testMode()->insert($insertData, true); + + $expectedSQL = 'INSERT INTO "jobs" ("id", "name") VALUES (1, CONCAT("id", \'Grocery Sales\'))'; + + $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledInsert())); + } + /** * @see https://github.com/codeigniter4/CodeIgniter4/issues/5365 */ diff --git a/tests/system/Database/Builder/PrefixTest.php b/tests/system/Database/Builder/PrefixTest.php index 35455d4856f9..082d316a54b4 100644 --- a/tests/system/Database/Builder/PrefixTest.php +++ b/tests/system/Database/Builder/PrefixTest.php @@ -11,6 +11,7 @@ namespace CodeIgniter\Database\Builder; +use CodeIgniter\Database\BaseBuilder; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Mock\MockConnection; @@ -49,4 +50,40 @@ public function testPrefixesSetOnTableNamesWithWhereClause() $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); $this->assertSame($expectedBinds, $builder->getBinds()); } + + public function testPrefixWithSubquery(): void + { + $expected = <<<'NOWDOC' + SELECT "u"."id", "u"."name", (SELECT 1 FROM "ci_users" "sub" WHERE "sub"."id" = "u"."id") "one" + FROM "ci_users" "u" + WHERE "u"."id" = 1 + NOWDOC; + + $subquery = $this->db->table('users sub') + ->select('1', false) + ->where('sub.id = u.id'); + + $builder = $this->db->table('users u') + ->select('u.id, u.name') + ->selectSubquery($subquery, 'one') + ->where('u.id', 1); + + $this->assertSame($expected, $builder->getCompiledSelect()); + } + + public function testPrefixWithNewQuery(): void + { + $expectedSQL = <<<'NOWDOC' + SELECT "users_1"."id", "name" + FROM (SELECT "u"."id", "u"."name" FROM "ci_users" "u") "users_1" + WHERE "users_1"."id" > 10 + NOWDOC; + + $subquery = (new BaseBuilder('users u', $this->db))->select('u.id, u.name'); + $builder = $this->db->newQuery()->fromSubquery($subquery, 'users_1') + ->select('users_1.id, name') + ->where('users_1.id > ', 10); + + $this->assertSame($expectedSQL, $builder->getCompiledSelect()); + } } diff --git a/tests/system/Database/Builder/SelectTest.php b/tests/system/Database/Builder/SelectTest.php index 19dff466cf03..ed6204f2d2be 100644 --- a/tests/system/Database/Builder/SelectTest.php +++ b/tests/system/Database/Builder/SelectTest.php @@ -258,7 +258,7 @@ public function testSelectMinThrowsExceptionOnEmptyValue() $builder = new BaseBuilder('invoices', $this->db); $this->expectException(DataException::class); - $this->expectExceptionMessage('Empty statement is given for the field `Select`'); + $this->expectExceptionMessage('Empty statement is given for the field "Select"'); $builder->selectSum(''); } @@ -279,7 +279,7 @@ public function testSelectMinThrowsExceptionOnMultipleColumn() $builder = new BaseBuilder('users', $this->db); $this->expectException(DataException::class); - $this->expectExceptionMessage('You must provide a valid column name not separated by comma.'); + $this->expectExceptionMessage('You must provide a valid "column name not separated by comma".'); $builder->selectSum('name,role'); } diff --git a/tests/system/Database/Builder/WhenTest.php b/tests/system/Database/Builder/WhenTest.php new file mode 100644 index 000000000000..d37dd80d8667 --- /dev/null +++ b/tests/system/Database/Builder/WhenTest.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Database\Builder; + +use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\Mock\MockConnection; + +/** + * @internal + */ +final class WhenTest extends CIUnitTestCase +{ + /** + * @var MockConnection + */ + protected MockConnection $db; + + protected function setUp(): void + { + parent::setUp(); + + $this->db = new MockConnection([]); + } + + public function testWhenTrue() + { + $builder = $this->db->table('jobs'); + + $expectedSQL = 'SELECT * FROM "jobs"'; + $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); + + $builder = $builder->when(true, static function ($query) { + $query->select('id'); + }); + + $expectedSQL = 'SELECT "id" FROM "jobs"'; + $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); + } + + public function testWhenTruthy() + { + $builder = $this->db->table('jobs'); + + $builder = $builder->when('abc', static function ($query) { + $query->select('id'); + }); + + $expectedSQL = 'SELECT "id" FROM "jobs"'; + $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); + } + + public function testWhenRunsDefaultWhenFalse() + { + $builder = $this->db->table('jobs'); + + $builder = $builder->when(false, static function ($query) { + $query->select('id'); + }, static function ($query) { + $query->select('name'); + }); + + $expectedSQL = 'SELECT "name" FROM "jobs"'; + $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); + } + + public function testWhenDoesntModifyWhenFalse() + { + $builder = $this->db->table('jobs'); + + $builder = $builder->when(false, static function ($query) { + $query->select('id'); + }); + + $expectedSQL = 'SELECT * FROM "jobs"'; + $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); + } + + public function testWhenPassesParemeters() + { + $builder = $this->db->table('jobs'); + $name = 'developer'; + + $builder = $builder->when($name, static function ($query, $name) { + $query->where('name', $name); + }); + + $expectedSQL = 'SELECT * FROM "jobs" WHERE "name" = \'developer\''; + $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); + } +} diff --git a/tests/system/Database/Live/EscapeTest.php b/tests/system/Database/Live/EscapeTest.php index 8aacd092ff16..552a6efeff1f 100644 --- a/tests/system/Database/Live/EscapeTest.php +++ b/tests/system/Database/Live/EscapeTest.php @@ -11,6 +11,7 @@ namespace CodeIgniter\Database\Live; +use CodeIgniter\Database\RawSql; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\DatabaseTestTrait; @@ -78,4 +79,22 @@ public function testEscapeLikeStringDirect() $this->expectNotToPerformAssertions(); } } + + public function testEscapeStringArray() + { + $stringArray = [' A simple string ', new RawSql('CURRENT_TIMESTAMP()'), false, null]; + + $escapedString = $this->db->escape($stringArray); + + $this->assertSame("' A simple string '", $escapedString[0]); + $this->assertSame('CURRENT_TIMESTAMP()', $escapedString[1]); + + if ($this->db->DBDriver === 'Postgre') { + $this->assertSame('FALSE', $escapedString[2]); + } else { + $this->assertSame(0, $escapedString[2]); + } + + $this->assertSame('NULL', $escapedString[3]); + } } diff --git a/tests/system/Database/Live/ForgeTest.php b/tests/system/Database/Live/ForgeTest.php index fff12563b6b6..32fc9bfcd402 100644 --- a/tests/system/Database/Live/ForgeTest.php +++ b/tests/system/Database/Live/ForgeTest.php @@ -637,7 +637,7 @@ public function testCompositeForeignKey() public function testCompositeForeignKeyFieldNotExistException() { $this->expectException(DatabaseException::class); - $this->expectExceptionMessage('Field `user_id, user_second_id` not found.'); + $this->expectExceptionMessage('Field "user_id, user_second_id" not found.'); $attributes = []; @@ -689,7 +689,7 @@ public function testCompositeForeignKeyFieldNotExistException() public function testForeignKeyFieldNotExistException() { $this->expectException(DatabaseException::class); - $this->expectExceptionMessage('Field `user_id` not found.'); + $this->expectExceptionMessage('Field "user_id" not found.'); $attributes = []; @@ -866,64 +866,182 @@ public function testAddFields() $this->assertIsArray($fieldsNames); - $fields = ['id', 'name', 'username', 'active']; - $this->assertContains($fieldsData[0]->name, $fields); - $this->assertContains($fieldsData[1]->name, $fields); - unset($fields); - if ($this->db->DBDriver === 'MySQLi') { - $this->assertSame('int', $fieldsData[0]->type); - $this->assertSame('varchar', $fieldsData[1]->type); + $expected = [ + 0 => [ + 'name' => 'id', + 'type' => 'int', + 'max_length' => 11, + 'nullable' => false, + 'default' => null, + 'primary_key' => 1, + ], + 1 => [ + 'name' => 'username', + 'type' => 'varchar', + 'max_length' => 255, + 'nullable' => false, + 'default' => null, + 'primary_key' => 0, + ], + 2 => [ + 'name' => 'name', + 'type' => 'varchar', + 'max_length' => 255, + 'nullable' => false, + 'default' => null, + 'primary_key' => 0, + ], + 3 => [ + 'name' => 'active', + 'type' => 'int', + 'max_length' => 11, + 'nullable' => false, + 'default' => '0', + 'primary_key' => 0, + ], + ]; - if (version_compare($this->db->getVersion(), '8.0.17', '<')) { + if (version_compare($this->db->getVersion(), '8.0.17', '>=')) { // As of MySQL 8.0.17, the display width attribute for integer data types // is deprecated and is not reported back anymore. // @see https://dev.mysql.com/doc/refman/8.0/en/numeric-type-attributes.html - $this->assertSame(11, $fieldsData[0]->max_length); + $expected[0]['max_length'] = null; + $expected[3]['max_length'] = null; } - - $this->assertNull($fieldsData[0]->default); - $this->assertNull($fieldsData[1]->default); - - $this->assertSame(1, (int) $fieldsData[0]->primary_key); - - $this->assertSame(255, (int) $fieldsData[1]->max_length); } elseif ($this->db->DBDriver === 'Postgre') { - $this->assertSame('integer', $fieldsData[0]->type); - $this->assertSame('character varying', $fieldsData[1]->type); - - $this->assertFalse($fieldsData[0]->nullable); - $this->assertFalse($fieldsData[1]->nullable); - - $this->assertSame(32, (int) $fieldsData[0]->max_length); - $this->assertSame(255, (int) $fieldsData[1]->max_length); - - $this->assertNull($fieldsData[1]->default); + $expected = [ + 0 => [ + 'name' => 'id', + 'type' => 'integer', + 'nullable' => false, + 'default' => "nextval('db_forge_test_fields_id_seq'::regclass)", + 'max_length' => '32', + ], + 1 => [ + 'name' => 'username', + 'type' => 'character varying', + 'nullable' => false, + 'default' => null, + 'max_length' => '255', + ], + 2 => [ + 'name' => 'name', + 'type' => 'character varying', + 'nullable' => false, + 'default' => null, + 'max_length' => '255', + ], + 3 => [ + 'name' => 'active', + 'type' => 'integer', + 'nullable' => false, + 'default' => '0', + 'max_length' => '32', + ], + ]; } elseif ($this->db->DBDriver === 'SQLite3') { - $this->assertSame('integer', strtolower($fieldsData[0]->type)); - $this->assertSame('varchar', strtolower($fieldsData[1]->type)); - - $this->assertNull($fieldsData[1]->default); + $expected = [ + 0 => [ + 'name' => 'id', + 'type' => 'INTEGER', + 'max_length' => null, + 'default' => null, + 'primary_key' => true, + 'nullable' => true, + ], + 1 => [ + 'name' => 'username', + 'type' => 'VARCHAR', + 'max_length' => null, + 'default' => null, + 'primary_key' => false, + 'nullable' => false, + ], + 2 => [ + 'name' => 'name', + 'type' => 'VARCHAR', + 'max_length' => null, + 'default' => null, + 'primary_key' => false, + 'nullable' => false, + ], + 3 => [ + 'name' => 'active', + 'type' => 'INTEGER', + 'max_length' => null, + 'default' => '0', + 'primary_key' => false, + 'nullable' => false, + ], + ]; } elseif ($this->db->DBDriver === 'SQLSRV') { - $this->assertSame('int', $fieldsData[0]->type); - $this->assertSame('varchar', $fieldsData[1]->type); - - $this->assertSame(10, (int) $fieldsData[0]->max_length); - $this->assertSame(255, (int) $fieldsData[1]->max_length); - - $this->assertNull($fieldsData[1]->default); + $expected = [ + 0 => [ + 'name' => 'id', + 'type' => 'int', + 'default' => null, + 'max_length' => 10, + ], + 1 => [ + 'name' => 'username', + 'type' => 'varchar', + 'default' => null, + 'max_length' => 255, + ], + 2 => [ + 'name' => 'name', + 'type' => 'varchar', + 'default' => null, + 'max_length' => 255, + ], + 3 => [ + 'name' => 'active', + 'type' => 'int', + 'default' => '((0))', // Why? + 'max_length' => 10, + ], + ]; } elseif ($this->db->DBDriver === 'OCI8') { - // Check types - $this->assertSame('NUMBER', $fieldsData[0]->type); - $this->assertSame('VARCHAR2', $fieldsData[1]->type); - - $this->assertSame('11', $fieldsData[0]->max_length); - $this->assertSame('255', $fieldsData[1]->max_length); + $expected = [ + 0 => [ + 'name' => 'id', + 'type' => 'NUMBER', + 'max_length' => '11', + 'default' => '"ORACLE"."ISEQ$$_80229".nextval', // Sequence id may change + 'nullable' => false, + ], + 1 => [ + 'name' => 'username', + 'type' => 'VARCHAR2', + 'max_length' => '255', + 'default' => '', + 'nullable' => false, + ], + 2 => [ + 'name' => 'name', + 'type' => 'VARCHAR2', + 'max_length' => '255', + 'default' => '', + 'nullable' => false, + ], + 3 => [ + 'name' => 'active', + 'type' => 'NUMBER', + 'max_length' => '11', + 'default' => '0 ', // Why? + 'nullable' => false, + ], + ]; - $this->assertSame('', $fieldsData[1]->default); + // Sequence id may change + $this->assertMatchesRegularExpression('/"ORACLE"."ISEQ\\$\\$_\d+".nextval/', $fieldsData[0]->default); + $expected[0]['default'] = $fieldsData[0]->default; } else { $this->fail(sprintf('DB driver "%s" is not supported.', $this->db->DBDriver)); } + + $this->assertSame($expected, json_decode(json_encode($fieldsData), true)); } public function testCompositeKey() @@ -1237,4 +1355,21 @@ public function testDropKey() $this->forge->dropTable('key_test_users', true); } + + public function testAddTextColumnWithConstraint() + { + // some DBMS do not allow a constraint for type TEXT + $result = $this->forge->addColumn('user', [ + 'text_with_constraint' => ['type' => 'text', 'constraint' => 255, 'default' => ''], + ]); + + $this->assertTrue($this->db->fieldExists('text_with_constraint', 'user')); + + // SQLSRV requires dropping default constraint before dropping column + $result = $this->forge->dropColumn('user', 'text_with_constraint'); + + $this->db->resetDataCache(); + + $this->assertFalse($this->db->fieldExists('text_with_constraint', 'user')); + } } diff --git a/tests/system/Database/Live/MySQLi/RawSqlTest.php b/tests/system/Database/Live/MySQLi/RawSqlTest.php new file mode 100644 index 000000000000..6356b604f011 --- /dev/null +++ b/tests/system/Database/Live/MySQLi/RawSqlTest.php @@ -0,0 +1,203 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Database\Live\MySQLi; + +use CodeIgniter\Database\RawSql; +use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\DatabaseTestTrait; +use stdclass; +use Tests\Support\Database\Seeds\CITestSeeder; + +/** + * @group DatabaseLive + * + * @internal + */ +final class RawSqlTest extends CIUnitTestCase +{ + use DatabaseTestTrait; + + protected $refresh = true; + protected $seed = CITestSeeder::class; + + protected function setUp(): void + { + parent::setUp(); + + if ($this->db->DBDriver !== 'MySQLi') { + $this->markTestSkipped('Only MySQLi has its own implementation.'); + } else { + $this->addSqlFunction(); + } + } + + protected function addSqlFunction() + { + $this->db->query('DROP FUNCTION IF EXISTS setDateTime'); + + $sql = "CREATE FUNCTION setDateTime ( setDate varchar(20) ) + RETURNS DATETIME + READS SQL DATA + DETERMINISTIC + BEGIN + RETURN CONVERT(CONCAT(setDate,' ','01:01:11'), DATETIME); + END;"; + + $this->db->query($sql); + } + + public function testRawSqlUpdateObject() + { + $data = []; + + $row = new stdclass(); + $row->email = 'derek@world.com'; + $row->created_at = new RawSql("setDateTime('2022-01-01')"); + $data[] = $row; + + $row = new stdclass(); + $row->email = 'ahmadinejad@world.com'; + $row->created_at = new RawSql("setDateTime('2022-01-01')"); + $data[] = $row; + + $this->db->table('user')->updateBatch($data, 'email'); + + $row->created_at = new RawSql("setDateTime('2022-01-11')"); + + $this->db->table('user')->update($row, "email = 'ahmadinejad@world.com'"); + + $this->seeInDatabase('user', ['email' => 'derek@world.com', 'created_at' => '2022-01-01 01:01:11']); + $this->seeInDatabase('user', ['email' => 'ahmadinejad@world.com', 'created_at' => '2022-01-11 01:01:11']); + } + + public function testRawSqlSetUpdateObject() + { + $data = []; + + $row = new stdclass(); + $row->email = 'derek@world.com'; + $row->created_at = new RawSql("setDateTime('2022-02-01')"); + $data[] = $row; + + $row = new stdclass(); + $row->email = 'ahmadinejad@world.com'; + $row->created_at = new RawSql("setDateTime('2022-02-01')"); + $data[] = $row; + + $this->db->table('user')->setUpdateBatch($data, 'email')->updateBatch(null, 'email'); + + $row->created_at = new RawSql("setDateTime('2022-02-11')"); + + $this->db->table('user')->set($row)->update(null, "email = 'ahmadinejad@world.com'"); + + $this->seeInDatabase('user', ['email' => 'derek@world.com', 'created_at' => '2022-02-01 01:01:11']); + $this->seeInDatabase('user', ['email' => 'ahmadinejad@world.com', 'created_at' => '2022-02-11 01:01:11']); + } + + public function testRawSqlUpdateArray() + { + $data = [ + ['email' => 'derek@world.com', 'created_at' => new RawSql("setDateTime('2022-03-01')")], + ['email' => 'ahmadinejad@world.com', 'created_at' => new RawSql("setDateTime('2022-03-01')")], + ]; + + $this->db->table('user')->updateBatch($data, 'email'); + + $this->seeInDatabase('user', ['email' => 'derek@world.com', 'created_at' => '2022-03-01 01:01:11']); + $this->seeInDatabase('user', ['email' => 'ahmadinejad@world.com', 'created_at' => '2022-03-01 01:01:11']); + + $data = ['email' => 'ahmadinejad@world.com', 'created_at' => new RawSql("setDateTime('2022-03-11')")]; + + $this->db->table('user')->update($data, "email = 'ahmadinejad@world.com'"); + + $this->seeInDatabase('user', ['email' => 'ahmadinejad@world.com', 'created_at' => '2022-03-11 01:01:11']); + } + + public function testRawSqlInsertArray() + { + $data = [ + ['email' => 'pedro@world.com', 'created_at' => new RawSql("setDateTime('2022-04-01')")], + ['email' => 'todd@world.com', 'created_at' => new RawSql("setDateTime('2022-04-01')")], + ]; + + $this->db->table('user')->insertBatch($data); + + $this->seeInDatabase('user', ['email' => 'pedro@world.com', 'created_at' => '2022-04-01 01:01:11']); + $this->seeInDatabase('user', ['email' => 'todd@world.com', 'created_at' => '2022-04-01 01:01:11']); + + $data = ['email' => 'jason@world.com', 'created_at' => new RawSql("setDateTime('2022-04-11')")]; + + $this->db->table('user')->insert($data); + + $this->seeInDatabase('user', ['email' => 'jason@world.com', 'created_at' => '2022-04-11 01:01:11']); + } + + public function testRawSqlInsertObject() + { + $data = []; + + $row = new stdclass(); + $row->email = 'tony@world.com'; + $row->created_at = new RawSql("setDateTime('2022-05-01')"); + $data[] = $row; + + $row = new stdclass(); + $row->email = 'sara@world.com'; + $row->created_at = new RawSql("setDateTime('2022-05-01')"); + $data[] = $row; + + $this->db->table('user')->insertBatch($data); + + $row->email = 'jessica@world.com'; + $row->created_at = new RawSql("setDateTime('2022-05-11')"); + + $this->db->table('user')->insert($row); + + $this->seeInDatabase('user', ['email' => 'tony@world.com', 'created_at' => '2022-05-01 01:01:11']); + $this->seeInDatabase('user', ['email' => 'sara@world.com', 'created_at' => '2022-05-01 01:01:11']); + $this->seeInDatabase('user', ['email' => 'jessica@world.com', 'created_at' => '2022-05-11 01:01:11']); + } + + public function testRawSqlSetInsertObject() + { + $data = []; + + $row = new stdclass(); + $row->email = 'laura@world.com'; + $row->created_at = new RawSql("setDateTime('2022-06-01')"); + $data[] = $row; + + $row = new stdclass(); + $row->email = 'travis@world.com'; + $row->created_at = new RawSql("setDateTime('2022-06-01')"); + $data[] = $row; + + $this->db->table('user')->setInsertBatch($data)->insertBatch(); + + $this->seeInDatabase('user', ['email' => 'laura@world.com', 'created_at' => '2022-06-01 01:01:11']); + $this->seeInDatabase('user', ['email' => 'travis@world.com', 'created_at' => '2022-06-01 01:01:11']); + + $row->email = 'steve@world.com'; + $row->created_at = new RawSql("setDateTime('2022-06-11')"); + + $this->db->table('user')->set($row)->insert(); + + $this->seeInDatabase('user', ['email' => 'steve@world.com', 'created_at' => '2022-06-11 01:01:11']); + + $this->db->table('user') + ->set('email', 'dan@world.com') + ->set('created_at', new RawSql("setDateTime('2022-06-13')")) + ->insert(); + + $this->seeInDatabase('user', ['email' => 'dan@world.com', 'created_at' => '2022-06-13 01:01:11']); + } +} diff --git a/tests/system/Database/Live/SQLite/AlterTableTest.php b/tests/system/Database/Live/SQLite/AlterTableTest.php index ce1055d04c2b..0d3da18d1a81 100644 --- a/tests/system/Database/Live/SQLite/AlterTableTest.php +++ b/tests/system/Database/Live/SQLite/AlterTableTest.php @@ -74,7 +74,7 @@ private function dropTables() public function testFromTableThrowsOnNoTable() { $this->expectException(DataException::class); - $this->expectExceptionMessage('Table `foo` was not found in the current database.'); + $this->expectExceptionMessage('Table "foo" was not found in the current database.'); $this->table->fromTable('foo'); } @@ -156,6 +156,46 @@ public function testDropColumnMaintainsKeys() $this->assertTrue($result); } + public function testDropColumnDropCompositeKey() + { + $this->forge->dropTable('actions', true); + + $fields = [ + 'category' => ['type' => 'varchar', 'constraint' => 63], + 'name' => ['type' => 'varchar', 'constraint' => 63], + 'created_at' => ['type' => 'datetime', 'null' => true], + ]; + + $this->forge->addField('id'); + $this->forge->addField($fields); + + $this->forge->addKey('name'); + $this->forge->addKey(['category', 'name']); + $this->forge->addKey('created_at'); + + $this->forge->createTable('actions'); + + $indexes = $this->db->getIndexData('actions'); + + // the composite index was created + $this->assertSame(['category', 'name'], $indexes['actions_category_name']->fields); + + // drop one of the columns in the composite index + $this->forge->dropColumn('actions', 'category'); + + // get indexes again + $indexes = $this->db->getIndexData('actions'); + + // check that composite index was dropped. + $this->assertArrayNotHasKey('actions_category_name', $indexes); + + // check that that other keys are present + $this->assertArrayHasKey('actions_name', $indexes); + $this->assertArrayHasKey('actions_created_at', $indexes); + + $this->forge->dropTable('actions'); + } + public function testModifyColumnSuccess() { $this->createTable('janky'); diff --git a/tests/system/Database/Migrations/MigrationTest.php b/tests/system/Database/Migrations/MigrationTest.php index 1c1561044be0..54d7bb9e3b4c 100644 --- a/tests/system/Database/Migrations/MigrationTest.php +++ b/tests/system/Database/Migrations/MigrationTest.php @@ -29,7 +29,7 @@ protected function setUp(): void public function testDBGroup() { - $migration = new class () extends Migration { + $migration = new class () extends Migration { protected $DBGroup = 'tests'; public function up() diff --git a/tests/system/Email/EmailTest.php b/tests/system/Email/EmailTest.php index da428519071d..df932a488c21 100644 --- a/tests/system/Email/EmailTest.php +++ b/tests/system/Email/EmailTest.php @@ -27,7 +27,7 @@ public function testEmailValidation() $config->validate = true; $email = new Email($config); $email->setTo('invalid'); - $this->assertStringContainsString('Invalid email address: invalid', $email->printDebugger()); + $this->assertStringContainsString('Invalid email address: "invalid"', $email->printDebugger()); } public function autoClearProvider() diff --git a/tests/system/Entity/EntityTest.php b/tests/system/Entity/EntityTest.php index 66a0a4478f9c..7d0ca037d504 100644 --- a/tests/system/Entity/EntityTest.php +++ b/tests/system/Entity/EntityTest.php @@ -295,7 +295,7 @@ public function testCastInteger() public function testCastIntBool() { - $entity = new class () extends Entity { + $entity = new class () extends Entity { protected $casts = [ 'active' => 'int-bool', ]; @@ -623,7 +623,7 @@ public function testCastAsJSONSyntaxError() $this->expectExceptionMessage('Syntax error, malformed JSON'); (Closure::bind(static function (string $value) { - $entity = new Entity(); + $entity = new Entity(); $entity->casts['dummy'] = 'json[array]'; return $entity->castAs($value, 'dummy'); @@ -637,7 +637,7 @@ public function testCastAsJSONAnotherErrorDepth() $string = '{' . str_repeat('"test":{', 513) . '"test":"value"' . str_repeat('}', 513) . '}'; (Closure::bind(static function (string $value) { - $entity = new Entity(); + $entity = new Entity(); $entity->casts['dummy'] = 'json[array]'; return $entity->castAs($value, 'dummy'); @@ -651,7 +651,7 @@ public function testCastAsJSONControlCharCheck() $string = "{\n\t\"property1\": \"The quick brown fox\njumps over the lazy dog\",\n\t\"property2\":\"value2\"\n}"; (Closure::bind(static function (string $value) { - $entity = new Entity(); + $entity = new Entity(); $entity->casts['dummy'] = 'json[array]'; return $entity->castAs($value, 'dummy'); @@ -665,7 +665,7 @@ public function testCastAsJSONStateMismatch() $string = '[{"name":"jack","product_id":"1234"]'; (Closure::bind(static function (string $value) { - $entity = new Entity(); + $entity = new Entity(); $entity->casts['dummy'] = 'json[array]'; return $entity->castAs($value, 'dummy'); @@ -832,7 +832,7 @@ public function testDataMappingIssetUnsetSwapped() public function testToArraySkipAttributesWithUnderscoreInFirstCharacter() { - $entity = new class () extends Entity { + $entity = new class () extends Entity { protected $attributes = [ '_foo' => null, 'bar' => null, diff --git a/tests/system/Events/EventsTest.php b/tests/system/Events/EventsTest.php index 9b69a219b929..58a8159f3e1c 100644 --- a/tests/system/Events/EventsTest.php +++ b/tests/system/Events/EventsTest.php @@ -43,6 +43,7 @@ protected function tearDown(): void /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testInitialize() diff --git a/tests/system/Files/FileCollectionTest.php b/tests/system/Files/FileCollectionTest.php index 7936cc2e62da..4b4fd9d3f63a 100644 --- a/tests/system/Files/FileCollectionTest.php +++ b/tests/system/Files/FileCollectionTest.php @@ -108,7 +108,7 @@ public function testConstructorAddsFiles() $this->file, ]; - $collection = new class ([$this->file]) extends FileCollection { + $collection = new class ([$this->file]) extends FileCollection { protected $files = [ SUPPORTPATH . 'Files/able/apple.php', ]; diff --git a/tests/system/Filters/CSRFTest.php b/tests/system/Filters/CSRFTest.php index 395eb8ee9bb4..b516321bddb3 100644 --- a/tests/system/Filters/CSRFTest.php +++ b/tests/system/Filters/CSRFTest.php @@ -31,22 +31,40 @@ protected function setUp(): void $this->config = new \Config\Filters(); } - public function testNormal() + public function testDoNotCheckCliRequest() { $this->config->globals = [ 'before' => ['csrf'], 'after' => [], ]; - $this->request = Services::request(null, false); + $this->request = Services::clirequest(null, false); $this->response = Services::response(); $filters = new Filters($this->config, $this->request, $this->response); $uri = 'admin/foo/bar'; - // we expect CSRF requests to be ignored in CLI - $expected = $this->request; - $request = $filters->run($uri, 'before'); - $this->assertSame($expected, $request); + $request = $filters->run($uri, 'before'); + + $this->assertSame($this->request, $request); + } + + public function testPassGetRequest() + { + $this->config->globals = [ + 'before' => ['csrf'], + 'after' => [], + ]; + + $this->request = Services::incomingrequest(null, false); + $this->response = Services::response(); + + $filters = new Filters($this->config, $this->request, $this->response); + $uri = 'admin/foo/bar'; + + $request = $filters->run($uri, 'before'); + + // GET request is not protected, so no SecurityException will be thrown. + $this->assertSame($this->request, $request); } } diff --git a/tests/system/Filters/HoneypotTest.php b/tests/system/Filters/HoneypotTest.php index 94ec0b61d1ff..c9cff3fa4125 100644 --- a/tests/system/Filters/HoneypotTest.php +++ b/tests/system/Filters/HoneypotTest.php @@ -80,6 +80,7 @@ public function testBeforeClean() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testAfter() @@ -102,6 +103,7 @@ public function testAfter() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testAfterNotApplicable() diff --git a/tests/system/HTTP/CLIRequestTest.php b/tests/system/HTTP/CLIRequestTest.php index bf78ccc6e752..b294d1621b2b 100644 --- a/tests/system/HTTP/CLIRequestTest.php +++ b/tests/system/HTTP/CLIRequestTest.php @@ -589,4 +589,31 @@ public function testMethodIsCliReturnsAlwaysTrue() { $this->assertTrue($this->request->isCLI()); } + + public function testGetGet() + { + $this->assertSame([], $this->request->getGet()); + $this->assertNull($this->request->getGet('test')); + $this->assertSame([], $this->request->getGet(['test', 'abc'])); + } + + public function testGetPost() + { + $this->assertSame([], $this->request->getPost()); + } + + public function testGetPostGet() + { + $this->assertSame([], $this->request->getPostGet()); + } + + public function testGetGetPost() + { + $this->assertSame([], $this->request->getGetPost()); + } + + public function testGetLocale() + { + $this->assertSame('en', $this->request->getLocale()); + } } diff --git a/tests/system/HTTP/ContentSecurityPolicyTest.php b/tests/system/HTTP/ContentSecurityPolicyTest.php index 7e41c9a0d4a5..7542ed5a1934 100644 --- a/tests/system/HTTP/ContentSecurityPolicyTest.php +++ b/tests/system/HTTP/ContentSecurityPolicyTest.php @@ -60,6 +60,7 @@ protected function work(string $parm = 'Hello') /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testExistence() @@ -72,6 +73,7 @@ public function testExistence() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testReportOnly() @@ -85,6 +87,7 @@ public function testReportOnly() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testDefaults() @@ -104,6 +107,7 @@ public function testDefaults() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testChildSrc() @@ -121,6 +125,7 @@ public function testChildSrc() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testConnectSrc() @@ -137,6 +142,7 @@ public function testConnectSrc() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testFontSrc() @@ -155,6 +161,7 @@ public function testFontSrc() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testFormAction() @@ -173,6 +180,7 @@ public function testFormAction() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testFrameAncestor() @@ -190,6 +198,7 @@ public function testFrameAncestor() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testFrameSrc() @@ -207,6 +216,7 @@ public function testFrameSrc() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testImageSrc() @@ -224,6 +234,7 @@ public function testImageSrc() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testMediaSrc() @@ -241,6 +252,7 @@ public function testMediaSrc() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testManifestSrc() @@ -258,6 +270,7 @@ public function testManifestSrc() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testPluginType() @@ -275,6 +288,7 @@ public function testPluginType() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testPluginArray() @@ -290,6 +304,7 @@ public function testPluginArray() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testObjectSrc() @@ -307,6 +322,7 @@ public function testObjectSrc() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testScriptSrc() @@ -324,6 +340,7 @@ public function testScriptSrc() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testStyleSrc() @@ -341,6 +358,7 @@ public function testStyleSrc() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testBaseURIDefault() @@ -354,6 +372,7 @@ public function testBaseURIDefault() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testBaseURI() @@ -368,6 +387,7 @@ public function testBaseURI() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testBaseURIRich() @@ -382,6 +402,7 @@ public function testBaseURIRich() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testDefaultSrc() @@ -398,6 +419,7 @@ public function testDefaultSrc() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testReportURI() @@ -413,6 +435,7 @@ public function testReportURI() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testSandboxFlags() @@ -429,6 +452,7 @@ public function testSandboxFlags() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testUpgradeInsecureRequests() @@ -443,6 +467,7 @@ public function testUpgradeInsecureRequests() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testBodyEmpty() @@ -456,6 +481,7 @@ public function testBodyEmpty() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testBodyScriptNonce() @@ -527,6 +553,7 @@ public function testBodyStyleNonceDisableAutoNonce() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testBodyStyleNonce() @@ -566,6 +593,7 @@ public function testBodyStyleNonceCustomStyleTag() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testHeaderWrongCaseNotFound() @@ -579,6 +607,7 @@ public function testHeaderWrongCaseNotFound() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testHeaderIgnoreCase() @@ -592,6 +621,7 @@ public function testHeaderIgnoreCase() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testCSPDisabled() @@ -623,6 +653,7 @@ public function testGetStyleNonce() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testHeaderScriptNonceEmittedOnceGetScriptNonceCalled() diff --git a/tests/system/HTTP/DownloadResponseTest.php b/tests/system/HTTP/DownloadResponseTest.php index eb6df43eecac..f0c9b3024f6d 100644 --- a/tests/system/HTTP/DownloadResponseTest.php +++ b/tests/system/HTTP/DownloadResponseTest.php @@ -299,6 +299,7 @@ public function testPretendOutput() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testRealOutput() diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index f19f9cf53ee4..f7d50dccff24 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -124,6 +124,7 @@ public function testCanGetOldInputArrayWithSESSION() * @see https://github.com/codeigniter4/CodeIgniter4/issues/1492 * * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testCanGetOldInputArrayWithSessionService() diff --git a/tests/system/HTTP/NegotiateTest.php b/tests/system/HTTP/NegotiateTest.php index c1ad8aa4da19..2f1eaba11a35 100644 --- a/tests/system/HTTP/NegotiateTest.php +++ b/tests/system/HTTP/NegotiateTest.php @@ -20,24 +20,17 @@ */ final class NegotiateTest extends CIUnitTestCase { - private ?Request $request; + private ?IncomingRequest $request; private ?Negotiate $negotiate; protected function setUp(): void { parent::setUp(); - $this->request = new Request(new App()); - + $this->request = new IncomingRequest(new App(), new URI(), null, new UserAgent()); $this->negotiate = new Negotiate($this->request); } - protected function tearDown(): void - { - $this->request = $this->negotiate = null; - unset($this->request, $this->negotiate); - } - public function testNegotiateMediaFindsHighestMatch() { $this->request->setHeader('Accept', 'text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c'); diff --git a/tests/system/HTTP/RedirectResponseTest.php b/tests/system/HTTP/RedirectResponseTest.php index 95cd6003f275..1b8fe62f49d9 100644 --- a/tests/system/HTTP/RedirectResponseTest.php +++ b/tests/system/HTTP/RedirectResponseTest.php @@ -117,6 +117,7 @@ public function testRedirectRelativeConvertsToFullURI() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testWithInput() @@ -137,6 +138,7 @@ public function testWithInput() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testWithValidationErrors() @@ -157,6 +159,7 @@ public function testWithValidationErrors() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testWith() @@ -173,6 +176,7 @@ public function testWith() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testRedirectBack() @@ -189,6 +193,7 @@ public function testRedirectBack() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testRedirectBackMissing() @@ -204,6 +209,7 @@ public function testRedirectBackMissing() /** * @runInSeparateProcess + * * @preserveGlobalState disabled * * @see https://github.com/codeigniter4/CodeIgniter4/issues/2119 @@ -245,6 +251,7 @@ public function testWithCookies() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testWithCookiesWithEmptyCookies() diff --git a/tests/system/HTTP/ResponseSendTest.php b/tests/system/HTTP/ResponseSendTest.php index b67820785eb5..b30587d0493e 100644 --- a/tests/system/HTTP/ResponseSendTest.php +++ b/tests/system/HTTP/ResponseSendTest.php @@ -44,6 +44,7 @@ final class ResponseSendTest extends CIUnitTestCase /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testHeadersMissingDate() @@ -77,6 +78,7 @@ public function testHeadersMissingDate() * it makes sure that sending gives CSP a chance to do its thing. * * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testHeadersWithCSP() @@ -111,7 +113,9 @@ public function testHeadersWithCSP() * Make sure cookies are set by RedirectResponse this way * * @see https://github.com/codeigniter4/CodeIgniter4/issues/1393 + * * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testRedirectResponseCookies() diff --git a/tests/system/HTTP/URITest.php b/tests/system/HTTP/URITest.php index 1a51741b8c36..8e6bcb95ede9 100644 --- a/tests/system/HTTP/URITest.php +++ b/tests/system/HTTP/URITest.php @@ -660,6 +660,7 @@ public function testResolveRelativeURI($rel, $expected) /** * @dataProvider defaultResolutions + * * @group single * * @param mixed $rel @@ -789,6 +790,7 @@ public function testGetQueryWithStrings() /** * @see https://github.com/codeigniter4/CodeIgniter4/issues/331 + * * @group single */ public function testNoExtraSlashes() diff --git a/tests/system/Helpers/FormHelperTest.php b/tests/system/Helpers/FormHelperTest.php index 3fdcc582d0fc..5ce907d56454 100644 --- a/tests/system/Helpers/FormHelperTest.php +++ b/tests/system/Helpers/FormHelperTest.php @@ -836,6 +836,7 @@ public function testSetCheckboxWithValueZero() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testSetRadioFromSessionOldInput() @@ -856,6 +857,7 @@ public function testSetRadioFromSessionOldInput() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testSetRadioFromPost() @@ -869,6 +871,7 @@ public function testSetRadioFromPost() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testSetRadioFromPostWithValueZero() @@ -926,6 +929,43 @@ public function testSetRadioDefault() $this->assertSame('', set_radio('code', 'beta', false)); } + public function testValidationErrorsFromSession() + { + $_SESSION = ['_ci_validation_errors' => ['foo' => 'bar']]; + + $this->assertSame(['foo' => 'bar'], validation_errors()); + + $_SESSION = []; + } + + public function testValidationErrorsFromValidation() + { + $validation = Services::validation(); + $validation->setRule('id', 'ID', 'required')->run([]); + + $this->assertSame(['id' => 'The ID field is required.'], validation_errors()); + } + + public function testValidationListErrors() + { + $validation = Services::validation(); + $validation->setRule('id', 'ID', 'required')->run([]); + + $html = validation_list_errors(); + + $this->assertStringContainsString('
  • The ID field is required.
  • ', $html); + } + + public function testValidationShowError() + { + $validation = Services::validation(); + $validation->setRule('id', 'ID', 'required')->run([]); + + $html = validation_show_error('id'); + + $this->assertSame('The ID field is required.' . "\n", $html); + } + public function testFormParseFormAttributesTrue() { $expected = 'readonly '; diff --git a/tests/system/Helpers/TextHelperTest.php b/tests/system/Helpers/TextHelperTest.php index 7c736614a36d..bdc236909f8c 100755 --- a/tests/system/Helpers/TextHelperTest.php +++ b/tests/system/Helpers/TextHelperTest.php @@ -196,7 +196,6 @@ public function testEntitiesToAsciiSmallOrdinals() public function testConvertAccentedCharacters() { - //$this->ci_vfs_clone('application/Config/ForeignChars.php'); $this->assertSame('AAAeEEEIIOOEUUUeY', convert_accented_characters('ÀÂÄÈÊËÎÏÔŒÙÛÜŸ')); $this->assertSame('a e i o u n ue', convert_accented_characters('á é í ó ú ñ ü')); } diff --git a/tests/system/I18n/TimeDifferenceTest.php b/tests/system/I18n/TimeDifferenceTest.php index 7141f71769d3..4f8a02f038af 100644 --- a/tests/system/I18n/TimeDifferenceTest.php +++ b/tests/system/I18n/TimeDifferenceTest.php @@ -19,12 +19,23 @@ */ final class TimeDifferenceTest extends CIUnitTestCase { + private string $currentLocale; + protected function setUp(): void { parent::setUp(); helper('date'); - Locale::setDefault('America/Chicago'); + + $this->currentLocale = Locale::getDefault(); + Locale::setDefault('en-US'); + } + + protected function tearDown(): void + { + parent::tearDown(); + + Locale::setDefault($this->currentLocale); } public function testDifferenceBasics() diff --git a/tests/system/I18n/TimeTest.php b/tests/system/I18n/TimeTest.php index 2db4fcc48c2b..80433fbb212c 100644 --- a/tests/system/I18n/TimeTest.php +++ b/tests/system/I18n/TimeTest.php @@ -25,14 +25,25 @@ */ final class TimeTest extends CIUnitTestCase { + private string $currentLocale; + protected function setUp(): void { parent::setUp(); helper('date'); + + $this->currentLocale = Locale::getDefault(); Locale::setDefault('en_US'); } + protected function tearDown(): void + { + parent::tearDown(); + + Locale::setDefault($this->currentLocale); + } + public function testNewTimeNow() { $formatter = new IntlDateFormatter( @@ -55,7 +66,7 @@ public function testTimeWithTimezone() IntlDateFormatter::SHORT, IntlDateFormatter::SHORT, 'Europe/London', // Default for CodeIgniter - IntlDateFormatter::GREGORIAN, + IntlDateFormatter::GREGORIAN, 'yyyy-MM-dd HH:mm:ss' ); @@ -71,7 +82,7 @@ public function testTimeWithTimezoneAndLocale() IntlDateFormatter::SHORT, IntlDateFormatter::SHORT, 'Europe/London', // Default for CodeIgniter - IntlDateFormatter::GREGORIAN, + IntlDateFormatter::GREGORIAN, 'yyyy-MM-dd HH:mm:ss' ); @@ -1128,4 +1139,17 @@ public function testSetTestNowWithFaLocale() Locale::setDefault($currentLocale); } + + public function testToDatabase() + { + $currentLocale = Locale::getDefault(); + Locale::setDefault('fa'); + + $time = Time::parse('2017-01-12 00:00', 'America/Chicago'); + + $this->assertSame('۲۰۱۷-۰۱-۱۲ ۰۰:۰۰:۰۰', (string) $time); + $this->assertSame('2017-01-12 00:00:00', $time->toDatabase()); + + Locale::setDefault($currentLocale); + } } diff --git a/tests/system/Language/LanguageTest.php b/tests/system/Language/LanguageTest.php index 58994434b3ec..bcf7be306d59 100644 --- a/tests/system/Language/LanguageTest.php +++ b/tests/system/Language/LanguageTest.php @@ -154,7 +154,7 @@ public function testLanguageDuplicateKey() $this->lang = new Language('en'); $this->assertSame('These are not the droids you are looking for', $this->lang->getLine('More.strongForce', [])); $this->assertSame('I have a very bad feeling about this', $this->lang->getLine('More.cannotMove', [])); - $this->assertSame('Could not move file {0} to {1} ({2}).', $this->lang->getLine('Files.cannotMove', [])); + $this->assertSame('Could not move file "{0}" to "{1}". Reason: {2}', $this->lang->getLine('Files.cannotMove', [])); $this->assertSame('I have a very bad feeling about this', $this->lang->getLine('More.cannotMove', [])); } @@ -206,7 +206,7 @@ public function testPrioritizedLocator() { // this should load the replacement bundle of messages $message = lang('Core.missingExtension', [], 'en'); - $this->assertSame('The framework needs the following extension(s) installed and loaded: {0}.', $message); + $this->assertSame('The framework needs the following extension(s) installed and loaded: "{0}".', $message); // and we should have our new message too $this->assertSame('billions and billions', lang('Core.bazillion', [], 'en')); } diff --git a/tests/system/Models/DeleteModelTest.php b/tests/system/Models/DeleteModelTest.php index 0e47d8535a90..3dfa9a0bbd5f 100644 --- a/tests/system/Models/DeleteModelTest.php +++ b/tests/system/Models/DeleteModelTest.php @@ -204,7 +204,7 @@ public static function emptyPkValues(): iterable public function testThrowsWithNoDateFormat(): void { $this->expectException(ModelException::class); - $this->expectExceptionMessage('`Tests\Support\Models\UserModel` model class does not have a valid dateFormat.'); + $this->expectExceptionMessage('"Tests\Support\Models\UserModel" model class does not have a valid dateFormat.'); $this->createModel(UserModel::class); $this->setPrivateProperty($this->model, 'dateFormat', ''); diff --git a/tests/system/Models/EventsModelTest.php b/tests/system/Models/EventsModelTest.php index 7cf2442036c8..96a8a429728e 100644 --- a/tests/system/Models/EventsModelTest.php +++ b/tests/system/Models/EventsModelTest.php @@ -167,7 +167,7 @@ public function testInvalidEventException(): void $this->setPrivateProperty($this->model, 'beforeInsert', ['anotherBeforeInsertMethod']); $this->expectException(DataException::class); - $this->expectExceptionMessage('anotherBeforeInsertMethod is not a valid Model Event callback.'); + $this->expectExceptionMessage('"anotherBeforeInsertMethod" is not a valid Model Event callback.'); $this->model->insert($data); } diff --git a/tests/system/Models/FindModelTest.php b/tests/system/Models/FindModelTest.php index 8f370f02f1e9..cad3ba3171c0 100644 --- a/tests/system/Models/FindModelTest.php +++ b/tests/system/Models/FindModelTest.php @@ -302,7 +302,7 @@ public function testFirstWithNoPrimaryKey(): void public function testThrowsWithNoPrimaryKey(): void { $this->expectException(ModelException::class); - $this->expectExceptionMessage('`Tests\Support\Models\UserModel` model class does not specify a Primary Key.'); + $this->expectExceptionMessage('"Tests\Support\Models\UserModel" model class does not specify a Primary Key.'); $this->createModel(UserModel::class); $this->setPrivateProperty($this->model, 'primaryKey', ''); diff --git a/tests/system/Models/GeneralModelTest.php b/tests/system/Models/GeneralModelTest.php index cf5b890c2718..450d3cc87d60 100644 --- a/tests/system/Models/GeneralModelTest.php +++ b/tests/system/Models/GeneralModelTest.php @@ -102,7 +102,7 @@ public function testSetAllowedFields(): void 'updated_at', ]; - $model = new class () extends Model { + $model = new class () extends Model { protected $allowedFields = [ 'id', 'created_at', diff --git a/tests/system/Models/InsertModelTest.php b/tests/system/Models/InsertModelTest.php index 77b7de0bf646..bf258dc82985 100644 --- a/tests/system/Models/InsertModelTest.php +++ b/tests/system/Models/InsertModelTest.php @@ -221,7 +221,7 @@ public function testInsertPermitInsertNoData(): void 'updated_at' => ['type' => 'INTEGER', 'constraint' => 11, 'null' => true], ])->addKey('id', true)->createTable('insert_no_data', true); - $model = new class () extends Model { + $model = new class () extends Model { protected $table = 'insert_no_data'; protected $allowedFields = [ 'updated_at', diff --git a/tests/system/Models/SaveModelTest.php b/tests/system/Models/SaveModelTest.php index 0ff142375fde..dda1028a2fbf 100644 --- a/tests/system/Models/SaveModelTest.php +++ b/tests/system/Models/SaveModelTest.php @@ -263,7 +263,7 @@ public function testSaveNewEntityWithDate(): void ]; }; - $testModel = new class () extends Model { + $testModel = new class () extends Model { protected $table = 'empty'; protected $allowedFields = [ 'name', @@ -293,7 +293,7 @@ public function testInvalidAllowedFieldException(): void ]; $this->expectException(DataException::class); - $this->expectExceptionMessage('Allowed fields must be specified for model: Tests\Support\Models\JobModel'); + $this->expectExceptionMessage('Allowed fields must be specified for model: "Tests\Support\Models\JobModel"'); $this->model->save($data); } diff --git a/tests/system/RESTful/ResourceControllerTest.php b/tests/system/RESTful/ResourceControllerTest.php index d3dc0ea56a94..b65a7ddf38c4 100644 --- a/tests/system/RESTful/ResourceControllerTest.php +++ b/tests/system/RESTful/ResourceControllerTest.php @@ -36,6 +36,7 @@ * return correct responses. * * @runTestsInSeparateProcesses + * * @preserveGlobalState disabled * * @internal diff --git a/tests/system/RESTful/ResourcePresenterTest.php b/tests/system/RESTful/ResourcePresenterTest.php index c44bbba564d1..c36f6340c861 100644 --- a/tests/system/RESTful/ResourcePresenterTest.php +++ b/tests/system/RESTful/ResourcePresenterTest.php @@ -30,6 +30,7 @@ * return correct responses. * * @runTestsInSeparateProcesses + * * @preserveGlobalState disabled * * @internal diff --git a/tests/system/Router/RouterTest.php b/tests/system/Router/RouterTest.php index 95418fbb233c..912887a93e8c 100644 --- a/tests/system/Router/RouterTest.php +++ b/tests/system/Router/RouterTest.php @@ -423,7 +423,7 @@ public function testRouteResource() public function testRouteWithSlashInControllerName() { $this->expectExceptionMessage( - 'The namespace delimiter is a backslash (\), not a slash (/). Route handler: \App/Admin/Admins::edit_show/$1' + 'The namespace delimiter is a backslash (\), not a slash (/). Route handler: "\App/Admin/Admins::edit_show/$1"' ); $router = new Router($this->collection, $this->request); @@ -444,7 +444,7 @@ public function testRouteWithLeadingSlash() public function testRouteWithDynamicController() { $this->expectException(RouterException::class); - $this->expectExceptionMessage('A dynamic controller is not allowed for security reasons. Route handler: \$2::$3/$1'); + $this->expectExceptionMessage('A dynamic controller is not allowed for security reasons. Route handler: "\$2::$3/$1"'); $router = new Router($this->collection, $this->request); diff --git a/tests/system/Security/SecurityCSRFSessionRandomizeTokenTest.php b/tests/system/Security/SecurityCSRFSessionRandomizeTokenTest.php index dacd8787c041..6086472d3f4c 100644 --- a/tests/system/Security/SecurityCSRFSessionRandomizeTokenTest.php +++ b/tests/system/Security/SecurityCSRFSessionRandomizeTokenTest.php @@ -31,6 +31,7 @@ /** * @runTestsInSeparateProcesses + * * @preserveGlobalState disabled * * @internal diff --git a/tests/system/Security/SecurityCSRFSessionTest.php b/tests/system/Security/SecurityCSRFSessionTest.php index e4390360e392..7fbd7096853f 100644 --- a/tests/system/Security/SecurityCSRFSessionTest.php +++ b/tests/system/Security/SecurityCSRFSessionTest.php @@ -30,6 +30,7 @@ /** * @runTestsInSeparateProcesses + * * @preserveGlobalState disabled * * @internal diff --git a/tests/system/Session/SessionTest.php b/tests/system/Session/SessionTest.php index 238480997542..97c1005ba00e 100644 --- a/tests/system/Session/SessionTest.php +++ b/tests/system/Session/SessionTest.php @@ -23,6 +23,7 @@ /** * @runTestsInSeparateProcesses + * * @preserveGlobalState disabled * * @internal diff --git a/tests/system/Test/ControllerTestTraitTest.php b/tests/system/Test/ControllerTestTraitTest.php index c03e5132ce2b..84892479fee9 100644 --- a/tests/system/Test/ControllerTestTraitTest.php +++ b/tests/system/Test/ControllerTestTraitTest.php @@ -23,6 +23,7 @@ * Exercise our Controller class. * * @runTestsInSeparateProcesses + * * @preserveGlobalState disabled * * @internal diff --git a/tests/system/Test/TestCaseEmissionsTest.php b/tests/system/Test/TestCaseEmissionsTest.php index 64f21b121734..edc3d6b34ef4 100644 --- a/tests/system/Test/TestCaseEmissionsTest.php +++ b/tests/system/Test/TestCaseEmissionsTest.php @@ -42,6 +42,7 @@ final class TestCaseEmissionsTest extends CIUnitTestCase /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testHeadersEmitted() @@ -70,6 +71,7 @@ public function testHeadersEmitted() /** * @runInSeparateProcess + * * @preserveGlobalState disabled */ public function testHeadersNotEmitted() diff --git a/tests/system/Validation/RulesTest.php b/tests/system/Validation/RulesTest.php index 9430cc83fda2..419070714599 100644 --- a/tests/system/Validation/RulesTest.php +++ b/tests/system/Validation/RulesTest.php @@ -19,11 +19,13 @@ /** * @internal + * + * @no-final */ -final class RulesTest extends CIUnitTestCase +class RulesTest extends CIUnitTestCase { - private Validation $validation; - private array $config = [ + protected Validation $validation; + protected array $config = [ 'ruleSets' => [ Rules::class, FormatRules::class, diff --git a/tests/system/Validation/StrictRules/RulesTest.php b/tests/system/Validation/StrictRules/RulesTest.php index f3f8ce946020..e6d2e034d12c 100644 --- a/tests/system/Validation/StrictRules/RulesTest.php +++ b/tests/system/Validation/StrictRules/RulesTest.php @@ -11,20 +11,18 @@ namespace CodeIgniter\Validation\StrictRules; -use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Validation\RulesTest as TraditionalRulesTest; use CodeIgniter\Validation\Validation; -use Config\Services; use Generator; -use stdClass; use Tests\Support\Validation\TestRules; /** * @internal */ -final class RulesTest extends CIUnitTestCase +final class RulesTest extends TraditionalRulesTest { - private Validation $validation; - private array $config = [ + protected Validation $validation; + protected array $config = [ 'ruleSets' => [ Rules::class, FormatRules::class, @@ -42,91 +40,16 @@ final class RulesTest extends CIUnitTestCase ], ]; - protected function setUp(): void - { - parent::setUp(); - $this->validation = new Validation((object) $this->config, Services::renderer()); - $this->validation->reset(); - } - - /** - * @dataProvider provideRequiredCases - */ - public function testRequired(array $data, bool $expected): void - { - $this->validation->setRules(['foo' => 'required']); - $this->assertSame($expected, $this->validation->run($data)); - } - - public function provideRequiredCases(): Generator - { - yield from [ - [['foo' => null], false], - [['foo' => 123], true], - [['foo' => null, 'bar' => 123], false], - [['foo' => [123]], true], - [['foo' => []], false], - [['foo' => new stdClass()], true], - ]; - } - - /** - * @dataProvider ifExistProvider - */ - public function testIfExist(array $rules, array $data, bool $expected): void - { - $this->validation->setRules($rules); - $this->assertSame($expected, $this->validation->run($data)); - } - - public function ifExistProvider(): Generator - { - yield from [ - [ - ['foo' => 'required'], - ['foo' => ''], - false, - ], - [ - ['foo' => 'required'], - ['foo' => null], - false, - ], - [ - ['foo' => 'if_exist|required'], - ['foo' => ''], - false, - ], - // Input data does not exist then the other rules will be ignored - [ - ['foo' => 'if_exist|required'], - [], - true, - ], - // Testing for multi-dimensional data - [ - ['foo.bar' => 'if_exist|required'], - ['foo' => ['bar' => '']], - false, - ], - [ - ['foo.bar' => 'if_exist|required'], - ['foo' => []], - true, - ], - ]; - } - /** - * @dataProvider providePermitEmptyCases + * @dataProvider providePermitEmptyCasesStrict */ - public function testPermitEmpty(array $rules, array $data, bool $expected): void + public function testPermitEmptyStrict(array $rules, array $data, bool $expected): void { $this->validation->setRules($rules); $this->assertSame($expected, $this->validation->run($data)); } - public function providePermitEmptyCases(): Generator + public function providePermitEmptyCasesStrict(): Generator { yield from [ [ @@ -159,462 +82,101 @@ public function providePermitEmptyCases(): Generator ['foo' => false], true, ], - [ - ['foo' => 'permit_empty|valid_email'], - ['foo' => ''], - true, - ], - [ - ['foo' => 'permit_empty|valid_email'], - ['foo' => 'user@domain.tld'], - true, - ], - [ - ['foo' => 'permit_empty|valid_email'], - ['foo' => 'invalid'], - false, - ], - // Required has more priority - [ - ['foo' => 'permit_empty|required|valid_email'], - ['foo' => ''], - false, - ], - [ - ['foo' => 'permit_empty|required'], - ['foo' => ''], - false, - ], - [ - ['foo' => 'permit_empty|required'], - ['foo' => null], - false, - ], - [ - ['foo' => 'permit_empty|required'], - ['foo' => false], - false, - ], - // This tests will return true because the input data is trimmed - [ - ['foo' => 'permit_empty|required'], - ['foo' => '0'], - true, - ], - [ - ['foo' => 'permit_empty|required'], - ['foo' => 0], - true, - ], - [ - ['foo' => 'permit_empty|required'], - ['foo' => 0.0], - true, - ], - [ - ['foo' => 'permit_empty|required_with[bar]'], - ['foo' => ''], - true, - ], - [ - ['foo' => 'permit_empty|required_with[bar]'], - ['foo' => 0], - true, - ], - [ - ['foo' => 'permit_empty|required_with[bar]'], - ['foo' => 0.0, 'bar' => 1], - true, - ], - [ - ['foo' => 'permit_empty|required_with[bar]'], - ['foo' => '', 'bar' => 1], - false, - ], - [ - ['foo' => 'permit_empty|required_without[bar]'], - ['foo' => ''], - false, - ], - [ - ['foo' => 'permit_empty|required_without[bar]'], - ['foo' => 0], - true, - ], - [ - ['foo' => 'permit_empty|required_without[bar]'], - ['foo' => 0.0, 'bar' => 1], - true, - ], - [ - ['foo' => 'permit_empty|required_without[bar]'], - ['foo' => '', 'bar' => 1], - true, - ], - ]; - } - - /** - * @dataProvider provideMatchesCases - */ - public function testMatches(array $data, bool $expected): void - { - $this->validation->setRules(['foo' => 'matches[bar]']); - $this->assertSame($expected, $this->validation->run($data)); - } - - public function provideMatchesCases(): Generator - { - yield from [ - [['foo' => null, 'bar' => null], true], - [['foo' => 'match', 'bar' => 'match'], true], - [['foo' => 'match', 'bar' => 'nope'], false], - ]; - } - - /** - * @dataProvider provideMatchesNestedCases - */ - public function testMatchesNested(array $data, bool $expected): void - { - $this->validation->setRules(['nested.foo' => 'matches[nested.bar]']); - $this->assertSame($expected, $this->validation->run($data)); - } - - public function provideMatchesNestedCases(): Generator - { - yield from [ - [['nested' => ['foo' => 'match', 'bar' => 'match']], true], - [['nested' => ['foo' => 'match', 'bar' => 'nope']], false], ]; } /** - * @dataProvider provideMatchesCases + * @dataProvider provideGreaterThanEqualStrict + * + * @param int $value */ - public function testDiffers(array $data, bool $expected): void + public function testGreaterThanEqualStrict($value, string $param, bool $expected): void { - $this->validation->setRules(['foo' => 'differs[bar]']); - $this->assertSame(! $expected, $this->validation->run($data)); - } + $this->validation->setRules(['foo' => "greater_than_equal_to[{$param}]"]); - /** - * @dataProvider provideMatchesNestedCases - */ - public function testDiffersNested(array $data, bool $expected): void - { - $this->validation->setRules(['nested.foo' => 'differs[nested.bar]']); - $this->assertSame(! $expected, $this->validation->run($data)); - } - - /** - * @dataProvider provideEqualsCases - */ - public function testEquals(array $data, string $param, bool $expected): void - { - $this->validation->setRules(['foo' => "equals[{$param}]"]); + $data = ['foo' => $value]; $this->assertSame($expected, $this->validation->run($data)); } - public function provideEqualsCases(): Generator - { - yield from [ - 'null' => [['foo' => null], '', false], - 'empty' => [['foo' => ''], '', true], - 'fail' => [['foo' => 'bar'], 'notbar', false], - 'pass' => [['foo' => 'bar'], 'bar', true], - 'casing' => [['foo' => 'bar'], 'Bar', false], - ]; - } - - /** - * @dataProvider provideMinLengthCases - */ - public function testMinLength(?string $data, string $length, bool $expected): void - { - $this->validation->setRules(['foo' => "min_length[{$length}]"]); - $this->assertSame($expected, $this->validation->run(['foo' => $data])); - } - - public function provideMinLengthCases(): Generator + public function provideGreaterThanEqualStrict(): Generator { yield from [ - 'null' => [null, '2', false], - 'less' => ['bar', '2', true], - 'equal' => ['bar', '3', true], - 'greater' => ['bar', '4', false], + [0, '0', true], + [1, '0', true], + [-1, '0', false], + [true, '0', false], ]; } /** - * @dataProvider provideMinLengthCases - */ - public function testMaxLength(?string $data, string $length, bool $expected): void - { - $this->validation->setRules(['foo' => "max_length[{$length}]"]); - $this->assertSame(! $expected || $length === '3', $this->validation->run(['foo' => $data])); - } - - public function testMaxLengthReturnsFalseWithNonNumericVal(): void - { - $this->validation->setRules(['foo' => 'max_length[bar]']); - $this->assertFalse($this->validation->run(['foo' => 'bar'])); - } - - /** - * @dataProvider provideExactLengthCases + * @dataProvider provideGreaterThanStrict + * + * @param int $value */ - public function testExactLength(?string $data, bool $expected): void - { - $this->validation->setRules(['foo' => 'exact_length[3]']); - $this->assertSame($expected, $this->validation->run(['foo' => $data])); - } - - public function provideExactLengthCases(): Generator + public function testGreaterThanStrict($value, string $param, bool $expected): void { - yield from [ - 'null' => [null, false], - 'exact' => ['bar', true], - 'less' => ['ba', false], - 'greater' => ['bars', false], - ]; - } + $this->validation->setRules(['foo' => "greater_than[{$param}]"]); - public function testExactLengthDetectsBadLength(): void - { - $data = ['foo' => 'bar']; - $this->validation->setRules(['foo' => 'exact_length[abc]']); - $this->assertFalse($this->validation->run($data)); - } - - /** - * @dataProvider greaterThanProvider - */ - public function testGreaterThan(?string $first, ?string $second, bool $expected): void - { - $data = ['foo' => $first]; - $this->validation->setRules(['foo' => "greater_than[{$second}]"]); + $data = ['foo' => $value]; $this->assertSame($expected, $this->validation->run($data)); } - public function greaterThanProvider(): Generator + public function provideGreaterThanStrict(): Generator { yield from [ - ['-10', '-11', true], - ['10', '9', true], - ['10', '10', false], - ['10', 'a', false], - ['10a', '10', false], - [null, null, false], + [-10, '-11', true], + [10, '9', true], + [10, '10', false], + [10, 'a', false], + [true, '0', false], ]; } /** - * @dataProvider greaterThanEqualProvider + * @dataProvider provideLessThanStrict + * + * @param int $value */ - public function testGreaterThanEqual(?string $first, ?string $second, bool $expected): void - { - $data = ['foo' => $first]; - $this->validation->setRules(['foo' => "greater_than_equal_to[{$second}]"]); - $this->assertSame($expected, $this->validation->run($data)); - } - - public function greaterThanEqualProvider(): Generator + public function testLessThanStrict($value, string $param, bool $expected): void { - yield from [ - ['0', '0', true], - ['1', '0', true], - ['-1', '0', false], - ['10a', '0', false], - [null, null, false], - ['1', null, true], - [null, '1', false], - ]; - } + $this->validation->setRules(['foo' => "less_than[{$param}]"]); - /** - * @dataProvider lessThanProvider - */ - public function testLessThan(?string $first, ?string $second, bool $expected): void - { - $data = ['foo' => $first]; - $this->validation->setRules(['foo' => "less_than[{$second}]"]); + $data = ['foo' => $value]; $this->assertSame($expected, $this->validation->run($data)); } - public function lessThanProvider(): Generator + public function provideLessThanStrict(): Generator { yield from [ - ['-10', '-11', false], - ['9', '10', true], - ['10', '9', false], - ['10', '10', false], - ['10', 'a', true], - ['10a', '10', false], - [null, null, false], + [-10, '-11', false], + [9, '10', true], + [10, '9', false], + [10, '10', false], + [10, 'a', true], + [true, '0', false], ]; } /** - * @dataProvider lessThanEqualProvider + * @dataProvider provideLessThanEqualStrict + * + * @param int $value */ - public function testLessEqualThan(?string $first, ?string $second, bool $expected): void + public function testLessEqualThanStrict($value, ?string $param, bool $expected): void { - $data = ['foo' => $first]; - $this->validation->setRules(['foo' => "less_than_equal_to[{$second}]"]); - $this->assertSame($expected, $this->validation->run($data)); - } + $this->validation->setRules(['foo' => "less_than_equal_to[{$param}]"]); - public function lessThanEqualProvider(): Generator - { - yield from [ - ['0', '0', true], - ['1', '0', false], - ['-1', '0', true], - ['10a', '0', false], - [null, null, false], - ['1', null, false], - [null, '1', false], - ]; - } - - /** - * @dataProvider inListProvider - */ - public function testInList(?string $first, ?string $second, bool $expected): void - { - $data = ['foo' => $first]; - $this->validation->setRules(['foo' => "in_list[{$second}]"]); - $this->assertSame($expected, $this->validation->run($data)); - } - - /** - * @dataProvider inListProvider - */ - public function testNotInList(?string $first, ?string $second, bool $expected): void - { - $data = ['foo' => $first]; - $this->validation->setRules(['foo' => "not_in_list[{$second}]"]); - $this->assertSame(! $expected, $this->validation->run($data)); - } - - public function inListProvider(): Generator - { - yield from [ - ['red', 'red,Blue,123', true], - ['Blue', 'red, Blue,123', true], - ['Blue', 'red,Blue,123', true], - ['123', 'red,Blue,123', true], - ['Red', 'red,Blue,123', false], - [' red', 'red,Blue,123', false], - ['1234', 'red,Blue,123', false], - [null, 'red,Blue,123', false], - ['red', null, false], - ]; - } - - /** - * @dataProvider requiredWithProvider - */ - public function testRequiredWith(?string $field, ?string $check, bool $expected): void - { - $data = [ - 'foo' => 'bar', - 'bar' => 'something', - 'baz' => null, - 'array' => [ - 'nonEmptyField1' => 'value1', - 'nonEmptyField2' => 'value2', - 'emptyField1' => null, - 'emptyField2' => null, - ], - ]; - - $this->validation->setRules([$field => "required_with[{$check}]"]); + $data = ['foo' => $value]; $this->assertSame($expected, $this->validation->run($data)); } - public function requiredWithProvider(): Generator + public function provideLessThanEqualStrict(): Generator { yield from [ - ['nope', 'bar', false], - ['foo', 'bar', true], - ['nope', 'baz', true], - [null, null, true], - [null, 'foo', false], - ['foo', null, true], - [ - 'array.emptyField1', - 'array.emptyField2', - true, - ], - [ - 'array.nonEmptyField1', - 'array.emptyField2', - true, - ], - [ - 'array.emptyField1', - 'array.nonEmptyField2', - false, - ], - [ - 'array.nonEmptyField1', - 'array.nonEmptyField2', - true, - ], - ]; - } - - /** - * @dataProvider requiredWithoutProvider - */ - public function testRequiredWithout(?string $field, ?string $check, bool $expected): void - { - $data = [ - 'foo' => 'bar', - 'bar' => 'something', - 'baz' => null, - 'array' => [ - 'nonEmptyField1' => 'value1', - 'nonEmptyField2' => 'value2', - 'emptyField1' => null, - 'emptyField2' => null, - ], - ]; - - $this->validation->setRules([$field => "required_without[{$check}]"]); - $this->assertSame($expected, $this->validation->run($data)); - } - - public function requiredWithoutProvider(): Generator - { - yield from [ - ['nope', 'bars', false], - ['foo', 'nope', true], - [null, null, false], - [null, 'foo', true], - ['foo', null, true], - [ - 'array.emptyField1', - 'array.emptyField2', - false, - ], - [ - 'array.nonEmptyField1', - 'array.emptyField2', - true, - ], - [ - 'array.emptyField1', - 'array.nonEmptyField2', - true, - ], - [ - 'array.nonEmptyField1', - 'array.nonEmptyField2', - true, - ], + [0, '0', true], + [1, '0', false], + [-1, '0', true], + [true, '0', false], ]; } } diff --git a/tests/system/Validation/ValidationTest.php b/tests/system/Validation/ValidationTest.php index c6339139044d..09faa1bb2282 100644 --- a/tests/system/Validation/ValidationTest.php +++ b/tests/system/Validation/ValidationTest.php @@ -25,6 +25,7 @@ /** * @internal + * * @no-final */ class ValidationTest extends CIUnitTestCase @@ -463,6 +464,28 @@ public function testRunGroupWithCustomErrorMessage(): void ], $this->validation->getErrors()); } + /** + * @see https://github.com/codeigniter4/CodeIgniter4/issues/6245 + */ + public function testRunWithCustomErrorsAndAsteriskField(): void + { + $data = [ + 'foo' => [ + ['bar' => null], + ['bar' => null], + ], + ]; + $this->validation->setRules( + ['foo.*.bar' => ['label' => 'foo bar', 'rules' => 'required']], + ['foo.*.bar' => ['required' => 'Required']] + ); + $this->validation->run($data); + $this->assertSame([ + 'foo.0.bar' => 'Required', + 'foo.1.bar' => 'Required', + ], $this->validation->getErrors()); + } + /** * @dataProvider rulesSetupProvider * @@ -619,6 +642,42 @@ public function testJsonInput(): void unset($_SERVER['CONTENT_TYPE']); } + /** + * @see https://github.com/codeigniter4/CodeIgniter4/issues/6466 + */ + public function testJsonInputObjectArray(): void + { + $json = <<<'EOL' + { + "p": [ + { + "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + } + ] + } + EOL; + + $_SERVER['CONTENT_TYPE'] = 'application/json'; + + $config = new App(); + $config->baseURL = 'http://example.com/'; + + $request = new IncomingRequest($config, new URI(), $json, new UserAgent()); + + $rules = [ + 'p' => 'required|array_count[2]', + ]; + $validated = $this->validation + ->withRequest($request->withMethod('patch')) + ->setRules($rules) + ->run(); + + $this->assertFalse($validated); + $this->assertSame(['p' => 'Validation.array_count'], $this->validation->getErrors()); + + unset($_SERVER['CONTENT_TYPE']); + } + public function testHasRule(): void { $this->validation->setRuleGroup('groupA'); diff --git a/tests/system/View/ParserTest.php b/tests/system/View/ParserTest.php index ab9c0860b851..55ea539d72a5 100644 --- a/tests/system/View/ParserTest.php +++ b/tests/system/View/ParserTest.php @@ -267,7 +267,7 @@ public function testParseLoopObjectProperties() public function testParseLoopEntityProperties() { - $power = new class () extends Entity { + $power = new class () extends Entity { public $foo = 'bar'; protected $bar = 'baz'; @@ -297,7 +297,7 @@ public function toArray(bool $onlyChanged = false, bool $cast = true, bool $recu public function testParseLoopEntityObjectProperties() { - $power = new class () extends Entity { + $power = new class () extends Entity { protected $attributes = [ 'foo' => 'bar', 'bar' => 'baz', @@ -738,7 +738,7 @@ public function testParserPluginClosure() public function testParserPluginParams() { $this->parser->addPlugin('growth', static function ($str, array $params) { - $step = $params['step'] ?? 1; + $step = $params['step'] ?? 1; $count = $params['count'] ?? 2; $out = ''; @@ -919,7 +919,7 @@ public function testRenderFindsView() public function testRenderCannotFindView() { $this->expectException(ViewException::class); - $this->expectExceptionMessageMatches('!\AInvalid file: (?:.+)View(?:/|\\\\)Views(?:/|\\\\)Simplest\.php\z!'); + $this->expectExceptionMessageMatches('!\AInvalid file: \"(?:.+)View(?:/|\\\\)Views(?:/|\\\\)Simplest\.php\"\z!'); $this->parser->setData(['testString' => 'Hello World']); $this->parser->render('Simplest'); diff --git a/user_guide_src/source/changelogs/index.rst b/user_guide_src/source/changelogs/index.rst index 65711cdb4f90..5ee95c93365a 100644 --- a/user_guide_src/source/changelogs/index.rst +++ b/user_guide_src/source/changelogs/index.rst @@ -13,6 +13,8 @@ See all the changes. :titlesonly: v4.3.0 + v4.2.7 + v4.2.6 v4.2.5 v4.2.4 v4.2.3 diff --git a/user_guide_src/source/changelogs/v4.2.2.rst b/user_guide_src/source/changelogs/v4.2.2.rst index 263af1d4f6ff..a98ce675f51c 100644 --- a/user_guide_src/source/changelogs/v4.2.2.rst +++ b/user_guide_src/source/changelogs/v4.2.2.rst @@ -16,7 +16,6 @@ BREAKING - The method signature of ``CodeIgniter\Debug\Exceptions::__construct()`` has been changed. The ``IncomingRequest`` typehint on the ``$request`` parameter was removed. Extending classes should likewise remove the parameter so as not to break LSP. - The method signature of ``BaseBuilder.php::insert()`` and ``BaseBuilder.php::update()`` have been changed. The ``?array`` typehint on the ``$set`` parameter was removed. - A bug that caused pages to be cached before after filters were executed when using page caching has been fixed. Adding response headers or changing the response body in after filters now caches them correctly. -- Due to a bug fix, now :php:func:`random_string()` with the first parameter ``'crypto'`` throws ``InvalidArgumentException`` if the second parameter ``$len`` is an odd number. Changes ******* diff --git a/user_guide_src/source/changelogs/v4.2.5.rst b/user_guide_src/source/changelogs/v4.2.5.rst index a670705e4153..c9ff36b05508 100644 --- a/user_guide_src/source/changelogs/v4.2.5.rst +++ b/user_guide_src/source/changelogs/v4.2.5.rst @@ -1,7 +1,7 @@ Version 4.2.5 ############# -Release Date: Unreleased +Release Date: August 28, 2022 **4.2.5 release of CodeIgniter4** @@ -14,11 +14,12 @@ BREAKING - The method signature of ``BaseConnection::tableExists()`` has been changed. A second optional parameter ``$cached`` was added. This directs whether to use cache data or not. Default is ``true``, use cache data. - The abstract method signature of ``BaseBuilder::_listTables()`` has been changed. A second optional parameter ``$tableName`` was added. Providing a table name will generate SQL listing only that table. +- The method signature of ``Validation::processRules()`` and ``Validation::getErrorMessage()`` have been changed. Both of these methods add new ``$originalField`` parameter. Enhancements ************ -none. +- Kint has been updated to 4.2.0. Changes ******* @@ -32,5 +33,6 @@ none. Bugs Fixed ********** +- When using subqueries in the main query, prefixes are added to the table alias. See the repo's `CHANGELOG.md `_ for a complete list of bugs fixed. diff --git a/user_guide_src/source/changelogs/v4.2.6.rst b/user_guide_src/source/changelogs/v4.2.6.rst new file mode 100644 index 000000000000..2da5842ea88f --- /dev/null +++ b/user_guide_src/source/changelogs/v4.2.6.rst @@ -0,0 +1,38 @@ +Version 4.2.6 +############# + +Release Date: September 4, 2022 + +**4.2.6 release of CodeIgniter4** + +.. contents:: + :local: + :depth: 2 + +BREAKING +******** + +none. + +Enhancements +************ + +none. + +Changes +******* + +none. + +Deprecations +************ + +- :php:meth:`CodeIgniter\\Cookie\\Cookie::withNeverExpiring()` is deprecated. + +Bugs Fixed +********** + +Many bugs fixed, but notably: +- AssertionError occurs when using Validation in CLI `https://github.com/codeigniter4/CodeIgniter4/pull/6452` + +See the repo's `CHANGELOG.md `_ for a complete list of bugs fixed. diff --git a/user_guide_src/source/changelogs/v4.2.7.rst b/user_guide_src/source/changelogs/v4.2.7.rst new file mode 100644 index 000000000000..916a67676280 --- /dev/null +++ b/user_guide_src/source/changelogs/v4.2.7.rst @@ -0,0 +1,37 @@ +Version 4.2.7 +############# + +Release Date: Unreleased + +**4.2.7 release of CodeIgniter4** + +.. contents:: + :local: + :depth: 2 + +BREAKING +******** + +none. + +Enhancements +************ + +none. + +Changes +******* + +none. + +Deprecations +************ + +none. + +Bugs Fixed +********** + +none. + +See the repo's `CHANGELOG.md `_ for a complete list of bugs fixed. diff --git a/user_guide_src/source/changelogs/v4.3.0.rst b/user_guide_src/source/changelogs/v4.3.0.rst index ce193eea5f84..da11d2b15f1c 100644 --- a/user_guide_src/source/changelogs/v4.3.0.rst +++ b/user_guide_src/source/changelogs/v4.3.0.rst @@ -43,30 +43,62 @@ Others - The ``spark`` file has been changed due to a change in the processing of Spark commands. - ``InvalidArgumentException`` that is a kind of ``LogicException`` in ``BaseBuilder::_whereIn()`` is not suppressed by the configuration. Previously if ``CI_DEBUG`` was false, the exception was suppressed. - ``RouteCollection::resetRoutes()`` resets Auto-Discovery of Routes. Previously once discovered, RouteCollection never discover Routes files again even if ``RouteCollection::resetRoutes()`` is called. +- ``CITestStreamFilter::$buffer = ''`` no longer causes the filter to be registered to listen for streams. Now there + is a ``CITestStreamFilter::registration()`` method for this. See :ref:`upgrade-430-stream-filter` for details. Enhancements ************ -CLI -=== +Commands +======== + +- The call handler for Spark commands from the ``CodeIgniter\CodeIgniter`` class has been extracted. This will reduce the cost of console calls. +- Added ``spark filter:check`` command to check the filters for a route. See :ref:`Controller Filters ` for the details. +- Now ``spark routes`` command shows route names. See :ref:`URI Routing `. +- Help information for a spark command can now be accessed using the ``--help`` option (e.g. ``php spark serve --help``) - Added methods ``CLI::promptByMultipleKeys()`` to support multiple value in input, unlike ``promptByKey()``. See :ref:`prompt-by-multiple-keys` for details. -Others -====== +Testing +======= + - Added the ``StreamFilterTrait`` to make it easier to work with capturing data from STDOUT and STDERR streams. See :ref:`testing-cli-output`. +- The CITestStreamFilter filter class now implements methods for adding a filter to streams. See :ref:`testing-cli-output`. - Added the ``PhpStreamWrapper`` to make it easier to work with setting data to ``php://stdin``. See :ref:`testing-cli-input`. +- Added method :ref:`benchmark-timer-record` to measure performance in a callable. Also enhanced common function ``timer()`` to accept optional callable. + +Database +======== + +- SQLite :ref:`BaseConnection::getIndexData() ` now can return pseudo index named ``PRIMARY`` for `AUTOINCREMENT` column, and each returned index data has ``type`` property. +- SQLSRV now automatically drops ``DEFAULT`` constraint when using :ref:`Forge::dropColumn() `. +- ``BaseConnection::escape()`` now excludes the ``RawSql`` data type. This allows passing SQL strings into data. +- Added ``when()`` method to conditionally add a clause to the query. See :ref:`BaseBuilder::when() ` for details. + +Model +===== + - Added before and after events to ``BaseModel::insertBatch()`` and ``BaseModel::updateBatch()`` methods. See :ref:`model-events-callbacks`. - Added ``Model::allowEmptyInserts()`` method to insert empty data. See :ref:`Using CodeIgniter's Model ` -- Added ``$routes->useSupportedLocalesOnly(true)`` so that the Router returns 404 Not Found if the locale in the URL is not supported in ``Config\App::$supportedLocales``. See :ref:`Localization ` +- Added new :ref:`entities-property-casting` class ``IntBoolCast`` for Entity. + +Libraries +========= + - Added methods ``replace()``, ``addLineAfter()`` and ``addLineBefore()`` to modify files in Publisher. See :ref:`Publisher ` for details. -- The call handler for Spark commands from the ``CodeIgniter\CodeIgniter`` class has been extracted. This will reduce the cost of console calls. -- SQLite ``BaseConnection::getIndexData()`` now can return pseudo index named ``PRIMARY`` for `AUTOINCREMENT` column, and each returned index data has ``type`` property. -- Added ``spark filter:check`` command to check the filters for a route. See :ref:`Controller Filters ` for the details. - Now **Encryption** can decrypt data encrypted with CI3's Encryption. See :ref:`encryption-compatible-with-ci3`. -- Added method ``Timer::record()`` to measure performance in a callable. Also enhanced common function ``timer()`` to accept optional callable. -- Now ``spark routes`` command shows route names. See :ref:`URI Routing `. -- Added new :ref:`entities-property-casting` class ``IntBoolCast`` for Entity. -- Help information for a spark command can now be accessed using the ``--help`` option (e.g. ``php spark serve --help``) +- Added :ref:`Time::toDatabase() ` to get a datetime string that can be used with databases regardless of locale. + +Helpers and Functions +===================== + +- Now you can autoload helpers by **app/Config/Autoload.php**. +- Added new Form helper function :php:func:`validation_errors()`, :php:func:`validation_list_errors()` and :php:func:`validation_show_error()` to display Validation Errors. +- You can set the locale to :php:func:`route_to()` if you pass a locale value as the last parameter. + +Others +====== + +- Added ``$routes->useSupportedLocalesOnly(true)`` so that the Router returns 404 Not Found if the locale in the URL is not supported in ``Config\App::$supportedLocales``. See :ref:`Localization ` Changes ******* @@ -82,12 +114,16 @@ Changes - The ``CodeIgniter\CLI\CommandRunner`` class has been removed due to a change in Spark commands processing. - The system route configuration file ``system/Config/Routes.php`` has been removed. - The route configuration file ``app/Config/Routes.php`` has been changed. Removed include of system routes configuration file. -- All atomic type properties in ``Config`` classes have been typed. +- Config + - All atomic type properties in ``Config`` classes have been typed. + - Changed the default setting to not redirect when a CSRF check fails so that it is easy to see that it is a CSRF error. +- Updated English language strings to be more consistent. Deprecations ************ -none. +- ``RouteCollection::localizeRoute()`` is deprecated. +- ``RouteCollection::fillRouteParams()`` is deprecated. Use ``RouteCollection::buildReverseRoute()`` instead. Bugs Fixed ********** diff --git a/user_guide_src/source/concepts/services.rst b/user_guide_src/source/concepts/services.rst index 3e14a87acba0..0e20441faee2 100644 --- a/user_guide_src/source/concepts/services.rst +++ b/user_guide_src/source/concepts/services.rst @@ -36,7 +36,7 @@ come in handy. Instead of creating the instance ourself, we let a central class create an instance of the class for us. This class is kept very simple. It only contains a method for each class that we want -to use as a service. The method typically returns a shared instance of that class, passing any dependencies +to use as a service. The method typically returns a **shared instance** of that class, passing any dependencies it might have into it. Then, we would replace our timer creation code with code that calls this new class: .. literalinclude:: services/002.php @@ -57,6 +57,15 @@ As many CodeIgniter classes are provided as services, you can get them like the The ``$typography`` is an instance of the Typography class, and if you call ``\Config\Services::typography()`` again, you will get the exactly same instance. +The Services typically return a **shared instance** of the class. The following code creates a ``CURLRequest`` instance at the first call. And the second call returns the exactly same instance. + +.. literalinclude:: services/015.php + +Therefore, the parameter ``$options2`` for the ``$client2`` does not work. It is just ignored. + +Getting a New Instance +====================== + If you want to get a new instance of the Typography class, you need to pass ``false`` to the argument ``$getShared``: .. literalinclude:: services/014.php diff --git a/user_guide_src/source/concepts/services/015.php b/user_guide_src/source/concepts/services/015.php new file mode 100644 index 000000000000..efaf1be9f00d --- /dev/null +++ b/user_guide_src/source/concepts/services/015.php @@ -0,0 +1,15 @@ + 'http://example.com/api/v1/', + 'timeout' => 3, +]; +$client1 = \Config\Services::curlrequest($options1); + +$options2 = [ + 'baseURI' => 'http://another.example.com/api/v2/', + 'timeout' => 10, +]; +$client2 = \Config\Services::curlrequest($options2); +// $options2 does not work. +// $client2 is the exactly same instance as $client1. diff --git a/user_guide_src/source/conf.py b/user_guide_src/source/conf.py index 0c5ce70cd366..076101e2d2a8 100644 --- a/user_guide_src/source/conf.py +++ b/user_guide_src/source/conf.py @@ -24,7 +24,7 @@ version = '4.2' # The full version, including alpha/beta/rc tags. -release = '4.2.4' +release = '4.2.6' # -- General configuration --------------------------------------------------- diff --git a/user_guide_src/source/database/call_function/002.php b/user_guide_src/source/database/call_function/002.php index 64d5e0fbc85f..fa26ae0edc68 100644 --- a/user_guide_src/source/database/call_function/002.php +++ b/user_guide_src/source/database/call_function/002.php @@ -1,3 +1,3 @@ callFunction('some_function', $param1, $param2, /* ... */); +$db->callFunction('some_function', $param1, $param2 /* , ... */); diff --git a/user_guide_src/source/database/metadata.rst b/user_guide_src/source/database/metadata.rst index ed864afaf801..41aa41ef9784 100644 --- a/user_guide_src/source/database/metadata.rst +++ b/user_guide_src/source/database/metadata.rst @@ -101,13 +101,17 @@ The following data is available from this function if supported by your database: - name - column name -- max_length - maximum length of the column -- primary_key - 1 if the column is a primary key - type - the type of the column +- max_length - maximum length of the column +- primary_key - integer ``1`` if the column is a primary key (all integer ``1``, even if there are multiple primary keys), otherwise integer ``0`` (This field is currently only available for MySQL and SQLite3) +- nullable - boolean ``true`` if the column is nullable, otherwise boolean ``false`` (This field is currently not available in SQL Server) +- default - the default value List the Indexes in a Table =========================== +.. _db-metadata-getindexdata: + $db->getIndexData() ------------------- diff --git a/user_guide_src/source/database/queries.rst b/user_guide_src/source/database/queries.rst index 08163309c6e7..6a5424de8c39 100644 --- a/user_guide_src/source/database/queries.rst +++ b/user_guide_src/source/database/queries.rst @@ -99,12 +99,14 @@ It's a very good security practice to escape your data before submitting it into your database. CodeIgniter has three methods that help you do this: +.. _database-queries-db_escape: + 1. $db->escape() ================ -This function determines the data type so -that it can escape only string data. It also automatically adds -single quotes around the data so you don't have to: +This function determines the data type so that it can escape only string +data. It also automatically adds single quotes around the data so you +don't have to: .. literalinclude:: queries/009.php diff --git a/user_guide_src/source/database/query_builder.rst b/user_guide_src/source/database/query_builder.rst index 76032dfe3b31..edf8e31ce618 100755 --- a/user_guide_src/source/database/query_builder.rst +++ b/user_guide_src/source/database/query_builder.rst @@ -800,7 +800,9 @@ Here is an example using an object: The first parameter is an object. -.. note:: All values are escaped automatically producing safer queries. +.. note:: All values except ``RawSql`` are escaped automatically producing safer queries. + +.. warning:: When you use ``RawSql``, you MUST escape the data manually. Failure to do so could result in SQL injections. $builder->ignore() ------------------ @@ -845,7 +847,9 @@ method. Here is an example using an array: The first parameter is an associative array of values. -.. note:: All values are escaped automatically producing safer queries. +.. note:: All values except ``RawSql`` are escaped automatically producing safer queries. + +.. warning:: When you use ``RawSql``, you MUST escape the data manually. Failure to do so could result in SQL injections. ************* Updating Data @@ -918,7 +922,9 @@ Or you can supply an object: .. literalinclude:: query_builder/089.php -.. note:: All values are escaped automatically producing safer queries. +.. note:: All values except ``RawSql`` are escaped automatically producing safer queries. + +.. warning:: When you use ``RawSql``, you MUST escape the data manually. Failure to do so could result in SQL injections. You'll notice the use of the ``$builder->where()`` method, enabling you to set the **WHERE** clause. You can optionally pass this information @@ -947,7 +953,9 @@ Here is an example using an array: The first parameter is an associative array of values, the second parameter is the where key. -.. note:: All values are escaped automatically producing safer queries. +.. note:: All values except ``RawSql`` are escaped automatically producing safer queries. + +.. warning:: When you use ``RawSql``, you MUST escape the data manually. Failure to do so could result in SQL injections. .. note:: ``affectedRows()`` won't give you proper results with this method, due to the very nature of how it works. Instead, ``updateBatch()`` @@ -1012,6 +1020,35 @@ that it produces a **DELETE** SQL string instead of an **INSERT** SQL string. For more information view documentation for ``$builder->getCompiledInsert()``. +********************** +Conditional Statements +********************** + +.. _db-builder-when: + +$builder->when() +------------------ + +This allows modifying the query based on a condition, without breaking out of the +query builder chain. The first parameter is the condition, and it should evaluate +to a boolean. The second parameter is a closure to call with that will be ran +when the condition is true. + +For example, you might only want to apply a given WHERE statement based on the +value sent within an HTTP request: + +.. literalinclude:: query_builder/105.php + +Since the condition is evaluated as ``true``, the closure will be called. The value +set in the condition will be passed as the second parameter to the closure so it +can be used in the query. + +Sometimes you might want to apply a different statement if the condition evaluates to false. +This can be accomplished by providing a second closure: + +.. literalinclude:: query_builder/106.php + + *************** Method Chaining *************** diff --git a/user_guide_src/source/database/query_builder/042.php b/user_guide_src/source/database/query_builder/042.php index 7c9935e4f356..26a4c0316e1f 100644 --- a/user_guide_src/source/database/query_builder/042.php +++ b/user_guide_src/source/database/query_builder/042.php @@ -1,4 +1,5 @@ like('title', 'match'); $builder->orLike('body', $match); +$builder->like('title', 'match'); +$builder->orLike('body', $match); // WHERE `title` LIKE '%match%' ESCAPE '!' OR `body` LIKE '%match%' ESCAPE '!' diff --git a/user_guide_src/source/database/query_builder/063.php b/user_guide_src/source/database/query_builder/063.php index 0b7543273430..ada34b2e65d4 100644 --- a/user_guide_src/source/database/query_builder/063.php +++ b/user_guide_src/source/database/query_builder/063.php @@ -1,4 +1,5 @@ havingLike('title', 'match'); $builder->orHavingLike('body', $match); +$builder->havingLike('title', 'match'); +$builder->orHavingLike('body', $match); // HAVING `title` LIKE '%match%' ESCAPE '!' OR `body` LIKE '%match%' ESCAPE '!' diff --git a/user_guide_src/source/database/query_builder/076.php b/user_guide_src/source/database/query_builder/076.php index 2e87e8345dd0..5816cbbef151 100644 --- a/user_guide_src/source/database/query_builder/076.php +++ b/user_guide_src/source/database/query_builder/076.php @@ -1,10 +1,17 @@ 'My title', - 'name' => 'My Name', - 'date' => 'My date', + 'id' => new RawSql('DEFAULT'), + 'title' => 'My title', + 'name' => 'My Name', + 'date' => '2022-01-01', + 'last_update' => new RawSql('CURRENT_TIMESTAMP()'), ]; $builder->insert($data); -// Produces: INSERT INTO mytable (title, name, date) VALUES ('My title', 'My name', 'My date') +/* Produces: + INSERT INTO mytable (id, title, name, date, last_update) + VALUES (DEFAULT, 'My title', 'My name', '2022-01-01', CURRENT_TIMESTAMP()) +*/ diff --git a/user_guide_src/source/database/query_builder/105.php b/user_guide_src/source/database/query_builder/105.php new file mode 100644 index 000000000000..f48a36d173a0 --- /dev/null +++ b/user_guide_src/source/database/query_builder/105.php @@ -0,0 +1,9 @@ +getPost('status'); + +$users = $this->db->table('users') + ->when($status, static function ($query, $status) { + $query->where('status', $status); + }) + ->get(); diff --git a/user_guide_src/source/database/query_builder/106.php b/user_guide_src/source/database/query_builder/106.php new file mode 100644 index 000000000000..015a28e37fa4 --- /dev/null +++ b/user_guide_src/source/database/query_builder/106.php @@ -0,0 +1,11 @@ +getPost('return_inactive'); + +$users = $this->db->table('users') + ->when($onlyInactive, static function ($query, $onlyInactive) { + $query->where('status', 'inactive'); + }, static function ($query) { + $query->where('status', 'active'); + }) + ->get(); diff --git a/user_guide_src/source/dbmgmt/forge.rst b/user_guide_src/source/dbmgmt/forge.rst index a6d75b9d7c7e..b3052e3aa9d0 100644 --- a/user_guide_src/source/dbmgmt/forge.rst +++ b/user_guide_src/source/dbmgmt/forge.rst @@ -262,6 +262,8 @@ Examples: Dropping Columns From a Table ============================== +.. _db-forge-dropColumn: + $forge->dropColumn() -------------------- diff --git a/user_guide_src/source/general/common_functions.rst b/user_guide_src/source/general/common_functions.rst index 02d29f16b981..08653ed1f1b6 100755 --- a/user_guide_src/source/general/common_functions.rst +++ b/user_guide_src/source/general/common_functions.rst @@ -335,17 +335,22 @@ Miscellaneous Functions .. php:function:: route_to($method[, ...$params]) :param string $method: The named route alias, or name of the controller/method to match. - :param int|string $params: One or more parameters to be passed to be matched in the route. + :param int|string $params: One or more parameters to be passed to be matched in the route. The last parameter allows you to set the locale. .. note:: This function requires the controller/method to have a route defined in **app/Config/routes.php**. - Generates a route for you based on either a named route alias, - or a controller::method combination. Will take parameters into effect, if provided. + Generates a route for you based on a controller::method combination. Will take parameters into effect, if provided. .. literalinclude:: common_functions/009.php + Generates a route for you based on a named route alias. + .. literalinclude:: common_functions/010.php + Since v4.3.0, when you use ``{locale}`` in your route, you can optionally specify the locale value as the last parameter. + + .. literalinclude:: common_functions/011.php + .. note:: ``route_to()`` returns a route, not a full URI path for your site. If your **baseURL** contains sub folders, the return value is not the same as the URI to link. In that case, just use :php:func:`url_to()` instead. diff --git a/user_guide_src/source/general/common_functions/011.php b/user_guide_src/source/general/common_functions/011.php new file mode 100644 index 000000000000..3d354cec2533 --- /dev/null +++ b/user_guide_src/source/general/common_functions/011.php @@ -0,0 +1,16 @@ +add( + '{locale}/users/(:num)/gallery(:any)', + 'Galleries::showUserGallery/$1/$2', + ['as' => 'user_gallery'] +); + +?> + +` class in CodeIgniter 4. + Loading this Helper =================== @@ -27,17 +29,18 @@ The following functions are available: :returns: UNIX timestamp :rtype: int - Returns the current time as a UNIX timestamp, referenced either to your server's - local time or any PHP supported timezone, based on the "time reference" setting - in your config file. If you do not intend to set your master time reference to - any other PHP supported timezone (which you'll typically do if you run a site - that lets each user set their own timezone settings) there is no benefit to using - this function over PHP's ``time()`` function. + .. note:: It is recommended to use the :doc:`Time <../libraries/time>` class instead. Use ``Time::now()->getTimestamp()`` to get the current UNIX timestamp. + + If a timezone is not provided, it will return the current UNIX timestamp by ``time()``. .. literalinclude:: date_helper/002.php - If a timezone is not provided, it will return ``time()`` based on the - **time_reference** setting. + If any PHP supported timezone is provided, it will return a timestamp that is offset by the time difference. It is not the same as the current UNIX timestamp. + + If you do not intend to set your master time reference to + any other PHP supported timezone (which you'll typically do if you run a site + that lets each user set their own timezone settings) there is no benefit to using + this function over PHP's ``time()`` function. .. php:function:: timezone_select([$class = '', $default = '', $what = \DateTimeZone::ALL, $country = null]) @@ -48,11 +51,8 @@ The following functions are available: :returns: Preformatted HTML select field :rtype: string - Generates a `select` form field of available timezones (optionally filtered by `$what` and `$country`). + Generates a `select` form field of available timezones (optionally filtered by ``$what`` and ``$country``). You can supply an option class to apply to the field to make formatting easier, as well as a default selected value. .. literalinclude:: date_helper/003.php - -Many functions previously found in the CodeIgniter 3 ``date_helper`` have been moved to the ``I18n`` -module in CodeIgniter 4. diff --git a/user_guide_src/source/helpers/date_helper/002.php b/user_guide_src/source/helpers/date_helper/002.php index 6631bccdd4cc..e0361c0b3575 100644 --- a/user_guide_src/source/helpers/date_helper/002.php +++ b/user_guide_src/source/helpers/date_helper/002.php @@ -1,3 +1,3 @@ /> /> +.. php:function:: validation_errors() + + :returns: The validation errors + :rtype: array + + This function was introduced in v4.3.0. + + Returns the validation errors. First, this function checks the validation errors + that are stored in the session. To store the errors in the session, you need to use ``withInput()`` with :php:func:`redirect() `. + + The returned array is the same as ``Validation::getErrors()``. + See :ref:`Validation ` for details. + + Example:: + + + +.. php:function:: validation_list_errors($template = 'list') + + :param string $template: Validation template name + :returns: Rendered HTML of the validation errors + :rtype: string + + This function was introduced in v4.3.0. + + Returns the rendered HTML of the validation errors. + + The parameter ``$template`` is a Validation template name. + See :ref:`validation-customizing-error-display` for details. + + This function uses :php:func:`validation_errors()` internally. + + Example:: + + + +.. php:function:: validation_show_error($field, $template = 'single') + + :param string $field: Field name + :param string $template: Validation template name + :returns: Rendered HTML of the validation error + :rtype: string + + This function was introduced in v4.3.0. + + Returns a single error for the specified field in formatted HTML. + + The parameter ``$template`` is a Validation template name. + See :ref:`validation-customizing-error-display` for details. + + This function uses :php:func:`validation_errors()` internally. + + Example:: + + diff --git a/user_guide_src/source/incoming/controllers/004.php b/user_guide_src/source/incoming/controllers/004.php index 65b1c77f8014..9216bab50032 100644 --- a/user_guide_src/source/incoming/controllers/004.php +++ b/user_guide_src/source/incoming/controllers/004.php @@ -8,7 +8,7 @@ public function updateUser(int $userID) { if (! $this->validate([ 'email' => "required|is_unique[users.email,id,{$userID}]", - 'name' => 'required|alpha_numeric_spaces', + 'name' => 'required|alpha_numeric_spaces', ])) { return view('users/update', [ 'errors' => $this->validator->getErrors(), diff --git a/user_guide_src/source/incoming/routing.rst b/user_guide_src/source/incoming/routing.rst index 7f461a645dd2..cbe7359b22bc 100644 --- a/user_guide_src/source/incoming/routing.rst +++ b/user_guide_src/source/incoming/routing.rst @@ -47,6 +47,23 @@ You can supply multiple verbs that a route should match by passing them in as an .. literalinclude:: routing/004.php +Controller's Namespace +====================== + +If a controller name is stated without beginning with ``\``, the :ref:`routing-default-namespace` will be prepended: + +.. literalinclude:: routing/063.php + +If you put ``\`` at the beginning, it is treated as a fully qualified class name: + +.. literalinclude:: routing/064.php + +You can also specify the namespace with the ``namespace`` option: + +.. literalinclude:: routing/038.php + +See :ref:`assigning-namespace` for details. + Placeholders ============ @@ -329,7 +346,7 @@ available from the command line: .. literalinclude:: routing/032.php .. note:: It is recommended to use Spark Commands for CLI scripts instead of calling controllers via CLI. - See the :doc:`../cli/spark_commands` page for detailed information. + See the :doc:`../cli/cli_commands` page for detailed information. .. warning:: If you enable :ref:`auto-routing` and place the command file in **app/Controllers**, anyone could access the command with the help of auto-routing via HTTP. @@ -397,7 +414,7 @@ You specify an array for the filter value: Assigning Namespace ------------------- -While a default namespace will be prepended to the generated controllers (see below), you can also specify +While a :ref:`routing-default-namespace` will be prepended to the generated controllers, you can also specify a different namespace to be used in any options array, with the ``namespace`` option. The value should be the namespace you want modified: @@ -455,7 +472,7 @@ Routes are registered in the routing table in the order in which they are define .. note:: If a route (the URI path) is defined more than once with different handlers, only the first defined route is registered. -You can check registered routes in the routing table by running the :ref:`spark routes ` command. +You can check registered routes in the routing table by running the :ref:`spark routes ` command. Changing Route Priority ======================= @@ -483,6 +500,8 @@ Routes Configuration Options The RoutesCollection class provides several options that affect all routes, and can be modified to meet your application's needs. These options are available at the top of **app/Config/Routes.php**. +.. _routing-default-namespace: + Default Namespace ================= diff --git a/user_guide_src/source/incoming/routing/063.php b/user_guide_src/source/incoming/routing/063.php new file mode 100644 index 000000000000..4996345ed7ae --- /dev/null +++ b/user_guide_src/source/incoming/routing/063.php @@ -0,0 +1,4 @@ +post('api/users', 'Api\Users::update'); diff --git a/user_guide_src/source/incoming/routing/064.php b/user_guide_src/source/incoming/routing/064.php new file mode 100644 index 000000000000..8d6df0548e30 --- /dev/null +++ b/user_guide_src/source/incoming/routing/064.php @@ -0,0 +1,4 @@ +get('blog', '\Acme\Blog\Controllers\Home::list'); diff --git a/user_guide_src/source/installation/backward_compatibility_notes.rst b/user_guide_src/source/installation/backward_compatibility_notes.rst new file mode 100644 index 000000000000..9b7272edf148 --- /dev/null +++ b/user_guide_src/source/installation/backward_compatibility_notes.rst @@ -0,0 +1,16 @@ +############################ +Backward Compatibility Notes +############################ + +We try to develop our products to be as backward compatible (BC) as possible. + +Only major releases (such as 4.0, 5.0 etc.) are allowed to break backward compatibility. +Minor releases (such as 4.2, 4.3 etc.) may introduce new features, but must do so without breaking the existing API. + +However, the code is not mature and bug fixes may break compatibility in minor releases, or even in patch releases (such as 4.2.5). In that case, all the breaking changes are described in the :doc:`../changelogs/index`. + +***************************** +What are not Breaking Changes +***************************** + +- System messages defined in **system/Language/en/** are strictly for internal framework use and are not covered by backwards compatibility (BC) promise. If developers are relying on language string output they should be checking it against the function call (``lang('...')``), not the content. diff --git a/user_guide_src/source/installation/upgrade_420.rst b/user_guide_src/source/installation/upgrade_420.rst index 4adac8a7613a..e932b8cdf1ff 100644 --- a/user_guide_src/source/installation/upgrade_420.rst +++ b/user_guide_src/source/installation/upgrade_420.rst @@ -38,6 +38,26 @@ Config/Constants.php The constants ``EVENT_PRIORITY_LOW``, ``EVENT_PRIORITY_NORMAL`` and ``EVENT_PRIORITY_HIGH`` are deprecated, and the definitions are moved to ``app/Config/Constants.php``. If you use these constants, define them in ``app/Config/Constants.php``. Or use new class constants ``CodeIgniter\Events\Events::PRIORITY_LOW``, ``CodeIgniter\Events\Events::PRIORITY_NORMAL`` and ``CodeIgniter\Events\Events::PRIORITY_HIGH``. +composer.json +============= + +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``. + +.. code-block:: text + + { + ... + "autoload": { + "psr-4": { + "App\\": "app", <-- Remove this line + "Config\\": "app/Config" <-- Remove this line + } + }, + ... + } + Breaking Changes **************** diff --git a/user_guide_src/source/installation/upgrade_425.rst b/user_guide_src/source/installation/upgrade_425.rst new file mode 100644 index 000000000000..930f188c811d --- /dev/null +++ b/user_guide_src/source/installation/upgrade_425.rst @@ -0,0 +1,18 @@ +############################# +Upgrading from 4.2.3 to 4.2.5 +############################# + +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 +************* + +Version ``4.2.5`` did not alter any project files. diff --git a/user_guide_src/source/installation/upgrade_426.rst b/user_guide_src/source/installation/upgrade_426.rst new file mode 100644 index 000000000000..a6d91b1d297b --- /dev/null +++ b/user_guide_src/source/installation/upgrade_426.rst @@ -0,0 +1,32 @@ +############################# +Upgrading from 4.2.5 to 4.2.6 +############################# + +Please refer to the upgrade instructions corresponding to your installation method. + +- :ref:`Composer Installation App Starter Upgrading ` +- :ref:`Composer Installation Adding CodeIgniter4 to an Existing Project Upgrading ` +- :ref:`Manual Installation Upgrading ` + +.. contents:: + :local: + :depth: 2 + + +Project Files +************* + +A few files in the **project space** (root, app, public, writable) received cosmetic updates. +You need not touch these files at all. 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/App.php +* app/Config/ContentSecurityPolicy.php +* app/Config/Routes.php +* app/Config/Validation.php diff --git a/user_guide_src/source/installation/upgrade_430.rst b/user_guide_src/source/installation/upgrade_430.rst index 060f24259572..8aeef7e393bc 100644 --- a/user_guide_src/source/installation/upgrade_430.rst +++ b/user_guide_src/source/installation/upgrade_430.rst @@ -52,6 +52,65 @@ HTTP Status Code and Exit Code of Uncaught Exceptions - If you expect *Exit code* based on *Exception code*, the Exit code will be changed. In that case, you need to implement ``HasExitCodeInterface`` in the Exception. See :ref:`error-specify-exit-code`. +redirect()->withInput() and Validation Errors +============================================= + +``redirect()->withInput()`` and Validation errors had an undocumented behavior. +If you redirect with ``withInput()``, CodeIgniter stores the validation errors +in the session, and you can get the errors in the redirected page from +a validation object *before a new validation is run*:: + + // In the controller + if (! $this->validate($rules)) { + return redirect()->back()->withInput(); + } + + // In the view of the redirected page + listErrors() ?> + +This behavior was a bug and fixed in v4.3.0. + +If you have code that depends on the bug, you need to change the code. +Use new Form helpers, :php:func:`validation_errors()`, :php:func:`validation_list_errors()` and :php:func:`validation_show_error()` to display Validation Errors, +instead of the Validation object. + +.. _upgrade-430-stream-filter: + +Capturing STDERR and STDOUT streams in tests +============================================ + +The way error and output streams are captured has changed. Now instead of:: + + use CodeIgniter\Test\Filters\CITestStreamFilter; + + protected function setUp(): void + { + CITestStreamFilter::$buffer = ''; + $this->stream_filter = stream_filter_append(STDOUT, 'CITestStreamFilter'); + } + + protected function tearDown(): void + { + stream_filter_remove($this->stream_filter); + } + +need to use:: + + use CodeIgniter\Test\Filters\CITestStreamFilter; + + protected function setUp(): void + { + CITestStreamFilter::registration(); + CITestStreamFilter::addOutputFilter(); + } + + protected function tearDown(): void + { + CITestStreamFilter::removeOutputFilter(); + } + +Or use the trait ``CodeIgniter\Test\StreamFilterTrait``. See :ref:`testing-cli-output`. + Others ====== diff --git a/user_guide_src/source/installation/upgrade_4xx.rst b/user_guide_src/source/installation/upgrade_4xx.rst index b0c3d3dea9be..1a35121d8f7d 100644 --- a/user_guide_src/source/installation/upgrade_4xx.rst +++ b/user_guide_src/source/installation/upgrade_4xx.rst @@ -109,6 +109,7 @@ Helpers ======= - Helpers are pretty much the same as before, though some have been simplified. +- Since v4.3.0, you can autoload helpers by **app/Config/Autoload.php** as well as CI3. - In CI4, ``redirect()`` returns a ``RedirectResponse`` instance instead of redirecting and terminating script execution. You must return it. - `redirect() Documentation CodeIgniter 3.X `_ - `redirect() Documentation CodeIgniter 4.X <../general/common_functions.html#redirect>`_ diff --git a/user_guide_src/source/installation/upgrade_validations.rst b/user_guide_src/source/installation/upgrade_validations.rst index 5bd6a2a1d514..8529775ed331 100644 --- a/user_guide_src/source/installation/upgrade_validations.rst +++ b/user_guide_src/source/installation/upgrade_validations.rst @@ -17,12 +17,13 @@ What has been changed - CI4 validation has no Callbacks nor Callable in CI3. - CI4 validation format rules do not permit empty string. - CI4 validation never changes your data. +- Since v4.3.0, :php:func:`validation_errors()` has been introduced, but the API is different from CI3's. Upgrade Guide ============= 1. Within the view which contains the form you have to change: - - ```` to ``listErrors() ?>`` + - ```` to ```` 2. Within the controller you have to change the following: @@ -85,7 +86,7 @@ Path: **app/Views**:: - listErrors() ?> + diff --git a/user_guide_src/source/installation/upgrade_validations/002.php b/user_guide_src/source/installation/upgrade_validations/002.php index ddbed1ede5fc..76f6d08ac8e2 100644 --- a/user_guide_src/source/installation/upgrade_validations/002.php +++ b/user_guide_src/source/installation/upgrade_validations/002.php @@ -13,9 +13,7 @@ public function index() if (! $this->validate([ // Validation rules ])) { - echo view('myform', [ - 'validation' => $this->validator, - ]); + echo view('myform'); } else { echo view('formsuccess'); } diff --git a/user_guide_src/source/installation/upgrading.rst b/user_guide_src/source/installation/upgrading.rst index 84469bb6f987..582ab28c479d 100644 --- a/user_guide_src/source/installation/upgrading.rst +++ b/user_guide_src/source/installation/upgrading.rst @@ -5,6 +5,8 @@ Upgrading From a Previous Version Please read the upgrade notes corresponding to the version you are upgrading from. +See also :doc:`./backward_compatibility_notes`. + .. note:: If you don't know what version of CodeIgniter you are currently running, you can get it from :ref:`the Debug Toolbar `, or simply echo the constant ``\CodeIgniter\CodeIgniter::CI_VERSION``. @@ -12,7 +14,11 @@ upgrading from. .. toctree:: :titlesonly: + backward_compatibility_notes + upgrade_430 + upgrade_426 + upgrade_425 upgrade_423 upgrade_422 upgrade_421 diff --git a/user_guide_src/source/libraries/cookies.rst b/user_guide_src/source/libraries/cookies.rst index 1964eb4cf144..6cfc37327106 100644 --- a/user_guide_src/source/libraries/cookies.rst +++ b/user_guide_src/source/libraries/cookies.rst @@ -223,7 +223,7 @@ In runtime, you can manually supply a new default using the ``Cookie::setDefault Class Reference *************** -.. php:namespace:: CodeIgniter\HTTP\Cookie +.. php:namespace:: CodeIgniter\Cookie .. php:class:: Cookie @@ -326,6 +326,8 @@ Class Reference .. php:method:: withNeverExpiring() + .. important:: This method is deprecated. + :param string $name: :rtype: ``Cookie`` :returns: new ``Cookie`` instance diff --git a/user_guide_src/source/libraries/security.rst b/user_guide_src/source/libraries/security.rst index cbec85970755..d73daddc6ca7 100644 --- a/user_guide_src/source/libraries/security.rst +++ b/user_guide_src/source/libraries/security.rst @@ -112,18 +112,22 @@ may alter this behavior by editing the following config parameter value in Redirection on Failure ---------------------- -When a request fails the CSRF validation check, it will redirect to the previous page by default, -setting an ``error`` flash message that you can display to the end user with the following code in your view:: +Since v4.3.0, when a request fails the CSRF validation check, +it will throw a SecurityException by default, - getFlashdata('error') ?> - -This provides a nicer experience -than simply crashing. This can be turned off by editing the following config parameter value in +If you want to make it redirect to the previous page, +change the following config parameter value in **app/Config/Security.php**: .. literalinclude:: security/005.php -Even when the redirect value is ``true``, AJAX calls will not redirect, but will throw an error. +When redirected, an ``error`` flash message is set and can be displayed to the end user with the following code in your view:: + + getFlashdata('error') ?> + +This provides a nicer experience than simply crashing. + +Even when the redirect value is ``true``, AJAX calls will not redirect, but will throw a SecurityException. Enable CSRF Protection ====================== diff --git a/user_guide_src/source/libraries/security/005.php b/user_guide_src/source/libraries/security/005.php index f7539874c023..13e2b91a7010 100644 --- a/user_guide_src/source/libraries/security/005.php +++ b/user_guide_src/source/libraries/security/005.php @@ -6,6 +6,9 @@ class Security extends BaseConfig { - public $redirect = false; + // ... + + public bool $redirect = true; + // ... } diff --git a/user_guide_src/source/libraries/time.rst b/user_guide_src/source/libraries/time.rst index 89062bf7abf9..e34e7e9ff44e 100644 --- a/user_guide_src/source/libraries/time.rst +++ b/user_guide_src/source/libraries/time.rst @@ -171,6 +171,17 @@ Displays just the localized version of time portion of the value: .. literalinclude:: time/018.php +.. _time-todatabase: + +toDatabase() +============ + +.. versionadded:: 4.3.0 + +This method returns a string that can be used with databases regardless of locale. + +.. literalinclude:: time/042.php + humanize() ========== diff --git a/user_guide_src/source/libraries/time/042.php b/user_guide_src/source/libraries/time/042.php new file mode 100644 index 000000000000..56b7514c9457 --- /dev/null +++ b/user_guide_src/source/libraries/time/042.php @@ -0,0 +1,9 @@ +toDatabase(); // '2016-03-09 12:00:00' + +// Locale: fa +$time = Time::parse('March 9, 2016 12:00:00', 'America/Chicago'); +echo $time->toDatabase(); // '2016-03-09 12:00:00' diff --git a/user_guide_src/source/libraries/validation.rst b/user_guide_src/source/libraries/validation.rst index 6b50b7331e7f..50163030f262 100644 --- a/user_guide_src/source/libraries/validation.rst +++ b/user_guide_src/source/libraries/validation.rst @@ -71,7 +71,7 @@ code and save it to your **app/Views/** folder:: - listErrors() ?> + @@ -166,7 +166,7 @@ The form (**signup.php**) is a standard web form with a couple of exceptions: #. At the top of the form you'll notice the following function call: :: - listErrors() ?> + This function will return any error messages sent back by the validator. If there are no messages it returns an empty string. @@ -505,6 +505,8 @@ When specifying a field with a wildcard, all errors matching the mask will be ch .. literalinclude:: validation/029.php +.. _validation-customizing-error-display: + Customizing Error Display ************************* diff --git a/user_guide_src/source/libraries/validation/001.php b/user_guide_src/source/libraries/validation/001.php index b5e8a6f13812..c3f0d2b35baa 100644 --- a/user_guide_src/source/libraries/validation/001.php +++ b/user_guide_src/source/libraries/validation/001.php @@ -11,17 +11,13 @@ class Form extends BaseController public function index() { if (strtolower($this->request->getMethod()) !== 'post') { - return view('signup', [ - 'validation' => Services::validation(), - ]); + return view('signup'); } $rules = []; if (! $this->validate($rules)) { - return view('signup', [ - 'validation' => $this->validator, - ]); + return view('signup'); } return view('success'); diff --git a/user_guide_src/source/models/model.rst b/user_guide_src/source/models/model.rst index 8159f9181105..7f72023cb955 100644 --- a/user_guide_src/source/models/model.rst +++ b/user_guide_src/source/models/model.rst @@ -399,13 +399,20 @@ Cleans out the database table by permanently removing all rows that have 'delete .. literalinclude:: model/026.php +In-Model Validation +=================== + Validating Data -=============== +--------------- For many people, validating data in the model is the preferred way to ensure the data is kept to a single standard, without duplicating code. The Model class provides a way to automatically have all data validated prior to saving to the database with the ``insert()``, ``update()``, or ``save()`` methods. +.. important:: When you update data, the validation in the model class only validate provided fields. + So when you set the rule ``required``, if you don't pass the required field data, + the validation won't fail. This is to avoid validation errors when updating only some fields. + The first step is to fill out the ``$validationRules`` class property with the fields and rules that should be applied. If you have custom error message that you want to use, place them in the ``$validationMessages`` array: @@ -413,7 +420,11 @@ be applied. If you have custom error message that you want to use, place them in The other way to set the validation rules to fields by functions, -.. php:function:: setValidationRule($field, $fieldRules) +.. php:namespace:: CodeIgniter + +.. php:class:: Model + +.. php:method:: setValidationRule($field, $fieldRules) :param string $field: :param array $fieldRules: @@ -424,7 +435,7 @@ The other way to set the validation rules to fields by functions, .. literalinclude:: model/028.php -.. php:function:: setValidationRules($validationRules) +.. php:method:: setValidationRules($validationRules) :param array $validationRules: @@ -436,7 +447,7 @@ The other way to set the validation rules to fields by functions, The other way to set the validation message to fields by functions, -.. php:function:: setValidationMessage($field, $fieldMessages) +.. php:method:: setValidationMessage($field, $fieldMessages) :param string $field: :param array $fieldMessages: @@ -447,7 +458,7 @@ The other way to set the validation message to fields by functions, .. literalinclude:: model/030.php -.. php:function:: setValidationMessages($fieldMessages) +.. php:method:: setValidationMessages($fieldMessages) :param array $fieldMessages: @@ -473,7 +484,7 @@ and simply set ``$validationRules`` to the name of the validation rule group you .. literalinclude:: model/034.php Retrieving Validation Rules -=========================== +--------------------------- You can retrieve a model's validation rules by accessing its ``validationRules`` property: @@ -492,7 +503,7 @@ value an array of fieldnames of interest: .. literalinclude:: model/037.php Validation Placeholders -======================= +----------------------- The model provides a simple method to replace parts of your rules based on data that's being passed into it. This sounds fairly obscure but can be especially handy with the ``is_unique`` validation rule. Placeholders are simply diff --git a/user_guide_src/source/testing/benchmark.rst b/user_guide_src/source/testing/benchmark.rst index 68429df4aebe..dd27baba4a26 100644 --- a/user_guide_src/source/testing/benchmark.rst +++ b/user_guide_src/source/testing/benchmark.rst @@ -22,22 +22,38 @@ With the Timer, you can measure the time between two moments in the execution of it simple to measure the performance of different aspects of your application. All measurement is done using the ``start()`` and ``stop()`` methods. +Timer::start() +============== + The ``start()`` methods takes a single parameter: the name of this timer. You can use any string as the name of the timer. It is only used for you to reference later to know which measurement is which: .. literalinclude:: benchmark/001.php +Timer::stop() +============= + The ``stop()`` method takes the name of the timer that you want to stop as the only parameter, also: .. literalinclude:: benchmark/002.php The name is not case-sensitive, but otherwise must match the name you gave it when you started the timer. +timer() +======= + Alternatively, you can use the :doc:`global function ` ``timer()`` to start and stop timers: .. literalinclude:: benchmark/003.php +.. _benchmark-timer-record: + +Timer::record() +=============== + +.. versionadded:: 4.3.0 + Since v4.3.0, if you use very small code blocks to benchmark, you can also use the ``record()`` method. It accepts a no-parameter callable and measures its execution time. Methods ``start()`` and ``stop()`` will be called automatically around the function call. diff --git a/user_guide_src/source/testing/database.rst b/user_guide_src/source/testing/database.rst index 382b4ee4aa56..e8d04f2aec72 100644 --- a/user_guide_src/source/testing/database.rst +++ b/user_guide_src/source/testing/database.rst @@ -32,10 +32,13 @@ If you have multiple developers on your team, you will likely want to keep your the **.env** file. To do so, edit the file to ensure the following lines are present and have the correct information:: - database.tests.dbdriver = 'MySQLi'; - database.tests.username = 'root'; - database.tests.password = ''; - database.tests.database = ''; + database.tests.hostname = localhost + database.tests.database = ci4_test + database.tests.username = root + database.tests.password = root + database.tests.DBDriver = MySQLi + database.tests.DBPrefix = + database.tests.port = 3306 Migrations and Seeds ==================== diff --git a/user_guide_src/source/testing/overview.rst b/user_guide_src/source/testing/overview.rst index 749279c0260c..7d021519f7e8 100644 --- a/user_guide_src/source/testing/overview.rst +++ b/user_guide_src/source/testing/overview.rst @@ -277,6 +277,21 @@ See :ref:`Testing Traits `. If you override the ``setUp()`` or ``tearDown()`` methods in your test, then you must call the ``parent::setUp()`` and ``parent::tearDown()`` methods respectively to configure the ``StreamFilterTrait``. +**CITestStreamFilter** for manual/single use. + +If you need to capture streams in only one test, then instead of using the StreamFilterTrait trait, you can manually +add a filter to streams. + +**Overview of methods** + +- ``CITestStreamFilter::registration()`` Filter registration. +- ``CITestStreamFilter::addOutputFilter()`` Adding a filter to the output stream. +- ``CITestStreamFilter::addErrorFilter()`` Adding a filter to the error stream. +- ``CITestStreamFilter::removeOutputFilter()`` Removing a filter from the output stream. +- ``CITestStreamFilter::removeErrorFilter()`` Removing a filter from the error stream. + +.. literalinclude:: overview/020.php + .. _testing-cli-input: Testing CLI Input diff --git a/user_guide_src/source/testing/overview/020.php b/user_guide_src/source/testing/overview/020.php new file mode 100644 index 000000000000..b59250ab0f59 --- /dev/null +++ b/user_guide_src/source/testing/overview/020.php @@ -0,0 +1,18 @@ +request->getMethod() === 'post' && $this->validate([ 'title' => 'required|min_length[3]|max_length[255]', - 'body' => 'required', + 'body' => 'required', ])) { $model->save([ 'title' => $this->request->getPost('title'), diff --git a/utils/Rector/UnderscoreToCamelCaseVariableNameRector.php b/utils/Rector/UnderscoreToCamelCaseVariableNameRector.php index 530fe18b029e..054eb75a2800 100644 --- a/utils/Rector/UnderscoreToCamelCaseVariableNameRector.php +++ b/utils/Rector/UnderscoreToCamelCaseVariableNameRector.php @@ -55,7 +55,7 @@ public function run($a_b) } } CODE_SAMPLE -, + , <<<'CODE_SAMPLE' final class SomeClass {