From eef2c84e121df837eb15579c8cfbb676d0ef3d01 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 12 Apr 2024 11:56:27 +0900 Subject: [PATCH 001/277] docs: add changelog and upgrade for v4.6.0 --- user_guide_src/source/changelogs/index.rst | 1 + user_guide_src/source/changelogs/v4.6.0.rst | 84 +++++++++++++++++++ .../source/installation/upgrade_460.rst | 54 ++++++++++++ .../source/installation/upgrading.rst | 1 + 4 files changed, 140 insertions(+) create mode 100644 user_guide_src/source/changelogs/v4.6.0.rst create mode 100644 user_guide_src/source/installation/upgrade_460.rst diff --git a/user_guide_src/source/changelogs/index.rst b/user_guide_src/source/changelogs/index.rst index 299644d7f060..b0992e086a6b 100644 --- a/user_guide_src/source/changelogs/index.rst +++ b/user_guide_src/source/changelogs/index.rst @@ -12,6 +12,7 @@ See all the changes. .. toctree:: :titlesonly: + v4.6.0 v4.5.2 v4.5.1 v4.5.0 diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst new file mode 100644 index 000000000000..998e5ba269d6 --- /dev/null +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -0,0 +1,84 @@ +############# +Version 4.6.0 +############# + +Release Date: Unreleased + +**4.6.0 release of CodeIgniter4** + +.. contents:: + :local: + :depth: 3 + +********** +Highlights +********** + +- TBD + +******** +BREAKING +******** + +Behavior Changes +================ + +Interface Changes +================= + +Method Signature Changes +======================== + +************ +Enhancements +************ + +Commands +======== + +Testing +======= + +Database +======== + +Query Builder +------------- + +Forge +----- + +Others +------ + +Model +===== + +Libraries +========= + +Helpers and Functions +===================== + +Others +====== + +*************** +Message Changes +*************** + +******* +Changes +******* + +************ +Deprecations +************ + +********** +Bugs Fixed +********** + +See the repo's +`CHANGELOG.md `_ +for a complete list of bugs fixed. diff --git a/user_guide_src/source/installation/upgrade_460.rst b/user_guide_src/source/installation/upgrade_460.rst new file mode 100644 index 000000000000..15af1aca3c4a --- /dev/null +++ b/user_guide_src/source/installation/upgrade_460.rst @@ -0,0 +1,54 @@ +############################# +Upgrading from 4.5.x to 4.6.0 +############################# + +Please refer to the upgrade instructions corresponding to your installation method. + +- :ref:`Composer Installation App Starter Upgrading ` +- :ref:`Composer Installation Adding CodeIgniter4 to an Existing Project Upgrading ` +- :ref:`Manual Installation Upgrading ` + +.. contents:: + :local: + :depth: 2 + +********************** +Mandatory File Changes +********************** + +**************** +Breaking Changes +**************** + +********************* +Breaking Enhancements +********************* + +************* +Project Files +************* + +Some files in the **project space** (root, app, public, writable) received updates. Due to +these files being outside of the **system** scope they will not be changed without your intervention. + +There are some third-party CodeIgniter modules available to assist with merging changes to +the project space: `Explore on Packagist `_. + +Content Changes +=============== + +The following files received significant changes (including deprecations or visual adjustments) +and it is recommended that you merge the updated versions with your application: + +Config +------ + +- @TODO + +All Changes +=========== + +This is a list of all files in the **project space** that received changes; +many will be simple comments or formatting that have no effect on the runtime: + +- @TODO diff --git a/user_guide_src/source/installation/upgrading.rst b/user_guide_src/source/installation/upgrading.rst index b89d0619c89d..949bfeb15d96 100644 --- a/user_guide_src/source/installation/upgrading.rst +++ b/user_guide_src/source/installation/upgrading.rst @@ -16,6 +16,7 @@ See also :doc:`./backward_compatibility_notes`. backward_compatibility_notes + upgrade_460 upgrade_452 upgrade_451 upgrade_450 From 76593b06d9d20b6e15dd0c6357e4ae26883ac78e Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 16 Apr 2024 11:08:57 +0900 Subject: [PATCH 002/277] refactor: remove deprecated failValidationError() --- system/API/ResponseTrait.php | 12 ------------ tests/system/API/ResponseTraitTest.php | 18 ------------------ user_guide_src/source/changelogs/v4.6.0.rst | 8 ++++++++ .../source/installation/upgrade_460.rst | 7 +++++++ 4 files changed, 15 insertions(+), 30 deletions(-) diff --git a/system/API/ResponseTrait.php b/system/API/ResponseTrait.php index 01ce42860b32..7d70feab5f8c 100644 --- a/system/API/ResponseTrait.php +++ b/system/API/ResponseTrait.php @@ -224,18 +224,6 @@ protected function failNotFound(string $description = 'Not Found', ?string $code return $this->fail($description, $this->codes['resource_not_found'], $code, $message); } - /** - * Used when the data provided by the client cannot be validated. - * - * @return ResponseInterface - * - * @deprecated Use failValidationErrors instead - */ - protected function failValidationError(string $description = 'Bad Request', ?string $code = null, string $message = '') - { - return $this->fail($description, $this->codes['invalid_data'], $code, $message); - } - /** * Used when the data provided by the client cannot be validated on one or more fields. * diff --git a/tests/system/API/ResponseTraitTest.php b/tests/system/API/ResponseTraitTest.php index b54f3037b247..266b5c548cfd 100644 --- a/tests/system/API/ResponseTraitTest.php +++ b/tests/system/API/ResponseTraitTest.php @@ -447,24 +447,6 @@ public function testNotFound(): void $this->assertSame($this->formatter->format($expected), $this->response->getBody()); } - public function testValidationError(): void - { - $controller = $this->makeController(); - - $this->invoke($controller, 'failValidationError', ['Nope', 'FAT CHANCE', 'A Custom Reason']); - - $expected = [ - 'status' => 400, - 'error' => 'FAT CHANCE', - 'messages' => [ - 'error' => 'Nope', - ], - ]; - $this->assertSame('A Custom Reason', $this->response->getReasonPhrase()); - $this->assertSame(400, $this->response->getStatusCode()); - $this->assertSame($this->formatter->format($expected), $this->response->getBody()); - } - public function testValidationErrors(): void { $controller = $this->makeController(); diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index 998e5ba269d6..63e10d4b9454 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -29,6 +29,14 @@ Interface Changes Method Signature Changes ======================== +.. _v460-removed-deprecated-items: + +Removed Deprecated Items +======================== + +- **API:** The deprecated ``failValidationError()`` method in ``CodeIgniter\API\ResponseTrait`` + has been removed. Use ``failValidationErrors()`` instead. + ************ Enhancements ************ diff --git a/user_guide_src/source/installation/upgrade_460.rst b/user_guide_src/source/installation/upgrade_460.rst index 15af1aca3c4a..4344f7565b55 100644 --- a/user_guide_src/source/installation/upgrade_460.rst +++ b/user_guide_src/source/installation/upgrade_460.rst @@ -20,6 +20,13 @@ Mandatory File Changes Breaking Changes **************** +Removed Deprecated Items +======================== + +Some deprecated items have been removed. If you are still using these items, or +extending these classes, upgrade your code. +See :ref:`ChangeLog ` for details. + ********************* Breaking Enhancements ********************* From 060041e07172eed224dac38fc7666ab09b140ef3 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 16 Apr 2024 11:26:26 +0900 Subject: [PATCH 003/277] chore: vendor/bin/phpstan analyze --generate-baseline phpstan-baseline.php --- phpstan-baseline.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/phpstan-baseline.php b/phpstan-baseline.php index cfee07815e8a..bc99f1faf04e 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -10782,42 +10782,42 @@ 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:639\\:\\:__construct\\(\\) has parameter \\$request with no type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:621\\:\\:__construct\\(\\) has parameter \\$request with no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:639\\:\\:__construct\\(\\) has parameter \\$response with no type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:621\\:\\:__construct\\(\\) has parameter \\$response with no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:639\\:\\:fail\\(\\) has parameter \\$messages with no value type specified in iterable type array\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:621\\:\\:fail\\(\\) has parameter \\$messages with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:639\\:\\:format\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:621\\:\\:format\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:639\\:\\:respond\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:621\\:\\:respond\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:639\\:\\:respondCreated\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:621\\:\\:respondCreated\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:639\\:\\:respondDeleted\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:621\\:\\:respondDeleted\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:639\\:\\:respondUpdated\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:621\\:\\:respondUpdated\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; @@ -10867,17 +10867,17 @@ 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:639\\:\\:\\$format \\(\'html\'\\|\'json\'\\|\'xml\'\\|null\\) does not accept \'txt\'\\.$#', + 'message' => '#^Property class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:621\\:\\:\\$format \\(\'html\'\\|\'json\'\\|\'xml\'\\|null\\) does not accept \'txt\'\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:639\\:\\:\\$request has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:621\\:\\:\\$request has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:639\\:\\:\\$response has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:621\\:\\:\\$response has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; From e46175ee1f6a719319554ed575ee9b1d17e6ea3e Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 16 Apr 2024 11:32:55 +0900 Subject: [PATCH 004/277] docs: update sample code --- user_guide_src/source/outgoing/api_responses/002.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/outgoing/api_responses/002.php b/user_guide_src/source/outgoing/api_responses/002.php index e383ddc3c09f..6eefa7faae39 100644 --- a/user_guide_src/source/outgoing/api_responses/002.php +++ b/user_guide_src/source/outgoing/api_responses/002.php @@ -25,7 +25,7 @@ $this->failNotFound($description); // Data did not validate -$this->failValidationError($description); +$this->failValidationErrors($description); // Resource already exists $this->failResourceExists($description); From f47396a87ae354a727854c9c0bca8df592be38f1 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 30 Apr 2024 11:37:56 +0900 Subject: [PATCH 005/277] refactor!: remove depreacted ResponseInterface::getReason() --- system/HTTP/Response.php | 15 --------------- system/HTTP/ResponseInterface.php | 10 ---------- user_guide_src/source/changelogs/v4.6.0.rst | 2 ++ 3 files changed, 2 insertions(+), 25 deletions(-) diff --git a/system/HTTP/Response.php b/system/HTTP/Response.php index a19fbb5fee14..89ba119b11af 100644 --- a/system/HTTP/Response.php +++ b/system/HTTP/Response.php @@ -205,21 +205,6 @@ public function getStatusCode(): int return $this->statusCode; } - /** - * Gets the response response phrase associated with the status code. - * - * @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 - */ - public function getReason(): string - { - return $this->getReasonPhrase(); - } - /** * Gets the response reason phrase associated with the status code. * diff --git a/system/HTTP/ResponseInterface.php b/system/HTTP/ResponseInterface.php index 827eea44e298..68ec5c3fce7f 100644 --- a/system/HTTP/ResponseInterface.php +++ b/system/HTTP/ResponseInterface.php @@ -135,16 +135,6 @@ public function getStatusCode(): int; */ public function setStatusCode(int $code, string $reason = ''); - /** - * Gets the response phrase associated with the status code. - * - * @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; - /** * Gets the response reason phrase associated with the status code. * diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index 63e10d4b9454..afc7d89d9d51 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -36,6 +36,8 @@ Removed Deprecated Items - **API:** The deprecated ``failValidationError()`` method in ``CodeIgniter\API\ResponseTrait`` has been removed. Use ``failValidationErrors()`` instead. +- **HTTP:** The deprecated ``getReason()`` method in ``CodeIgniter\HTTP\Response`` + and ``ResponseInterface`` has been removed. Use ``getReasonPhrase()`` instead. ************ Enhancements From 1ca470f9ea4fd5f8f1be55d2a584f99298db030f Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 30 Apr 2024 12:01:14 +0900 Subject: [PATCH 006/277] refactor!: remove deprecated Logger::cleanFilenames() and TestLogger::cleanup() --- system/Log/Logger.php | 15 --------------- system/Test/TestLogger.php | 14 -------------- user_guide_src/source/changelogs/v4.6.0.rst | 3 +++ 3 files changed, 3 insertions(+), 29 deletions(-) diff --git a/system/Log/Logger.php b/system/Log/Logger.php index fca608f91ff8..369db8ffb36d 100644 --- a/system/Log/Logger.php +++ b/system/Log/Logger.php @@ -410,19 +410,4 @@ public function determineFile(): array 'unknown', ]; } - - /** - * Cleans the paths of filenames by replacing APPPATH, SYSTEMPATH, FCPATH - * with the actual var. i.e. - * - * /var/www/site/app/Controllers/Home.php - * becomes: - * APPPATH/Controllers/Home.php - * - * @deprecated Use dedicated `clean_path()` function. - */ - protected function cleanFileNames(string $file): string - { - return clean_path($file); - } } diff --git a/system/Test/TestLogger.php b/system/Test/TestLogger.php index 73fbab5571a7..bbd0839e93b2 100644 --- a/system/Test/TestLogger.php +++ b/system/Test/TestLogger.php @@ -92,18 +92,4 @@ public static function didLog(string $level, $message, bool $useExactComparison return false; } - - /** - * Expose filenames. - * - * @param string $file - * - * @return string - * - * @deprecated No longer needed as underlying protected method is also deprecated. - */ - public function cleanup($file) - { - return clean_path($file); - } } diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index afc7d89d9d51..becb1ec0688c 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -38,6 +38,9 @@ Removed Deprecated Items has been removed. Use ``failValidationErrors()`` instead. - **HTTP:** The deprecated ``getReason()`` method in ``CodeIgniter\HTTP\Response`` and ``ResponseInterface`` has been removed. Use ``getReasonPhrase()`` instead. +- **Logger:** The deprecated ``CodeIgniter\Log\Logger::cleanFilenames()`` and + ``CodeIgniter\Test\TestLogger::cleanup()`` have been removed. Use the + ``clean_path()`` function instead. ************ Enhancements From 2f72086ac3dc7acc60a68ac86bb0d57779828de7 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 27 Jan 2024 18:41:45 +0900 Subject: [PATCH 007/277] feat: add base RuntimeException and LogicException --- system/Exceptions/LogicException.php | 21 +++++++++++++++++++++ system/Exceptions/RuntimeException.php | 21 +++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 system/Exceptions/LogicException.php create mode 100644 system/Exceptions/RuntimeException.php diff --git a/system/Exceptions/LogicException.php b/system/Exceptions/LogicException.php new file mode 100644 index 000000000000..2fe46792efcc --- /dev/null +++ b/system/Exceptions/LogicException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Exceptions; + +/** + * Exception that represents error in the program logic. + */ +class LogicException extends \LogicException implements ExceptionInterface +{ +} diff --git a/system/Exceptions/RuntimeException.php b/system/Exceptions/RuntimeException.php new file mode 100644 index 000000000000..c35d55ba9e45 --- /dev/null +++ b/system/Exceptions/RuntimeException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Exceptions; + +/** + * Exception thrown if an error which can only be found on runtime occurs. + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} From 50c43a3110e63727b8bb7448399f177069de0753 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 27 Jan 2024 18:42:54 +0900 Subject: [PATCH 008/277] feat: add BadMethodCallException,BadFunctionCallException,InvalidArgumentException --- .../Exceptions/BadFunctionCallException.php | 22 +++++++++++++++++++ system/Exceptions/BadMethodCallException.php | 22 +++++++++++++++++++ .../Exceptions/InvalidArgumentException.php | 21 ++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 system/Exceptions/BadFunctionCallException.php create mode 100644 system/Exceptions/BadMethodCallException.php create mode 100644 system/Exceptions/InvalidArgumentException.php diff --git a/system/Exceptions/BadFunctionCallException.php b/system/Exceptions/BadFunctionCallException.php new file mode 100644 index 000000000000..84d9798dc0c4 --- /dev/null +++ b/system/Exceptions/BadFunctionCallException.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Exceptions; + +/** + * Exception thrown if a function is called in the wrong way, or the function + * does not exist. + */ +class BadFunctionCallException extends \BadFunctionCallException implements ExceptionInterface +{ +} diff --git a/system/Exceptions/BadMethodCallException.php b/system/Exceptions/BadMethodCallException.php new file mode 100644 index 000000000000..977e8ae721e2 --- /dev/null +++ b/system/Exceptions/BadMethodCallException.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Exceptions; + +/** + * Exception thrown if a method is called in the wrong way, or the method + * does not exist. + */ +class BadMethodCallException extends \BadMethodCallException implements ExceptionInterface +{ +} diff --git a/system/Exceptions/InvalidArgumentException.php b/system/Exceptions/InvalidArgumentException.php new file mode 100644 index 000000000000..4790249e3f78 --- /dev/null +++ b/system/Exceptions/InvalidArgumentException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Exceptions; + +/** + * Exception thrown if an argument is not of the expected type. + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} From 085eb2d76c85fa880870c507c81acf87e2bbb506 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 27 Jan 2024 18:46:17 +0900 Subject: [PATCH 009/277] refactor: FrameworkException extends RuntimeException --- system/Exceptions/FrameworkException.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/system/Exceptions/FrameworkException.php b/system/Exceptions/FrameworkException.php index 1fe47be49a6b..0141b139de62 100644 --- a/system/Exceptions/FrameworkException.php +++ b/system/Exceptions/FrameworkException.php @@ -13,15 +13,13 @@ namespace CodeIgniter\Exceptions; -use RuntimeException; - /** * Class FrameworkException * * A collection of exceptions thrown by the framework * that can only be determined at run time. */ -class FrameworkException extends RuntimeException implements ExceptionInterface +class FrameworkException extends RuntimeException { use DebugTraceableTrait; From 5178dd171d4ee9ce43e024d684d5a10ddce63d36 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 27 Jan 2024 18:50:22 +0900 Subject: [PATCH 010/277] feat: extends ExceptionInterface --- system/Exceptions/HTTPExceptionInterface.php | 2 +- system/Exceptions/HasExitCodeInterface.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/system/Exceptions/HTTPExceptionInterface.php b/system/Exceptions/HTTPExceptionInterface.php index 1974d63af6e5..b26f121fb9a6 100644 --- a/system/Exceptions/HTTPExceptionInterface.php +++ b/system/Exceptions/HTTPExceptionInterface.php @@ -16,6 +16,6 @@ /** * Interface for Exceptions that has exception code as HTTP status code. */ -interface HTTPExceptionInterface +interface HTTPExceptionInterface extends ExceptionInterface { } diff --git a/system/Exceptions/HasExitCodeInterface.php b/system/Exceptions/HasExitCodeInterface.php index 1557c82ad681..48d32655714c 100644 --- a/system/Exceptions/HasExitCodeInterface.php +++ b/system/Exceptions/HasExitCodeInterface.php @@ -16,7 +16,7 @@ /** * Interface for Exceptions that has exception code as exit code. */ -interface HasExitCodeInterface +interface HasExitCodeInterface extends ExceptionInterface { /** * Returns exit status code. From 9d2c903f7f42a45fcb74e71eab662c11e6b1c17f Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 27 Jan 2024 19:06:44 +0900 Subject: [PATCH 011/277] refactor: change parent class --- system/CLI/Exceptions/CLIException.php | 2 +- system/Exceptions/DownloadException.php | 4 +--- system/Files/Exceptions/FileException.php | 5 ++--- system/Files/Exceptions/FileNotFoundException.php | 5 ++--- system/Format/Exceptions/FormatException.php | 5 ++--- system/Router/Exceptions/MethodNotFoundException.php | 2 +- 6 files changed, 9 insertions(+), 14 deletions(-) diff --git a/system/CLI/Exceptions/CLIException.php b/system/CLI/Exceptions/CLIException.php index fae92c001d75..304b2ea4f444 100644 --- a/system/CLI/Exceptions/CLIException.php +++ b/system/CLI/Exceptions/CLIException.php @@ -14,7 +14,7 @@ namespace CodeIgniter\CLI\Exceptions; use CodeIgniter\Exceptions\DebugTraceableTrait; -use RuntimeException; +use CodeIgniter\Exceptions\RuntimeException; /** * CLIException diff --git a/system/Exceptions/DownloadException.php b/system/Exceptions/DownloadException.php index df78127d6d64..d4b224fdee4d 100644 --- a/system/Exceptions/DownloadException.php +++ b/system/Exceptions/DownloadException.php @@ -13,12 +13,10 @@ namespace CodeIgniter\Exceptions; -use RuntimeException; - /** * Class DownloadException */ -class DownloadException extends RuntimeException implements ExceptionInterface +class DownloadException extends RuntimeException { use DebugTraceableTrait; diff --git a/system/Files/Exceptions/FileException.php b/system/Files/Exceptions/FileException.php index 5feb97929ead..8634b211afa4 100644 --- a/system/Files/Exceptions/FileException.php +++ b/system/Files/Exceptions/FileException.php @@ -14,10 +14,9 @@ namespace CodeIgniter\Files\Exceptions; use CodeIgniter\Exceptions\DebugTraceableTrait; -use CodeIgniter\Exceptions\ExceptionInterface; -use RuntimeException; +use CodeIgniter\Exceptions\RuntimeException; -class FileException extends RuntimeException implements ExceptionInterface +class FileException extends RuntimeException { use DebugTraceableTrait; diff --git a/system/Files/Exceptions/FileNotFoundException.php b/system/Files/Exceptions/FileNotFoundException.php index 86b22625aa9e..46c2c1c6cfd5 100644 --- a/system/Files/Exceptions/FileNotFoundException.php +++ b/system/Files/Exceptions/FileNotFoundException.php @@ -14,10 +14,9 @@ namespace CodeIgniter\Files\Exceptions; use CodeIgniter\Exceptions\DebugTraceableTrait; -use CodeIgniter\Exceptions\ExceptionInterface; -use RuntimeException; +use CodeIgniter\Exceptions\RuntimeException; -class FileNotFoundException extends RuntimeException implements ExceptionInterface +class FileNotFoundException extends RuntimeException { use DebugTraceableTrait; diff --git a/system/Format/Exceptions/FormatException.php b/system/Format/Exceptions/FormatException.php index 5a8c2d2cfedb..46daad558c50 100644 --- a/system/Format/Exceptions/FormatException.php +++ b/system/Format/Exceptions/FormatException.php @@ -14,13 +14,12 @@ namespace CodeIgniter\Format\Exceptions; use CodeIgniter\Exceptions\DebugTraceableTrait; -use CodeIgniter\Exceptions\ExceptionInterface; -use RuntimeException; +use CodeIgniter\Exceptions\RuntimeException; /** * FormatException */ -class FormatException extends RuntimeException implements ExceptionInterface +class FormatException extends RuntimeException { use DebugTraceableTrait; diff --git a/system/Router/Exceptions/MethodNotFoundException.php b/system/Router/Exceptions/MethodNotFoundException.php index 6e82fb09583c..19e62eb02c6d 100644 --- a/system/Router/Exceptions/MethodNotFoundException.php +++ b/system/Router/Exceptions/MethodNotFoundException.php @@ -13,7 +13,7 @@ namespace CodeIgniter\Router\Exceptions; -use RuntimeException; +use CodeIgniter\Exceptions\RuntimeException; /** * @internal From 8e5e2078f56ea044cc33d589d8c8cd6288d66730 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 27 Jan 2024 19:19:52 +0900 Subject: [PATCH 012/277] refactor: remove unneeded `implements ExceptionInterface` --- system/Filters/Exceptions/FilterException.php | 3 +-- system/Honeypot/Exceptions/HoneypotException.php | 3 +-- system/Images/Exceptions/ImageException.php | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/system/Filters/Exceptions/FilterException.php b/system/Filters/Exceptions/FilterException.php index 1226ba318429..3a4d914e3cc3 100644 --- a/system/Filters/Exceptions/FilterException.php +++ b/system/Filters/Exceptions/FilterException.php @@ -14,12 +14,11 @@ namespace CodeIgniter\Filters\Exceptions; use CodeIgniter\Exceptions\ConfigException; -use CodeIgniter\Exceptions\ExceptionInterface; /** * FilterException */ -class FilterException extends ConfigException implements ExceptionInterface +class FilterException extends ConfigException { /** * Thrown when the provided alias is not within diff --git a/system/Honeypot/Exceptions/HoneypotException.php b/system/Honeypot/Exceptions/HoneypotException.php index ce743b8651b3..7ff493e4edf8 100644 --- a/system/Honeypot/Exceptions/HoneypotException.php +++ b/system/Honeypot/Exceptions/HoneypotException.php @@ -14,9 +14,8 @@ namespace CodeIgniter\Honeypot\Exceptions; use CodeIgniter\Exceptions\ConfigException; -use CodeIgniter\Exceptions\ExceptionInterface; -class HoneypotException extends ConfigException implements ExceptionInterface +class HoneypotException extends ConfigException { /** * Thrown when the template value of config is empty. diff --git a/system/Images/Exceptions/ImageException.php b/system/Images/Exceptions/ImageException.php index 93491bfa9e1b..91bf416d20ac 100644 --- a/system/Images/Exceptions/ImageException.php +++ b/system/Images/Exceptions/ImageException.php @@ -13,10 +13,9 @@ namespace CodeIgniter\Images\Exceptions; -use CodeIgniter\Exceptions\ExceptionInterface; use CodeIgniter\Exceptions\FrameworkException; -class ImageException extends FrameworkException implements ExceptionInterface +class ImageException extends FrameworkException { /** * Thrown when the image is not found. From 2087c55a627390cead195630b84baa303bf08835 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 27 Jan 2024 19:20:37 +0900 Subject: [PATCH 013/277] fix: parent exception classes --- system/Database/Exceptions/DatabaseException.php | 4 ++-- system/Exceptions/ConfigException.php | 2 +- system/Exceptions/CriticalError.php | 4 +--- system/Exceptions/PageNotFoundException.php | 3 +-- system/Exceptions/TestException.php | 2 +- system/HTTP/Exceptions/RedirectException.php | 4 ++-- 6 files changed, 8 insertions(+), 11 deletions(-) diff --git a/system/Database/Exceptions/DatabaseException.php b/system/Database/Exceptions/DatabaseException.php index cdb8e22832ec..244d45d09894 100644 --- a/system/Database/Exceptions/DatabaseException.php +++ b/system/Database/Exceptions/DatabaseException.php @@ -14,9 +14,9 @@ namespace CodeIgniter\Database\Exceptions; use CodeIgniter\Exceptions\HasExitCodeInterface; -use Error; +use CodeIgniter\Exceptions\RuntimeException; -class DatabaseException extends Error implements ExceptionInterface, HasExitCodeInterface +class DatabaseException extends RuntimeException implements HasExitCodeInterface { public function getExitCode(): int { diff --git a/system/Exceptions/ConfigException.php b/system/Exceptions/ConfigException.php index d8849b809c68..d8d27d094230 100644 --- a/system/Exceptions/ConfigException.php +++ b/system/Exceptions/ConfigException.php @@ -16,7 +16,7 @@ /** * Exception for automatic logging. */ -class ConfigException extends CriticalError implements HasExitCodeInterface +class ConfigException extends RuntimeException implements HasExitCodeInterface { use DebugTraceableTrait; diff --git a/system/Exceptions/CriticalError.php b/system/Exceptions/CriticalError.php index 756393d5d95d..d3f6803b7925 100644 --- a/system/Exceptions/CriticalError.php +++ b/system/Exceptions/CriticalError.php @@ -13,11 +13,9 @@ namespace CodeIgniter\Exceptions; -use Error; - /** * Error: Critical conditions, like component unavailable, etc. */ -class CriticalError extends Error +class CriticalError extends RuntimeException { } diff --git a/system/Exceptions/PageNotFoundException.php b/system/Exceptions/PageNotFoundException.php index b1af079e3a54..ba9b98c49350 100644 --- a/system/Exceptions/PageNotFoundException.php +++ b/system/Exceptions/PageNotFoundException.php @@ -14,9 +14,8 @@ namespace CodeIgniter\Exceptions; use Config\Services; -use OutOfBoundsException; -class PageNotFoundException extends OutOfBoundsException implements ExceptionInterface, HTTPExceptionInterface +class PageNotFoundException extends RuntimeException implements HTTPExceptionInterface { use DebugTraceableTrait; diff --git a/system/Exceptions/TestException.php b/system/Exceptions/TestException.php index f533dc279335..fd70709a6cb2 100644 --- a/system/Exceptions/TestException.php +++ b/system/Exceptions/TestException.php @@ -16,7 +16,7 @@ /** * Exception for automatic logging. */ -class TestException extends CriticalError +class TestException extends LogicException { use DebugTraceableTrait; diff --git a/system/HTTP/Exceptions/RedirectException.php b/system/HTTP/Exceptions/RedirectException.php index 5f2dcd05e3b7..318ffed42227 100644 --- a/system/HTTP/Exceptions/RedirectException.php +++ b/system/HTTP/Exceptions/RedirectException.php @@ -14,9 +14,9 @@ namespace CodeIgniter\HTTP\Exceptions; use CodeIgniter\Exceptions\HTTPExceptionInterface; +use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\HTTP\ResponsableInterface; use CodeIgniter\HTTP\ResponseInterface; -use Exception; use InvalidArgumentException; use LogicException; use Throwable; @@ -24,7 +24,7 @@ /** * RedirectException */ -class RedirectException extends Exception implements ResponsableInterface, HTTPExceptionInterface +class RedirectException extends RuntimeException implements ResponsableInterface, HTTPExceptionInterface { /** * HTTP status code for redirects From 3540f97e9e935ab84bf9ccb7019bb34b4b9f609c Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 27 Jan 2024 20:23:13 +0900 Subject: [PATCH 014/277] refactor: use CodeIgniter\Exceptions\RuntimeException --- system/Autoloader/Autoloader.php | 2 +- system/Cache/Exceptions/CacheException.php | 2 +- system/Config/BaseConfig.php | 2 +- system/Database/Exceptions/DataException.php | 2 +- system/Database/Forge.php | 2 +- system/Database/MigrationRunner.php | 2 +- system/Debug/Timer.php | 2 +- system/Encryption/Exceptions/EncryptionException.php | 2 +- system/HTTP/CLIRequest.php | 2 +- system/HTTP/Files/UploadedFile.php | 2 ++ system/HTTP/Files/UploadedFileInterface.php | 2 +- system/Log/Logger.php | 2 +- system/Publisher/ContentReplacer.php | 2 +- system/Publisher/Publisher.php | 2 +- system/Test/Fabricator.php | 2 +- system/Test/FilterTestTrait.php | 2 +- system/View/View.php | 2 +- 17 files changed, 18 insertions(+), 16 deletions(-) diff --git a/system/Autoloader/Autoloader.php b/system/Autoloader/Autoloader.php index 78d31eae0d7f..3aef07ae635f 100644 --- a/system/Autoloader/Autoloader.php +++ b/system/Autoloader/Autoloader.php @@ -14,6 +14,7 @@ namespace CodeIgniter\Autoloader; use CodeIgniter\Exceptions\ConfigException; +use CodeIgniter\Exceptions\RuntimeException; use Composer\Autoload\ClassLoader; use Composer\InstalledVersions; use Config\Autoload; @@ -24,7 +25,6 @@ use Kint; use Kint\Renderer\CliRenderer; use Kint\Renderer\RichRenderer; -use RuntimeException; /** * An autoloader that uses both PSR4 autoloading, and traditional classmaps. diff --git a/system/Cache/Exceptions/CacheException.php b/system/Cache/Exceptions/CacheException.php index 852e6302ced1..774daa9965b2 100644 --- a/system/Cache/Exceptions/CacheException.php +++ b/system/Cache/Exceptions/CacheException.php @@ -15,7 +15,7 @@ use CodeIgniter\Exceptions\DebugTraceableTrait; use CodeIgniter\Exceptions\ExceptionInterface; -use RuntimeException; +use CodeIgniter\Exceptions\RuntimeException; /** * CacheException diff --git a/system/Config/BaseConfig.php b/system/Config/BaseConfig.php index 8a82cbf7ce64..8f579ffc88f3 100644 --- a/system/Config/BaseConfig.php +++ b/system/Config/BaseConfig.php @@ -11,11 +11,11 @@ namespace CodeIgniter\Config; +use CodeIgniter\Exceptions\RuntimeException; use Config\Encryption; use Config\Modules; use ReflectionClass; use ReflectionException; -use RuntimeException; /** * Class BaseConfig diff --git a/system/Database/Exceptions/DataException.php b/system/Database/Exceptions/DataException.php index 18a54171cc29..a7ccd8c6f5c3 100644 --- a/system/Database/Exceptions/DataException.php +++ b/system/Database/Exceptions/DataException.php @@ -14,7 +14,7 @@ namespace CodeIgniter\Database\Exceptions; use CodeIgniter\Exceptions\DebugTraceableTrait; -use RuntimeException; +use CodeIgniter\Exceptions\RuntimeException; class DataException extends RuntimeException implements ExceptionInterface { diff --git a/system/Database/Forge.php b/system/Database/Forge.php index 797b685e861c..ca0428d9f7b3 100644 --- a/system/Database/Forge.php +++ b/system/Database/Forge.php @@ -14,8 +14,8 @@ namespace CodeIgniter\Database; use CodeIgniter\Database\Exceptions\DatabaseException; +use CodeIgniter\Exceptions\RuntimeException; use InvalidArgumentException; -use RuntimeException; use Throwable; /** diff --git a/system/Database/MigrationRunner.php b/system/Database/MigrationRunner.php index 719d45c15592..bf890ff24305 100644 --- a/system/Database/MigrationRunner.php +++ b/system/Database/MigrationRunner.php @@ -16,11 +16,11 @@ use CodeIgniter\CLI\CLI; use CodeIgniter\Events\Events; use CodeIgniter\Exceptions\ConfigException; +use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\I18n\Time; use Config\Database; use Config\Migrations as MigrationsConfig; use Config\Services; -use RuntimeException; use stdClass; /** diff --git a/system/Debug/Timer.php b/system/Debug/Timer.php index 0f2abdbea509..37e9901977e7 100644 --- a/system/Debug/Timer.php +++ b/system/Debug/Timer.php @@ -13,7 +13,7 @@ namespace CodeIgniter\Debug; -use RuntimeException; +use CodeIgniter\Exceptions\RuntimeException; /** * Class Timer diff --git a/system/Encryption/Exceptions/EncryptionException.php b/system/Encryption/Exceptions/EncryptionException.php index c2220bb88cb4..e18899467bd5 100644 --- a/system/Encryption/Exceptions/EncryptionException.php +++ b/system/Encryption/Exceptions/EncryptionException.php @@ -15,7 +15,7 @@ use CodeIgniter\Exceptions\DebugTraceableTrait; use CodeIgniter\Exceptions\ExceptionInterface; -use RuntimeException; +use CodeIgniter\Exceptions\RuntimeException; /** * Encryption exception diff --git a/system/HTTP/CLIRequest.php b/system/HTTP/CLIRequest.php index 0b2c57377e65..723c72b1ce98 100644 --- a/system/HTTP/CLIRequest.php +++ b/system/HTTP/CLIRequest.php @@ -13,9 +13,9 @@ namespace CodeIgniter\HTTP; +use CodeIgniter\Exceptions\RuntimeException; use Config\App; use Locale; -use RuntimeException; /** * Represents a request from the command-line. Provides additional diff --git a/system/HTTP/Files/UploadedFile.php b/system/HTTP/Files/UploadedFile.php index 78643aa7166e..da029d7739ca 100644 --- a/system/HTTP/Files/UploadedFile.php +++ b/system/HTTP/Files/UploadedFile.php @@ -13,10 +13,12 @@ namespace CodeIgniter\HTTP\Files; +use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\Files\File; use CodeIgniter\HTTP\Exceptions\HTTPException; use Config\Mimes; use Exception; +use InvalidArgumentException; /** * Value object representing a single file uploaded through an diff --git a/system/HTTP/Files/UploadedFileInterface.php b/system/HTTP/Files/UploadedFileInterface.php index 12cc25a0d050..167b0aa2f4d6 100644 --- a/system/HTTP/Files/UploadedFileInterface.php +++ b/system/HTTP/Files/UploadedFileInterface.php @@ -13,8 +13,8 @@ namespace CodeIgniter\HTTP\Files; +use CodeIgniter\Exceptions\RuntimeException; use InvalidArgumentException; -use RuntimeException; /** * Value object representing a single file uploaded through an diff --git a/system/Log/Logger.php b/system/Log/Logger.php index 369db8ffb36d..6762e3380ecf 100644 --- a/system/Log/Logger.php +++ b/system/Log/Logger.php @@ -13,10 +13,10 @@ namespace CodeIgniter\Log; +use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\Log\Exceptions\LogException; use CodeIgniter\Log\Handlers\HandlerInterface; use Psr\Log\LoggerInterface; -use RuntimeException; use Stringable; use Throwable; diff --git a/system/Publisher/ContentReplacer.php b/system/Publisher/ContentReplacer.php index 72dd8ace43b8..6e8c01fa2e41 100644 --- a/system/Publisher/ContentReplacer.php +++ b/system/Publisher/ContentReplacer.php @@ -13,7 +13,7 @@ namespace CodeIgniter\Publisher; -use RuntimeException; +use CodeIgniter\Exceptions\RuntimeException; /** * Replace Text Content diff --git a/system/Publisher/Publisher.php b/system/Publisher/Publisher.php index 90c966623b2a..bf81f6079b60 100644 --- a/system/Publisher/Publisher.php +++ b/system/Publisher/Publisher.php @@ -14,11 +14,11 @@ namespace CodeIgniter\Publisher; use CodeIgniter\Autoloader\FileLocatorInterface; +use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\Files\FileCollection; use CodeIgniter\HTTP\URI; use CodeIgniter\Publisher\Exceptions\PublisherException; use Config\Publisher as PublisherConfig; -use RuntimeException; use Throwable; /** diff --git a/system/Test/Fabricator.php b/system/Test/Fabricator.php index 8da0c60fc089..1f61efef5c62 100644 --- a/system/Test/Fabricator.php +++ b/system/Test/Fabricator.php @@ -15,13 +15,13 @@ use Closure; use CodeIgniter\Exceptions\FrameworkException; +use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\I18n\Time; use CodeIgniter\Model; use Config\App; use Faker\Factory; use Faker\Generator; use InvalidArgumentException; -use RuntimeException; /** * Fabricator diff --git a/system/Test/FilterTestTrait.php b/system/Test/FilterTestTrait.php index 93f273a39f1f..608e847413f3 100644 --- a/system/Test/FilterTestTrait.php +++ b/system/Test/FilterTestTrait.php @@ -14,6 +14,7 @@ namespace CodeIgniter\Test; use Closure; +use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\Filters\Exceptions\FilterException; use CodeIgniter\Filters\FilterInterface; use CodeIgniter\Filters\Filters; @@ -22,7 +23,6 @@ use CodeIgniter\Router\RouteCollection; use Config\Filters as FiltersConfig; use InvalidArgumentException; -use RuntimeException; /** * Filter Test Trait diff --git a/system/View/View.php b/system/View/View.php index cda3d64f6088..160f1c179278 100644 --- a/system/View/View.php +++ b/system/View/View.php @@ -15,12 +15,12 @@ use CodeIgniter\Autoloader\FileLocatorInterface; use CodeIgniter\Debug\Toolbar\Collectors\Views; +use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\Filters\DebugToolbar; use CodeIgniter\View\Exceptions\ViewException; use Config\Toolbar; use Config\View as ViewConfig; use Psr\Log\LoggerInterface; -use RuntimeException; /** * Class View From 049fb2be4d18fe835a6e097fb435b2714ca84fa6 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 27 Jan 2024 20:32:01 +0900 Subject: [PATCH 015/277] refactor: use CodeIgniter\Exceptions\InvalidArgumentException --- system/Autoloader/Autoloader.php | 2 +- system/BaseModel.php | 2 +- system/CLI/CLI.php | 2 +- system/Cache/Handlers/BaseHandler.php | 2 +- system/Commands/Database/ShowTableInfo.php | 2 +- system/Config/DotEnv.php | 2 +- system/Config/Factories.php | 2 +- system/Cookie/Cookie.php | 2 +- system/Database/BaseBuilder.php | 2 +- system/Database/Config.php | 2 +- system/Database/Database.php | 2 +- system/Database/Forge.php | 2 +- system/Database/Postgre/Builder.php | 2 +- system/Database/SQLite3/Builder.php | 2 +- system/Database/Seeder.php | 2 +- system/Files/FileCollection.php | 2 +- system/HTTP/CURLRequest.php | 2 +- system/HTTP/Exceptions/RedirectException.php | 2 +- system/HTTP/Files/UploadedFile.php | 2 +- system/HTTP/Files/UploadedFileInterface.php | 2 +- system/HTTP/IncomingRequest.php | 2 +- system/HTTP/Message.php | 2 +- system/HTTP/MessageTrait.php | 2 +- system/HTTP/OutgoingRequestInterface.php | 2 +- system/HTTP/ResponseInterface.php | 2 +- system/HTTP/ResponseTrait.php | 2 +- system/HTTP/URI.php | 2 +- system/Helpers/Array/ArrayHelper.php | 2 +- system/Images/Handlers/BaseHandler.php | 2 +- system/Router/RouteCollection.php | 2 +- system/Security/Security.php | 2 +- system/Test/ControllerTestTrait.php | 2 +- system/Test/DOMParser.php | 2 +- system/Test/FilterTestTrait.php | 2 +- system/Test/Mock/MockInputOutput.php | 2 +- system/Validation/FileRules.php | 2 +- system/Validation/Rules.php | 2 +- system/Validation/Validation.php | 2 +- 38 files changed, 38 insertions(+), 38 deletions(-) diff --git a/system/Autoloader/Autoloader.php b/system/Autoloader/Autoloader.php index 3aef07ae635f..567d590fdae3 100644 --- a/system/Autoloader/Autoloader.php +++ b/system/Autoloader/Autoloader.php @@ -14,6 +14,7 @@ namespace CodeIgniter\Autoloader; use CodeIgniter\Exceptions\ConfigException; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Exceptions\RuntimeException; use Composer\Autoload\ClassLoader; use Composer\InstalledVersions; @@ -21,7 +22,6 @@ use Config\Kint as KintConfig; use Config\Modules; use Config\Services; -use InvalidArgumentException; use Kint; use Kint\Renderer\CliRenderer; use Kint\Renderer\RichRenderer; diff --git a/system/BaseModel.php b/system/BaseModel.php index 9b8bb70ed482..eee042f6c38f 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -21,13 +21,13 @@ use CodeIgniter\Database\Query; use CodeIgniter\DataConverter\DataConverter; use CodeIgniter\Entity\Entity; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Exceptions\ModelException; use CodeIgniter\I18n\Time; use CodeIgniter\Pager\Pager; use CodeIgniter\Validation\ValidationInterface; use Config\Feature; use Config\Services; -use InvalidArgumentException; use ReflectionClass; use ReflectionException; use ReflectionProperty; diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index 865241b75cbf..dc9c9a971bb7 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -14,8 +14,8 @@ namespace CodeIgniter\CLI; use CodeIgniter\CLI\Exceptions\CLIException; +use CodeIgniter\Exceptions\InvalidArgumentException; use Config\Services; -use InvalidArgumentException; use Throwable; /** diff --git a/system/Cache/Handlers/BaseHandler.php b/system/Cache/Handlers/BaseHandler.php index 43d316f87b0d..b2c9778b4997 100644 --- a/system/Cache/Handlers/BaseHandler.php +++ b/system/Cache/Handlers/BaseHandler.php @@ -15,9 +15,9 @@ use Closure; use CodeIgniter\Cache\CacheInterface; +use CodeIgniter\Exceptions\InvalidArgumentException; use Config\Cache; use Exception; -use InvalidArgumentException; /** * Base class for cache handling diff --git a/system/Commands/Database/ShowTableInfo.php b/system/Commands/Database/ShowTableInfo.php index 05dcfe4d67a3..3e88e035980c 100644 --- a/system/Commands/Database/ShowTableInfo.php +++ b/system/Commands/Database/ShowTableInfo.php @@ -16,8 +16,8 @@ use CodeIgniter\CLI\BaseCommand; use CodeIgniter\CLI\CLI; use CodeIgniter\Database\BaseConnection; +use CodeIgniter\Exceptions\InvalidArgumentException; use Config\Database; -use InvalidArgumentException; /** * Get table data if it exists in the database. diff --git a/system/Config/DotEnv.php b/system/Config/DotEnv.php index db7152fd09d5..eb3e6c5e3c9d 100644 --- a/system/Config/DotEnv.php +++ b/system/Config/DotEnv.php @@ -13,7 +13,7 @@ namespace CodeIgniter\Config; -use InvalidArgumentException; +use CodeIgniter\Exceptions\InvalidArgumentException; /** * Environment-specific configuration diff --git a/system/Config/Factories.php b/system/Config/Factories.php index d98664a24a7b..dfd1c38b4f9a 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -14,8 +14,8 @@ namespace CodeIgniter\Config; use CodeIgniter\Database\ConnectionInterface; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Model; -use InvalidArgumentException; /** * Factories for creating instances. diff --git a/system/Cookie/Cookie.php b/system/Cookie/Cookie.php index 78dc44258eb9..48e1ad7c2fbe 100644 --- a/system/Cookie/Cookie.php +++ b/system/Cookie/Cookie.php @@ -15,10 +15,10 @@ use ArrayAccess; use CodeIgniter\Cookie\Exceptions\CookieException; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\I18n\Time; use Config\Cookie as CookieConfig; use DateTimeInterface; -use InvalidArgumentException; use LogicException; use ReturnTypeWillChange; diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index f9ca1efd4144..79cffb9c5171 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -16,9 +16,9 @@ use Closure; use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Database\Exceptions\DataException; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Traits\ConditionalTrait; use Config\Feature; -use InvalidArgumentException; /** * Class BaseBuilder diff --git a/system/Database/Config.php b/system/Database/Config.php index 03a0dd15743b..00d1b192cae3 100644 --- a/system/Database/Config.php +++ b/system/Database/Config.php @@ -14,8 +14,8 @@ namespace CodeIgniter\Database; use CodeIgniter\Config\BaseConfig; +use CodeIgniter\Exceptions\InvalidArgumentException; use Config\Database as DbConfig; -use InvalidArgumentException; /** * Class Config diff --git a/system/Database/Database.php b/system/Database/Database.php index c2bc66d30d11..649d347ff045 100644 --- a/system/Database/Database.php +++ b/system/Database/Database.php @@ -13,7 +13,7 @@ namespace CodeIgniter\Database; -use InvalidArgumentException; +use CodeIgniter\Exceptions\InvalidArgumentException; /** * Database Connection Factory diff --git a/system/Database/Forge.php b/system/Database/Forge.php index ca0428d9f7b3..1aa313358885 100644 --- a/system/Database/Forge.php +++ b/system/Database/Forge.php @@ -14,8 +14,8 @@ namespace CodeIgniter\Database; use CodeIgniter\Database\Exceptions\DatabaseException; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Exceptions\RuntimeException; -use InvalidArgumentException; use Throwable; /** diff --git a/system/Database/Postgre/Builder.php b/system/Database/Postgre/Builder.php index 857760930275..10c87be53dd1 100644 --- a/system/Database/Postgre/Builder.php +++ b/system/Database/Postgre/Builder.php @@ -16,7 +16,7 @@ use CodeIgniter\Database\BaseBuilder; use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Database\RawSql; -use InvalidArgumentException; +use CodeIgniter\Exceptions\InvalidArgumentException; /** * Builder for Postgre diff --git a/system/Database/SQLite3/Builder.php b/system/Database/SQLite3/Builder.php index a59270bbc0de..87bc4d73a885 100644 --- a/system/Database/SQLite3/Builder.php +++ b/system/Database/SQLite3/Builder.php @@ -16,7 +16,7 @@ use CodeIgniter\Database\BaseBuilder; use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Database\RawSql; -use InvalidArgumentException; +use CodeIgniter\Exceptions\InvalidArgumentException; /** * Builder for SQLite3 diff --git a/system/Database/Seeder.php b/system/Database/Seeder.php index be7cfcdf8a8c..ee6f6d1081b1 100644 --- a/system/Database/Seeder.php +++ b/system/Database/Seeder.php @@ -14,10 +14,10 @@ namespace CodeIgniter\Database; use CodeIgniter\CLI\CLI; +use CodeIgniter\Exceptions\InvalidArgumentException; use Config\Database; use Faker\Factory; use Faker\Generator; -use InvalidArgumentException; /** * Class Seeder diff --git a/system/Files/FileCollection.php b/system/Files/FileCollection.php index b9456dcc15ba..8b13efa2a570 100644 --- a/system/Files/FileCollection.php +++ b/system/Files/FileCollection.php @@ -13,11 +13,11 @@ namespace CodeIgniter\Files; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Files\Exceptions\FileException; use CodeIgniter\Files\Exceptions\FileNotFoundException; use Countable; use Generator; -use InvalidArgumentException; use IteratorAggregate; /** diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php index 4b1c9c625399..0bb5ad3ea6b1 100644 --- a/system/HTTP/CURLRequest.php +++ b/system/HTTP/CURLRequest.php @@ -13,10 +13,10 @@ namespace CodeIgniter\HTTP; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\HTTP\Exceptions\HTTPException; use Config\App; use Config\CURLRequest as ConfigCURLRequest; -use InvalidArgumentException; /** * A lightweight HTTP client for sending synchronous HTTP requests via cURL. diff --git a/system/HTTP/Exceptions/RedirectException.php b/system/HTTP/Exceptions/RedirectException.php index 318ffed42227..8d416fa4a5ee 100644 --- a/system/HTTP/Exceptions/RedirectException.php +++ b/system/HTTP/Exceptions/RedirectException.php @@ -14,10 +14,10 @@ namespace CodeIgniter\HTTP\Exceptions; use CodeIgniter\Exceptions\HTTPExceptionInterface; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\HTTP\ResponsableInterface; use CodeIgniter\HTTP\ResponseInterface; -use InvalidArgumentException; use LogicException; use Throwable; diff --git a/system/HTTP/Files/UploadedFile.php b/system/HTTP/Files/UploadedFile.php index da029d7739ca..fe19f0df0527 100644 --- a/system/HTTP/Files/UploadedFile.php +++ b/system/HTTP/Files/UploadedFile.php @@ -13,12 +13,12 @@ namespace CodeIgniter\HTTP\Files; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\Files\File; use CodeIgniter\HTTP\Exceptions\HTTPException; use Config\Mimes; use Exception; -use InvalidArgumentException; /** * Value object representing a single file uploaded through an diff --git a/system/HTTP/Files/UploadedFileInterface.php b/system/HTTP/Files/UploadedFileInterface.php index 167b0aa2f4d6..6e849d04a76b 100644 --- a/system/HTTP/Files/UploadedFileInterface.php +++ b/system/HTTP/Files/UploadedFileInterface.php @@ -13,8 +13,8 @@ namespace CodeIgniter\HTTP\Files; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Exceptions\RuntimeException; -use InvalidArgumentException; /** * Value object representing a single file uploaded through an diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index ec13e40f6ff5..b5cec37232a9 100755 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -13,12 +13,12 @@ namespace CodeIgniter\HTTP; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\HTTP\Exceptions\HTTPException; use CodeIgniter\HTTP\Files\FileCollection; use CodeIgniter\HTTP\Files\UploadedFile; use Config\App; use Config\Services; -use InvalidArgumentException; use Locale; use stdClass; diff --git a/system/HTTP/Message.php b/system/HTTP/Message.php index 71c4429f28ee..d83fcbb83230 100644 --- a/system/HTTP/Message.php +++ b/system/HTTP/Message.php @@ -13,7 +13,7 @@ namespace CodeIgniter\HTTP; -use InvalidArgumentException; +use CodeIgniter\Exceptions\InvalidArgumentException; /** * An HTTP message diff --git a/system/HTTP/MessageTrait.php b/system/HTTP/MessageTrait.php index 2f6e57a90c57..fc4af710c147 100644 --- a/system/HTTP/MessageTrait.php +++ b/system/HTTP/MessageTrait.php @@ -13,8 +13,8 @@ namespace CodeIgniter\HTTP; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\HTTP\Exceptions\HTTPException; -use InvalidArgumentException; /** * Message Trait diff --git a/system/HTTP/OutgoingRequestInterface.php b/system/HTTP/OutgoingRequestInterface.php index da9e36206c37..8e314a5be03e 100644 --- a/system/HTTP/OutgoingRequestInterface.php +++ b/system/HTTP/OutgoingRequestInterface.php @@ -13,7 +13,7 @@ namespace CodeIgniter\HTTP; -use InvalidArgumentException; +use CodeIgniter\Exceptions\InvalidArgumentException; /** * Representation of an outgoing, client-side request. diff --git a/system/HTTP/ResponseInterface.php b/system/HTTP/ResponseInterface.php index 68ec5c3fce7f..0f23f1354953 100644 --- a/system/HTTP/ResponseInterface.php +++ b/system/HTTP/ResponseInterface.php @@ -15,10 +15,10 @@ use CodeIgniter\Cookie\Cookie; use CodeIgniter\Cookie\CookieStore; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\HTTP\Exceptions\HTTPException; use CodeIgniter\Pager\PagerInterface; use DateTime; -use InvalidArgumentException; /** * Representation of an outgoing, server-side response. diff --git a/system/HTTP/ResponseTrait.php b/system/HTTP/ResponseTrait.php index 45f07d186170..726e85bae5a4 100644 --- a/system/HTTP/ResponseTrait.php +++ b/system/HTTP/ResponseTrait.php @@ -16,6 +16,7 @@ use CodeIgniter\Cookie\Cookie; use CodeIgniter\Cookie\CookieStore; use CodeIgniter\Cookie\Exceptions\CookieException; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\HTTP\Exceptions\HTTPException; use CodeIgniter\I18n\Time; use CodeIgniter\Pager\PagerInterface; @@ -23,7 +24,6 @@ use Config\Cookie as CookieConfig; use DateTime; use DateTimeZone; -use InvalidArgumentException; /** * Response Trait diff --git a/system/HTTP/URI.php b/system/HTTP/URI.php index efa99cf778a8..75c677345482 100644 --- a/system/HTTP/URI.php +++ b/system/HTTP/URI.php @@ -14,9 +14,9 @@ namespace CodeIgniter\HTTP; use BadMethodCallException; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\HTTP\Exceptions\HTTPException; use Config\App; -use InvalidArgumentException; use Stringable; /** diff --git a/system/Helpers/Array/ArrayHelper.php b/system/Helpers/Array/ArrayHelper.php index 76b8e8aedfbe..b9c356d381c1 100644 --- a/system/Helpers/Array/ArrayHelper.php +++ b/system/Helpers/Array/ArrayHelper.php @@ -13,7 +13,7 @@ namespace CodeIgniter\Helpers\Array; -use InvalidArgumentException; +use CodeIgniter\Exceptions\InvalidArgumentException; /** * @interal This is internal implementation for the framework. diff --git a/system/Images/Handlers/BaseHandler.php b/system/Images/Handlers/BaseHandler.php index eb704b564670..a6607e04d160 100644 --- a/system/Images/Handlers/BaseHandler.php +++ b/system/Images/Handlers/BaseHandler.php @@ -13,11 +13,11 @@ namespace CodeIgniter\Images\Handlers; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Images\Exceptions\ImageException; use CodeIgniter\Images\Image; use CodeIgniter\Images\ImageHandlerInterface; use Config\Images; -use InvalidArgumentException; /** * Base image handling implementation diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 3198d342aa9d..faf2af99b046 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -15,6 +15,7 @@ use Closure; use CodeIgniter\Autoloader\FileLocatorInterface; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\HTTP\Method; use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\Router\Exceptions\RouterException; @@ -22,7 +23,6 @@ use Config\Modules; use Config\Routing; use Config\Services; -use InvalidArgumentException; /** * @todo Implement nested resource routing (See CakePHP) diff --git a/system/Security/Security.php b/system/Security/Security.php index 077b3b9cdbb0..54e3839845a8 100644 --- a/system/Security/Security.php +++ b/system/Security/Security.php @@ -14,6 +14,7 @@ namespace CodeIgniter\Security; use CodeIgniter\Cookie\Cookie; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\Method; use CodeIgniter\HTTP\Request; @@ -24,7 +25,6 @@ use Config\Cookie as CookieConfig; use Config\Security as SecurityConfig; use ErrorException; -use InvalidArgumentException; use LogicException; /** diff --git a/system/Test/ControllerTestTrait.php b/system/Test/ControllerTestTrait.php index ab1a1aef3907..7417e7307a08 100644 --- a/system/Test/ControllerTestTrait.php +++ b/system/Test/ControllerTestTrait.php @@ -12,13 +12,13 @@ namespace CodeIgniter\Test; use CodeIgniter\Controller; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\HTTP\Exceptions\HTTPException; use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\HTTP\URI; use Config\App; use Config\Services; -use InvalidArgumentException; use Psr\Log\LoggerInterface; use Throwable; diff --git a/system/Test/DOMParser.php b/system/Test/DOMParser.php index fe9ee1d58b2a..ae4d4e4ee2b4 100644 --- a/system/Test/DOMParser.php +++ b/system/Test/DOMParser.php @@ -14,10 +14,10 @@ namespace CodeIgniter\Test; use BadMethodCallException; +use CodeIgniter\Exceptions\InvalidArgumentException; use DOMDocument; use DOMNodeList; use DOMXPath; -use InvalidArgumentException; /** * Load a response into a DOMDocument for testing assertions based on that diff --git a/system/Test/FilterTestTrait.php b/system/Test/FilterTestTrait.php index 608e847413f3..6c0a5d4670c0 100644 --- a/system/Test/FilterTestTrait.php +++ b/system/Test/FilterTestTrait.php @@ -14,6 +14,7 @@ namespace CodeIgniter\Test; use Closure; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\Filters\Exceptions\FilterException; use CodeIgniter\Filters\FilterInterface; @@ -22,7 +23,6 @@ use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\Router\RouteCollection; use Config\Filters as FiltersConfig; -use InvalidArgumentException; /** * Filter Test Trait diff --git a/system/Test/Mock/MockInputOutput.php b/system/Test/Mock/MockInputOutput.php index 750f142a00e7..ff7044e07ba5 100644 --- a/system/Test/Mock/MockInputOutput.php +++ b/system/Test/Mock/MockInputOutput.php @@ -14,9 +14,9 @@ namespace CodeIgniter\Test\Mock; use CodeIgniter\CLI\InputOutput; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Test\Filters\CITestStreamFilter; use CodeIgniter\Test\PhpStreamWrapper; -use InvalidArgumentException; use LogicException; final class MockInputOutput extends InputOutput diff --git a/system/Validation/FileRules.php b/system/Validation/FileRules.php index dd8c4290e11d..2171858c1582 100644 --- a/system/Validation/FileRules.php +++ b/system/Validation/FileRules.php @@ -13,11 +13,11 @@ namespace CodeIgniter\Validation; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\HTTP\CLIRequest; use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\RequestInterface; use Config\Mimes; -use InvalidArgumentException; /** * File validation rules diff --git a/system/Validation/Rules.php b/system/Validation/Rules.php index 272b2dcf61fe..aa031e216271 100644 --- a/system/Validation/Rules.php +++ b/system/Validation/Rules.php @@ -13,9 +13,9 @@ namespace CodeIgniter\Validation; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Helpers\Array\ArrayHelper; use Config\Database; -use InvalidArgumentException; /** * Validation Rules. diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php index eb87ef3fa776..82fc749f9cf3 100644 --- a/system/Validation/Validation.php +++ b/system/Validation/Validation.php @@ -15,6 +15,7 @@ use Closure; use CodeIgniter\Database\BaseConnection; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\HTTP\Exceptions\HTTPException; use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\Method; @@ -23,7 +24,6 @@ use CodeIgniter\View\RendererInterface; use Config\Services; use Config\Validation as ValidationConfig; -use InvalidArgumentException; use LogicException; use TypeError; From 4b8a50e6939d503e01147c8d612b9ad5572425f8 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 27 Jan 2024 20:32:26 +0900 Subject: [PATCH 016/277] refactor: use CodeIgniter\Exceptions\LogicException --- system/CodeIgniter.php | 2 +- system/Cookie/Cookie.php | 2 +- system/Database/MySQLi/Connection.php | 2 +- system/HTTP/Exceptions/RedirectException.php | 2 +- system/Security/Security.php | 2 +- system/Test/ConfigFromArrayTrait.php | 2 +- system/Test/Mock/MockInputOutput.php | 2 +- system/Validation/Validation.php | 2 +- system/View/Cells/Cell.php | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 1758228076ab..1ea1d0e2f610 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -16,6 +16,7 @@ use CodeIgniter\Debug\Timer; use CodeIgniter\Events\Events; use CodeIgniter\Exceptions\FrameworkException; +use CodeIgniter\Exceptions\LogicException; use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Filters\Filters; use CodeIgniter\HTTP\CLIRequest; @@ -41,7 +42,6 @@ use Kint\Renderer\CliRenderer; use Kint\Renderer\RichRenderer; use Locale; -use LogicException; use Throwable; /** diff --git a/system/Cookie/Cookie.php b/system/Cookie/Cookie.php index 48e1ad7c2fbe..7da90654ed9b 100644 --- a/system/Cookie/Cookie.php +++ b/system/Cookie/Cookie.php @@ -16,10 +16,10 @@ use ArrayAccess; use CodeIgniter\Cookie\Exceptions\CookieException; use CodeIgniter\Exceptions\InvalidArgumentException; +use CodeIgniter\Exceptions\LogicException; use CodeIgniter\I18n\Time; use Config\Cookie as CookieConfig; use DateTimeInterface; -use LogicException; use ReturnTypeWillChange; /** diff --git a/system/Database/MySQLi/Connection.php b/system/Database/MySQLi/Connection.php index b25f2e1af390..e8db25c76640 100644 --- a/system/Database/MySQLi/Connection.php +++ b/system/Database/MySQLi/Connection.php @@ -15,7 +15,7 @@ use CodeIgniter\Database\BaseConnection; use CodeIgniter\Database\Exceptions\DatabaseException; -use LogicException; +use CodeIgniter\Exceptions\LogicException; use mysqli; use mysqli_result; use mysqli_sql_exception; diff --git a/system/HTTP/Exceptions/RedirectException.php b/system/HTTP/Exceptions/RedirectException.php index 8d416fa4a5ee..a13bc933a095 100644 --- a/system/HTTP/Exceptions/RedirectException.php +++ b/system/HTTP/Exceptions/RedirectException.php @@ -15,10 +15,10 @@ use CodeIgniter\Exceptions\HTTPExceptionInterface; use CodeIgniter\Exceptions\InvalidArgumentException; +use CodeIgniter\Exceptions\LogicException; use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\HTTP\ResponsableInterface; use CodeIgniter\HTTP\ResponseInterface; -use LogicException; use Throwable; /** diff --git a/system/Security/Security.php b/system/Security/Security.php index 54e3839845a8..03e8b8da9e2c 100644 --- a/system/Security/Security.php +++ b/system/Security/Security.php @@ -15,6 +15,7 @@ use CodeIgniter\Cookie\Cookie; use CodeIgniter\Exceptions\InvalidArgumentException; +use CodeIgniter\Exceptions\LogicException; use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\Method; use CodeIgniter\HTTP\Request; @@ -25,7 +26,6 @@ use Config\Cookie as CookieConfig; use Config\Security as SecurityConfig; use ErrorException; -use LogicException; /** * Class Security diff --git a/system/Test/ConfigFromArrayTrait.php b/system/Test/ConfigFromArrayTrait.php index 3652fa1074a1..9dd2ac0c4de7 100644 --- a/system/Test/ConfigFromArrayTrait.php +++ b/system/Test/ConfigFromArrayTrait.php @@ -13,7 +13,7 @@ namespace CodeIgniter\Test; -use LogicException; +use CodeIgniter\Exceptions\LogicException; trait ConfigFromArrayTrait { diff --git a/system/Test/Mock/MockInputOutput.php b/system/Test/Mock/MockInputOutput.php index ff7044e07ba5..fb76781a177e 100644 --- a/system/Test/Mock/MockInputOutput.php +++ b/system/Test/Mock/MockInputOutput.php @@ -15,9 +15,9 @@ use CodeIgniter\CLI\InputOutput; use CodeIgniter\Exceptions\InvalidArgumentException; +use CodeIgniter\Exceptions\LogicException; use CodeIgniter\Test\Filters\CITestStreamFilter; use CodeIgniter\Test\PhpStreamWrapper; -use LogicException; final class MockInputOutput extends InputOutput { diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php index 82fc749f9cf3..d01262640709 100644 --- a/system/Validation/Validation.php +++ b/system/Validation/Validation.php @@ -16,6 +16,7 @@ use Closure; use CodeIgniter\Database\BaseConnection; use CodeIgniter\Exceptions\InvalidArgumentException; +use CodeIgniter\Exceptions\LogicException; use CodeIgniter\HTTP\Exceptions\HTTPException; use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\Method; @@ -24,7 +25,6 @@ use CodeIgniter\View\RendererInterface; use Config\Services; use Config\Validation as ValidationConfig; -use LogicException; use TypeError; /** diff --git a/system/View/Cells/Cell.php b/system/View/Cells/Cell.php index ddf7ef4ccf63..e60493804002 100644 --- a/system/View/Cells/Cell.php +++ b/system/View/Cells/Cell.php @@ -13,8 +13,8 @@ namespace CodeIgniter\View\Cells; +use CodeIgniter\Exceptions\LogicException; use CodeIgniter\Traits\PropertiesTrait; -use LogicException; use ReflectionClass; use Stringable; From 13106b84c19e129b551893e296527be7954e32bc Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 27 Jan 2024 20:34:45 +0900 Subject: [PATCH 017/277] refactor: use CodeIgniter\Exceptions\BadMethodCallException --- system/Database/BasePreparedQuery.php | 2 +- system/Database/MySQLi/PreparedQuery.php | 2 +- system/Database/OCI8/PreparedQuery.php | 2 +- system/Database/Postgre/PreparedQuery.php | 2 +- system/Database/PreparedQueryInterface.php | 2 +- system/Database/SQLSRV/PreparedQuery.php | 2 +- system/Database/SQLite3/PreparedQuery.php | 2 +- system/HTTP/SiteURI.php | 2 +- system/HTTP/URI.php | 2 +- system/Model.php | 2 +- system/Test/DOMParser.php | 2 +- system/Test/Mock/MockTable.php | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/system/Database/BasePreparedQuery.php b/system/Database/BasePreparedQuery.php index 8c4f252cfb7e..95e7c0df0c47 100644 --- a/system/Database/BasePreparedQuery.php +++ b/system/Database/BasePreparedQuery.php @@ -14,9 +14,9 @@ namespace CodeIgniter\Database; use ArgumentCountError; -use BadMethodCallException; use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Events\Events; +use CodeIgniter\Exceptions\BadMethodCallException; use ErrorException; /** diff --git a/system/Database/MySQLi/PreparedQuery.php b/system/Database/MySQLi/PreparedQuery.php index e9a6c6d10e30..2ff2f6638259 100644 --- a/system/Database/MySQLi/PreparedQuery.php +++ b/system/Database/MySQLi/PreparedQuery.php @@ -13,9 +13,9 @@ namespace CodeIgniter\Database\MySQLi; -use BadMethodCallException; use CodeIgniter\Database\BasePreparedQuery; use CodeIgniter\Database\Exceptions\DatabaseException; +use CodeIgniter\Exceptions\BadMethodCallException; use mysqli; use mysqli_result; use mysqli_sql_exception; diff --git a/system/Database/OCI8/PreparedQuery.php b/system/Database/OCI8/PreparedQuery.php index c267f8061377..a6b6c73c0348 100644 --- a/system/Database/OCI8/PreparedQuery.php +++ b/system/Database/OCI8/PreparedQuery.php @@ -13,9 +13,9 @@ namespace CodeIgniter\Database\OCI8; -use BadMethodCallException; use CodeIgniter\Database\BasePreparedQuery; use CodeIgniter\Database\Exceptions\DatabaseException; +use CodeIgniter\Exceptions\BadMethodCallException; /** * Prepared query for OCI8 diff --git a/system/Database/Postgre/PreparedQuery.php b/system/Database/Postgre/PreparedQuery.php index 832108675ab9..06e7ffbb1f4e 100644 --- a/system/Database/Postgre/PreparedQuery.php +++ b/system/Database/Postgre/PreparedQuery.php @@ -13,9 +13,9 @@ namespace CodeIgniter\Database\Postgre; -use BadMethodCallException; use CodeIgniter\Database\BasePreparedQuery; use CodeIgniter\Database\Exceptions\DatabaseException; +use CodeIgniter\Exceptions\BadMethodCallException; use Exception; use PgSql\Connection as PgSqlConnection; use PgSql\Result as PgSqlResult; diff --git a/system/Database/PreparedQueryInterface.php b/system/Database/PreparedQueryInterface.php index d9e11cd520d6..6ac69904e7f9 100644 --- a/system/Database/PreparedQueryInterface.php +++ b/system/Database/PreparedQueryInterface.php @@ -13,7 +13,7 @@ namespace CodeIgniter\Database; -use BadMethodCallException; +use CodeIgniter\Exceptions\BadMethodCallException; /** * @template TConnection diff --git a/system/Database/SQLSRV/PreparedQuery.php b/system/Database/SQLSRV/PreparedQuery.php index f3a4a14c2bc6..1f9dad6217e8 100755 --- a/system/Database/SQLSRV/PreparedQuery.php +++ b/system/Database/SQLSRV/PreparedQuery.php @@ -13,9 +13,9 @@ namespace CodeIgniter\Database\SQLSRV; -use BadMethodCallException; use CodeIgniter\Database\BasePreparedQuery; use CodeIgniter\Database\Exceptions\DatabaseException; +use CodeIgniter\Exceptions\BadMethodCallException; /** * Prepared query for Postgre diff --git a/system/Database/SQLite3/PreparedQuery.php b/system/Database/SQLite3/PreparedQuery.php index 21dc4c2fdeff..654afa1ce36e 100644 --- a/system/Database/SQLite3/PreparedQuery.php +++ b/system/Database/SQLite3/PreparedQuery.php @@ -13,9 +13,9 @@ namespace CodeIgniter\Database\SQLite3; -use BadMethodCallException; use CodeIgniter\Database\BasePreparedQuery; use CodeIgniter\Database\Exceptions\DatabaseException; +use CodeIgniter\Exceptions\BadMethodCallException; use Exception; use SQLite3; use SQLite3Result; diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index def61b65e17b..20bec2972122 100644 --- a/system/HTTP/SiteURI.php +++ b/system/HTTP/SiteURI.php @@ -13,7 +13,7 @@ namespace CodeIgniter\HTTP; -use BadMethodCallException; +use CodeIgniter\Exceptions\BadMethodCallException; use CodeIgniter\Exceptions\ConfigException; use CodeIgniter\HTTP\Exceptions\HTTPException; use Config\App; diff --git a/system/HTTP/URI.php b/system/HTTP/URI.php index 75c677345482..e3062d30dd7c 100644 --- a/system/HTTP/URI.php +++ b/system/HTTP/URI.php @@ -13,7 +13,7 @@ namespace CodeIgniter\HTTP; -use BadMethodCallException; +use CodeIgniter\Exceptions\BadMethodCallException; use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\HTTP\Exceptions\HTTPException; use Config\App; diff --git a/system/Model.php b/system/Model.php index b3ecfc653943..ee165c68371c 100644 --- a/system/Model.php +++ b/system/Model.php @@ -13,7 +13,6 @@ namespace CodeIgniter; -use BadMethodCallException; use Closure; use CodeIgniter\Database\BaseBuilder; use CodeIgniter\Database\BaseConnection; @@ -23,6 +22,7 @@ use CodeIgniter\Database\Exceptions\DataException; use CodeIgniter\Database\Query; use CodeIgniter\Entity\Entity; +use CodeIgniter\Exceptions\BadMethodCallException; use CodeIgniter\Exceptions\ModelException; use CodeIgniter\Validation\ValidationInterface; use Config\Database; diff --git a/system/Test/DOMParser.php b/system/Test/DOMParser.php index ae4d4e4ee2b4..f46ce1d65206 100644 --- a/system/Test/DOMParser.php +++ b/system/Test/DOMParser.php @@ -13,7 +13,7 @@ namespace CodeIgniter\Test; -use BadMethodCallException; +use CodeIgniter\Exceptions\BadMethodCallException; use CodeIgniter\Exceptions\InvalidArgumentException; use DOMDocument; use DOMNodeList; diff --git a/system/Test/Mock/MockTable.php b/system/Test/Mock/MockTable.php index 1976aaed7be1..a7edc359e4f8 100644 --- a/system/Test/Mock/MockTable.php +++ b/system/Test/Mock/MockTable.php @@ -13,7 +13,7 @@ namespace CodeIgniter\Test\Mock; -use BadMethodCallException; +use CodeIgniter\Exceptions\BadMethodCallException; use CodeIgniter\View\Table; class MockTable extends Table From 82eaa32d65a094cbeed0b72e41117e1da025d109 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 27 Jan 2024 21:16:27 +0900 Subject: [PATCH 018/277] refactor: use CodeIgniter\Exceptions\BadFunctionCallException --- system/Helpers/number_helper.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/system/Helpers/number_helper.php b/system/Helpers/number_helper.php index 96468ddcdea3..a0d0496452c3 100644 --- a/system/Helpers/number_helper.php +++ b/system/Helpers/number_helper.php @@ -11,6 +11,8 @@ * the LICENSE file that was distributed with this source code. */ +use CodeIgniter\Exceptions\BadFunctionCallException; + // CodeIgniter Number Helpers if (! function_exists('number_to_size')) { From 3fa8a261779fdac2f9cabf5bdee8265510dfa74e Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 27 Jan 2024 22:41:02 +0900 Subject: [PATCH 019/277] fix: replace \Exception with RuntimeException --- system/Cache/ResponseCache.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/Cache/ResponseCache.php b/system/Cache/ResponseCache.php index 388931813c7c..35c2617c8cb5 100644 --- a/system/Cache/ResponseCache.php +++ b/system/Cache/ResponseCache.php @@ -13,12 +13,12 @@ namespace CodeIgniter\Cache; +use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\HTTP\CLIRequest; use CodeIgniter\HTTP\Header; use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\ResponseInterface; use Config\Cache as CacheConfig; -use Exception; /** * Web Page Caching @@ -131,7 +131,7 @@ public function get($request, ResponseInterface $response): ?ResponseInterface || ! isset($cachedResponse['output']) || ! isset($cachedResponse['headers']) ) { - throw new Exception('Error unserializing page cache'); + throw new RuntimeException('Error unserializing page cache'); } $headers = $cachedResponse['headers']; From 3a35769b197bf8104a252c30afbd3149593dcc56 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 27 Jan 2024 22:42:41 +0900 Subject: [PATCH 020/277] fix: replace \Exception with BadMethodCallException --- system/Cache/Handlers/BaseHandler.php | 3 ++- system/Cache/Handlers/MemcachedHandler.php | 3 ++- system/Cache/Handlers/WincacheHandler.php | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/system/Cache/Handlers/BaseHandler.php b/system/Cache/Handlers/BaseHandler.php index b2c9778b4997..9c98bedc7693 100644 --- a/system/Cache/Handlers/BaseHandler.php +++ b/system/Cache/Handlers/BaseHandler.php @@ -15,6 +15,7 @@ use Closure; use CodeIgniter\Cache\CacheInterface; +use CodeIgniter\Exceptions\BadMethodCallException; use CodeIgniter\Exceptions\InvalidArgumentException; use Config\Cache; use Exception; @@ -108,6 +109,6 @@ public function remember(string $key, int $ttl, Closure $callback) */ public function deleteMatching(string $pattern) { - throw new Exception('The deleteMatching method is not implemented.'); + throw new BadMethodCallException('The deleteMatching method is not implemented.'); } } diff --git a/system/Cache/Handlers/MemcachedHandler.php b/system/Cache/Handlers/MemcachedHandler.php index e1048004077a..a0d487126cf4 100644 --- a/system/Cache/Handlers/MemcachedHandler.php +++ b/system/Cache/Handlers/MemcachedHandler.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Cache\Handlers; +use CodeIgniter\Exceptions\BadMethodCallException; use CodeIgniter\Exceptions\CriticalError; use CodeIgniter\I18n\Time; use Config\Cache; @@ -197,7 +198,7 @@ public function delete(string $key) */ public function deleteMatching(string $pattern) { - throw new Exception('The deleteMatching method is not implemented for Memcached. You must select File, Redis or Predis handlers to use it.'); + throw new BadMethodCallException('The deleteMatching method is not implemented for Memcached. You must select File, Redis or Predis handlers to use it.'); } /** diff --git a/system/Cache/Handlers/WincacheHandler.php b/system/Cache/Handlers/WincacheHandler.php index 0ddee50a7fde..d4d97701b135 100644 --- a/system/Cache/Handlers/WincacheHandler.php +++ b/system/Cache/Handlers/WincacheHandler.php @@ -13,9 +13,9 @@ namespace CodeIgniter\Cache\Handlers; +use CodeIgniter\Exceptions\BadMethodCallException; use CodeIgniter\I18n\Time; use Config\Cache; -use Exception; /** * Cache handler for WinCache from Microsoft & IIS. @@ -80,7 +80,7 @@ public function delete(string $key) */ public function deleteMatching(string $pattern) { - throw new Exception('The deleteMatching method is not implemented for Wincache. You must select File, Redis or Predis handlers to use it.'); + throw new BadMethodCallException('The deleteMatching method is not implemented for Wincache. You must select File, Redis or Predis handlers to use it.'); } /** From d33d3c7fd6eae429d5c44ca17b3f0123be0dae5c Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 27 Jan 2024 22:43:09 +0900 Subject: [PATCH 021/277] fix: replace \TypeError with InvalidArgumentException --- system/Validation/Validation.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php index d01262640709..3cdf2f2ee22d 100644 --- a/system/Validation/Validation.php +++ b/system/Validation/Validation.php @@ -25,7 +25,6 @@ use CodeIgniter\View\RendererInterface; use Config\Services; use Config\Validation as ValidationConfig; -use TypeError; /** * Validator @@ -540,12 +539,12 @@ public function withRequest(RequestInterface $request): ValidationInterface * * @return $this * - * @throws TypeError + * @throws InvalidArgumentException */ public function setRule(string $field, ?string $label, $rules, array $errors = []) { if (! is_array($rules) && ! is_string($rules)) { - throw new TypeError('$rules must be of type string|array'); + throw new InvalidArgumentException('$rules must be of type string|array'); } $ruleSet = [ From 08fea02f2c82732d2ed31085fc28b4220c8a9df0 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 28 Jan 2024 09:54:00 +0900 Subject: [PATCH 022/277] docs: fix class Doc comments --- system/Exceptions/ConfigException.php | 3 ++- system/Exceptions/TestException.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/system/Exceptions/ConfigException.php b/system/Exceptions/ConfigException.php index d8d27d094230..b30818124901 100644 --- a/system/Exceptions/ConfigException.php +++ b/system/Exceptions/ConfigException.php @@ -14,7 +14,8 @@ namespace CodeIgniter\Exceptions; /** - * Exception for automatic logging. + * Exception thrown if the value of the Config class is invalid or the type is + * incorrect. */ class ConfigException extends RuntimeException implements HasExitCodeInterface { diff --git a/system/Exceptions/TestException.php b/system/Exceptions/TestException.php index fd70709a6cb2..4487ae1e1815 100644 --- a/system/Exceptions/TestException.php +++ b/system/Exceptions/TestException.php @@ -14,7 +14,7 @@ namespace CodeIgniter\Exceptions; /** - * Exception for automatic logging. + * Exception thrown when there is an error with the test code. */ class TestException extends LogicException { From 616e0f5964409ec9820f6126aa04972c8efe5079 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 28 Jan 2024 10:15:25 +0900 Subject: [PATCH 023/277] test: update expectException() --- tests/system/Validation/ValidationTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/system/Validation/ValidationTest.php b/tests/system/Validation/ValidationTest.php index b5493a682c8e..64c42a088681 100644 --- a/tests/system/Validation/ValidationTest.php +++ b/tests/system/Validation/ValidationTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Validation; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\HTTP\Exceptions\HTTPException; use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\SiteURI; @@ -26,7 +27,6 @@ use PHPUnit\Framework\ExpectationFailedException; use Tests\Support\Validation\TestRules; use Throwable; -use TypeError; /** * @internal @@ -197,7 +197,7 @@ public function testSetRuleOverwritesRuleReverse(): void public function testSetRuleRulesFormat(bool $expected, $rules): void { if (! $expected) { - $this->expectException(TypeError::class); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('$rules must be of type string|array'); } From 832c77ff1aa46e86901faef779462b3bbb7dc414 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 28 Jan 2024 10:45:00 +0900 Subject: [PATCH 024/277] refactor: remove unneeded `implements ExceptionInterface` --- system/Cache/Exceptions/CacheException.php | 3 +-- system/Encryption/Exceptions/EncryptionException.php | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/system/Cache/Exceptions/CacheException.php b/system/Cache/Exceptions/CacheException.php index 774daa9965b2..650dad64d649 100644 --- a/system/Cache/Exceptions/CacheException.php +++ b/system/Cache/Exceptions/CacheException.php @@ -14,13 +14,12 @@ namespace CodeIgniter\Cache\Exceptions; use CodeIgniter\Exceptions\DebugTraceableTrait; -use CodeIgniter\Exceptions\ExceptionInterface; use CodeIgniter\Exceptions\RuntimeException; /** * CacheException */ -class CacheException extends RuntimeException implements ExceptionInterface +class CacheException extends RuntimeException { use DebugTraceableTrait; diff --git a/system/Encryption/Exceptions/EncryptionException.php b/system/Encryption/Exceptions/EncryptionException.php index e18899467bd5..9a0935d7d91d 100644 --- a/system/Encryption/Exceptions/EncryptionException.php +++ b/system/Encryption/Exceptions/EncryptionException.php @@ -14,13 +14,12 @@ namespace CodeIgniter\Encryption\Exceptions; use CodeIgniter\Exceptions\DebugTraceableTrait; -use CodeIgniter\Exceptions\ExceptionInterface; use CodeIgniter\Exceptions\RuntimeException; /** * Encryption exception */ -class EncryptionException extends RuntimeException implements ExceptionInterface +class EncryptionException extends RuntimeException { use DebugTraceableTrait; From b91b905a6e101a829137adb8c7593558ff56a7af Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 28 Jan 2024 10:45:36 +0900 Subject: [PATCH 025/277] feat: add domain-level ExceptionInterface If there are more than one Exception class in the domain, provide a domain-level interface for capture of all exceptions. --- .../Files/Exceptions/ExceptionInterface.php | 24 +++++++++++++++++++ system/HTTP/Exceptions/ExceptionInterface.php | 24 +++++++++++++++++++ .../Router/Exceptions/ExceptionInterface.php | 24 +++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 system/Files/Exceptions/ExceptionInterface.php create mode 100644 system/HTTP/Exceptions/ExceptionInterface.php create mode 100644 system/Router/Exceptions/ExceptionInterface.php diff --git a/system/Files/Exceptions/ExceptionInterface.php b/system/Files/Exceptions/ExceptionInterface.php new file mode 100644 index 000000000000..346ff1624e12 --- /dev/null +++ b/system/Files/Exceptions/ExceptionInterface.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Files\Exceptions; + +/** + * Provides a domain-level interface for broad capture + * of all Files-related exceptions. + * + * catch (\CodeIgniter\Files\Exceptions\ExceptionInterface) { ... } + */ +interface ExceptionInterface extends \CodeIgniter\Exceptions\ExceptionInterface +{ +} diff --git a/system/HTTP/Exceptions/ExceptionInterface.php b/system/HTTP/Exceptions/ExceptionInterface.php new file mode 100644 index 000000000000..984a94972364 --- /dev/null +++ b/system/HTTP/Exceptions/ExceptionInterface.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\HTTP\Exceptions; + +/** + * Provides a domain-level interface for broad capture + * of all HTTP-related exceptions. + * + * catch (\CodeIgniter\HTTP\Exceptions\ExceptionInterface) { ... } + */ +interface ExceptionInterface extends \CodeIgniter\Exceptions\ExceptionInterface +{ +} diff --git a/system/Router/Exceptions/ExceptionInterface.php b/system/Router/Exceptions/ExceptionInterface.php new file mode 100644 index 000000000000..49ed7cc11a78 --- /dev/null +++ b/system/Router/Exceptions/ExceptionInterface.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Router\Exceptions; + +/** + * Provides a domain-level interface for broad capture + * of all Router-related exceptions. + * + * catch (\CodeIgniter\Router\Exceptions\ExceptionInterface) { ... } + */ +interface ExceptionInterface extends \CodeIgniter\Exceptions\ExceptionInterface +{ +} From cff68f1c81846fef9eaee4d4cd1e2988c86d0d41 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 28 Jan 2024 10:48:56 +0900 Subject: [PATCH 026/277] feat: implements domain-level ExceptionInterface --- system/Database/Exceptions/DatabaseException.php | 2 +- system/Files/Exceptions/FileException.php | 2 +- system/Files/Exceptions/FileNotFoundException.php | 2 +- system/HTTP/Exceptions/HTTPException.php | 2 +- system/HTTP/Exceptions/RedirectException.php | 2 +- system/Router/Exceptions/MethodNotFoundException.php | 2 +- system/Router/Exceptions/RouterException.php | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/system/Database/Exceptions/DatabaseException.php b/system/Database/Exceptions/DatabaseException.php index 244d45d09894..77b170e0b407 100644 --- a/system/Database/Exceptions/DatabaseException.php +++ b/system/Database/Exceptions/DatabaseException.php @@ -16,7 +16,7 @@ use CodeIgniter\Exceptions\HasExitCodeInterface; use CodeIgniter\Exceptions\RuntimeException; -class DatabaseException extends RuntimeException implements HasExitCodeInterface +class DatabaseException extends RuntimeException implements ExceptionInterface, HasExitCodeInterface { public function getExitCode(): int { diff --git a/system/Files/Exceptions/FileException.php b/system/Files/Exceptions/FileException.php index 8634b211afa4..3202978c3af6 100644 --- a/system/Files/Exceptions/FileException.php +++ b/system/Files/Exceptions/FileException.php @@ -16,7 +16,7 @@ use CodeIgniter\Exceptions\DebugTraceableTrait; use CodeIgniter\Exceptions\RuntimeException; -class FileException extends RuntimeException +class FileException extends RuntimeException implements ExceptionInterface { use DebugTraceableTrait; diff --git a/system/Files/Exceptions/FileNotFoundException.php b/system/Files/Exceptions/FileNotFoundException.php index 46c2c1c6cfd5..4c043f9ab02c 100644 --- a/system/Files/Exceptions/FileNotFoundException.php +++ b/system/Files/Exceptions/FileNotFoundException.php @@ -16,7 +16,7 @@ use CodeIgniter\Exceptions\DebugTraceableTrait; use CodeIgniter\Exceptions\RuntimeException; -class FileNotFoundException extends RuntimeException +class FileNotFoundException extends RuntimeException implements ExceptionInterface { use DebugTraceableTrait; diff --git a/system/HTTP/Exceptions/HTTPException.php b/system/HTTP/Exceptions/HTTPException.php index 946af3d9fb15..146a565b8c77 100644 --- a/system/HTTP/Exceptions/HTTPException.php +++ b/system/HTTP/Exceptions/HTTPException.php @@ -18,7 +18,7 @@ /** * Things that can go wrong with HTTP */ -class HTTPException extends FrameworkException +class HTTPException extends FrameworkException implements ExceptionInterface { /** * For CurlRequest diff --git a/system/HTTP/Exceptions/RedirectException.php b/system/HTTP/Exceptions/RedirectException.php index a13bc933a095..0ee895fbb688 100644 --- a/system/HTTP/Exceptions/RedirectException.php +++ b/system/HTTP/Exceptions/RedirectException.php @@ -24,7 +24,7 @@ /** * RedirectException */ -class RedirectException extends RuntimeException implements ResponsableInterface, HTTPExceptionInterface +class RedirectException extends RuntimeException implements ExceptionInterface, ResponsableInterface, HTTPExceptionInterface { /** * HTTP status code for redirects diff --git a/system/Router/Exceptions/MethodNotFoundException.php b/system/Router/Exceptions/MethodNotFoundException.php index 19e62eb02c6d..2dabad79b6a1 100644 --- a/system/Router/Exceptions/MethodNotFoundException.php +++ b/system/Router/Exceptions/MethodNotFoundException.php @@ -18,6 +18,6 @@ /** * @internal */ -final class MethodNotFoundException extends RuntimeException +final class MethodNotFoundException extends RuntimeException implements ExceptionInterface { } diff --git a/system/Router/Exceptions/RouterException.php b/system/Router/Exceptions/RouterException.php index fe21b53d32b7..378647289a72 100644 --- a/system/Router/Exceptions/RouterException.php +++ b/system/Router/Exceptions/RouterException.php @@ -18,7 +18,7 @@ /** * RouterException */ -class RouterException extends FrameworkException +class RouterException extends FrameworkException implements ExceptionInterface { /** * Thrown when the actual parameter type does not match From fa0fea5610e9113ad020bdb3ce3cc6738be0eec8 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 29 Jan 2024 14:57:02 +0900 Subject: [PATCH 027/277] refactor: use CodeIgniter\Exceptions\InvalidArgumentException --- system/Helpers/text_helper.php | 1 + 1 file changed, 1 insertion(+) diff --git a/system/Helpers/text_helper.php b/system/Helpers/text_helper.php index 7f5fb2cf2d3f..8ce3677b0eee 100755 --- a/system/Helpers/text_helper.php +++ b/system/Helpers/text_helper.php @@ -11,6 +11,7 @@ * the LICENSE file that was distributed with this source code. */ +use CodeIgniter\Exceptions\InvalidArgumentException; use Config\ForeignCharacters; // CodeIgniter Text Helpers From 8e75308f86373b22c9f5b9c061a4fc705f775c8b Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 29 Jan 2024 15:00:02 +0900 Subject: [PATCH 028/277] test: use odeIgniter\Exceptions\RuntimeException --- system/HTTP/Files/UploadedFile.php | 2 -- tests/_support/Autoloader/FatalLocator.php | 2 +- tests/_support/Commands/AppInfo.php | 2 +- tests/_support/Config/Services.php | 2 +- tests/_support/Controllers/Popcorn.php | 2 +- tests/system/Autoloader/AutoloaderTest.php | 2 +- tests/system/CLI/CLITest.php | 2 +- tests/system/CommonFunctionsTest.php | 2 +- tests/system/CommonHelperTest.php | 2 +- tests/system/Config/BaseConfigTest.php | 2 +- tests/system/Config/ServicesTest.php | 2 +- tests/system/Database/Live/ForgeTest.php | 2 +- tests/system/Debug/ExceptionHandlerTest.php | 2 +- tests/system/Debug/ExceptionsTest.php | 2 +- tests/system/Debug/TimerTest.php | 2 +- tests/system/Format/JSONFormatterTest.php | 2 +- tests/system/View/ViewTest.php | 2 +- 17 files changed, 16 insertions(+), 18 deletions(-) diff --git a/system/HTTP/Files/UploadedFile.php b/system/HTTP/Files/UploadedFile.php index fe19f0df0527..78643aa7166e 100644 --- a/system/HTTP/Files/UploadedFile.php +++ b/system/HTTP/Files/UploadedFile.php @@ -13,8 +13,6 @@ namespace CodeIgniter\HTTP\Files; -use CodeIgniter\Exceptions\InvalidArgumentException; -use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\Files\File; use CodeIgniter\HTTP\Exceptions\HTTPException; use Config\Mimes; diff --git a/tests/_support/Autoloader/FatalLocator.php b/tests/_support/Autoloader/FatalLocator.php index 578bd02ad9ae..0b98fddc6eca 100644 --- a/tests/_support/Autoloader/FatalLocator.php +++ b/tests/_support/Autoloader/FatalLocator.php @@ -14,7 +14,7 @@ namespace Tests\Support\Autoloader; use CodeIgniter\Autoloader\FileLocator; -use RuntimeException; +use CodeIgniter\Exceptions\RuntimeException; /** * Class FatalLocator diff --git a/tests/_support/Commands/AppInfo.php b/tests/_support/Commands/AppInfo.php index ae609514ad54..767523071466 100644 --- a/tests/_support/Commands/AppInfo.php +++ b/tests/_support/Commands/AppInfo.php @@ -16,7 +16,7 @@ use CodeIgniter\CLI\BaseCommand; use CodeIgniter\CLI\CLI; use CodeIgniter\CodeIgniter; -use RuntimeException; +use CodeIgniter\Exceptions\RuntimeException; class AppInfo extends BaseCommand { diff --git a/tests/_support/Config/Services.php b/tests/_support/Config/Services.php index 809fd9bad173..712c79004a0b 100644 --- a/tests/_support/Config/Services.php +++ b/tests/_support/Config/Services.php @@ -13,11 +13,11 @@ namespace Tests\Support\Config; +use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\HTTP\SiteURIFactory; use CodeIgniter\HTTP\URI; use Config\App; use Config\Services as BaseServices; -use RuntimeException; /** * Services Class diff --git a/tests/_support/Controllers/Popcorn.php b/tests/_support/Controllers/Popcorn.php index 2223a3c3108a..f736f0dcc50f 100644 --- a/tests/_support/Controllers/Popcorn.php +++ b/tests/_support/Controllers/Popcorn.php @@ -15,7 +15,7 @@ use CodeIgniter\API\ResponseTrait; use CodeIgniter\Controller; -use RuntimeException; +use CodeIgniter\Exceptions\RuntimeException; /** * This is a testing only controller, intended to blow up in multiple diff --git a/tests/system/Autoloader/AutoloaderTest.php b/tests/system/Autoloader/AutoloaderTest.php index 647b041d5f78..094b4cfd6b38 100644 --- a/tests/system/Autoloader/AutoloaderTest.php +++ b/tests/system/Autoloader/AutoloaderTest.php @@ -16,6 +16,7 @@ use App\Controllers\Home; use Closure; use CodeIgniter\Exceptions\ConfigException; +use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\ReflectionHelper; use Config\Autoload; @@ -25,7 +26,6 @@ use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\PreserveGlobalState; use PHPUnit\Framework\Attributes\RunInSeparateProcess; -use RuntimeException; use UnnamespacedClass; /** diff --git a/tests/system/CLI/CLITest.php b/tests/system/CLI/CLITest.php index fc62e5888e53..80a55ade45cf 100644 --- a/tests/system/CLI/CLITest.php +++ b/tests/system/CLI/CLITest.php @@ -13,13 +13,13 @@ namespace CodeIgniter\CLI; +use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\PhpStreamWrapper; use CodeIgniter\Test\StreamFilterTrait; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use ReflectionProperty; -use RuntimeException; /** * @internal diff --git a/tests/system/CommonFunctionsTest.php b/tests/system/CommonFunctionsTest.php index 226f9f79c776..fbf3f3fe9ab2 100644 --- a/tests/system/CommonFunctionsTest.php +++ b/tests/system/CommonFunctionsTest.php @@ -15,6 +15,7 @@ use CodeIgniter\Config\BaseService; use CodeIgniter\Config\Factories; +use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\HTTP\CLIRequest; use CodeIgniter\HTTP\Exceptions\RedirectException; use CodeIgniter\HTTP\IncomingRequest; @@ -46,7 +47,6 @@ use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\PreserveGlobalState; use PHPUnit\Framework\Attributes\RunInSeparateProcess; -use RuntimeException; use stdClass; use Tests\Support\Models\JobModel; diff --git a/tests/system/CommonHelperTest.php b/tests/system/CommonHelperTest.php index 819b5c582d4c..0f91f1088073 100644 --- a/tests/system/CommonHelperTest.php +++ b/tests/system/CommonHelperTest.php @@ -14,12 +14,12 @@ namespace CodeIgniter; use CodeIgniter\Autoloader\FileLocator; +use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\Test\CIUnitTestCase; use Config\Services; use PHPUnit\Framework\Attributes\CoversFunction; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\MockObject\MockObject; -use RuntimeException; use Tests\Support\Autoloader\FatalLocator; /** diff --git a/tests/system/Config/BaseConfigTest.php b/tests/system/Config/BaseConfigTest.php index 68776b671fda..8a169801b32f 100644 --- a/tests/system/Config/BaseConfigTest.php +++ b/tests/system/Config/BaseConfigTest.php @@ -14,6 +14,7 @@ namespace CodeIgniter\Config; use CodeIgniter\Autoloader\FileLocator; +use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\Test\CIUnitTestCase; use Config\Modules; use Encryption; @@ -22,7 +23,6 @@ use PHPUnit\Framework\Attributes\RunInSeparateProcess; use PHPUnit\Framework\MockObject\MockObject; use RegistrarConfig; -use RuntimeException; use SimpleConfig; use Tests\Support\Config\BadRegistrar; use Tests\Support\Config\TestRegistrar; diff --git a/tests/system/Config/ServicesTest.php b/tests/system/Config/ServicesTest.php index 17b97dd2e259..5ab4026a5c92 100644 --- a/tests/system/Config/ServicesTest.php +++ b/tests/system/Config/ServicesTest.php @@ -20,6 +20,7 @@ use CodeIgniter\Debug\Timer; use CodeIgniter\Debug\Toolbar; use CodeIgniter\Email\Email; +use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\Filters\Filters; use CodeIgniter\Format\Format; use CodeIgniter\Honeypot\Honeypot; @@ -49,7 +50,6 @@ use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\PreserveGlobalState; use PHPUnit\Framework\Attributes\RunInSeparateProcess; -use RuntimeException; use Tests\Support\Config\Services; /** diff --git a/tests/system/Database/Live/ForgeTest.php b/tests/system/Database/Live/ForgeTest.php index 7fccd6b1d298..aa415a9d72a5 100644 --- a/tests/system/Database/Live/ForgeTest.php +++ b/tests/system/Database/Live/ForgeTest.php @@ -15,12 +15,12 @@ use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Database\Forge; +use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\DatabaseTestTrait; use Config\Database; use LogicException; use PHPUnit\Framework\Attributes\Group; -use RuntimeException; use stdClass; use Tests\Support\Database\Seeds\CITestSeeder; diff --git a/tests/system/Debug/ExceptionHandlerTest.php b/tests/system/Debug/ExceptionHandlerTest.php index 85eaed74621b..0d6ba9499ec3 100644 --- a/tests/system/Debug/ExceptionHandlerTest.php +++ b/tests/system/Debug/ExceptionHandlerTest.php @@ -15,13 +15,13 @@ use App\Controllers\Home; use CodeIgniter\Exceptions\PageNotFoundException; +use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\IniTestTrait; use CodeIgniter\Test\StreamFilterTrait; use Config\Exceptions as ExceptionsConfig; use Config\Services; use PHPUnit\Framework\Attributes\Group; -use RuntimeException; /** * @internal diff --git a/tests/system/Debug/ExceptionsTest.php b/tests/system/Debug/ExceptionsTest.php index aa3c2d8b49a6..0a760b79aa61 100644 --- a/tests/system/Debug/ExceptionsTest.php +++ b/tests/system/Debug/ExceptionsTest.php @@ -16,13 +16,13 @@ use CodeIgniter\Entity\Exceptions\CastException; use CodeIgniter\Exceptions\ConfigException; use CodeIgniter\Exceptions\PageNotFoundException; +use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\ReflectionHelper; use Config\Exceptions as ExceptionsConfig; use ErrorException; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\RequiresPhp; -use RuntimeException; /** * @internal diff --git a/tests/system/Debug/TimerTest.php b/tests/system/Debug/TimerTest.php index eb3cb243b398..d76e35583305 100644 --- a/tests/system/Debug/TimerTest.php +++ b/tests/system/Debug/TimerTest.php @@ -14,9 +14,9 @@ namespace CodeIgniter\Debug; use ArgumentCountError; +use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\Test\CIUnitTestCase; use PHPUnit\Framework\Attributes\Group; -use RuntimeException; /** * @internal diff --git a/tests/system/Format/JSONFormatterTest.php b/tests/system/Format/JSONFormatterTest.php index a479be9e8f63..7ac97091a4c7 100644 --- a/tests/system/Format/JSONFormatterTest.php +++ b/tests/system/Format/JSONFormatterTest.php @@ -13,9 +13,9 @@ namespace CodeIgniter\Format; +use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\Test\CIUnitTestCase; use PHPUnit\Framework\Attributes\Group; -use RuntimeException; /** * @internal diff --git a/tests/system/View/ViewTest.php b/tests/system/View/ViewTest.php index d1455a604f1b..98ce11245a4d 100644 --- a/tests/system/View/ViewTest.php +++ b/tests/system/View/ViewTest.php @@ -15,11 +15,11 @@ use CodeIgniter\Autoloader\FileLocator; use CodeIgniter\Config\Services; +use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\View\Exceptions\ViewException; use Config; use PHPUnit\Framework\Attributes\Group; -use RuntimeException; /** * @internal From 8de2d7004ff8417ae7df2edb87c7a4e182df0058 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 29 Jan 2024 15:02:09 +0900 Subject: [PATCH 029/277] test: use CodeIgniter\Exceptions\InvalidArgumentException --- tests/system/AutoReview/ComposerJsonTest.php | 2 +- tests/system/Autoloader/AutoloaderTest.php | 2 +- tests/system/Config/FactoriesTest.php | 2 +- tests/system/Database/Builder/InsertTest.php | 2 +- tests/system/HTTP/IncomingRequestTest.php | 2 +- tests/system/HTTP/MessageTest.php | 2 +- tests/system/Helpers/Array/ArrayHelperDotKeyExistsTest.php | 2 +- tests/system/Helpers/TextHelperTest.php | 2 +- tests/system/Helpers/URLHelper/MiscUrlTest.php | 2 +- tests/system/Models/UpdateModelTest.php | 2 +- tests/system/Validation/FileRulesTest.php | 2 +- .../system/Validation/StrictRules/DatabaseRelatedRulesTest.php | 2 +- tests/system/Validation/StrictRules/FileRulesTest.php | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/system/AutoReview/ComposerJsonTest.php b/tests/system/AutoReview/ComposerJsonTest.php index 995ef99f4f2b..e7ca88f688c2 100644 --- a/tests/system/AutoReview/ComposerJsonTest.php +++ b/tests/system/AutoReview/ComposerJsonTest.php @@ -13,7 +13,7 @@ namespace CodeIgniter\AutoReview; -use InvalidArgumentException; +use CodeIgniter\Exceptions\InvalidArgumentException; use JsonException; use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\Attributes\Group; diff --git a/tests/system/Autoloader/AutoloaderTest.php b/tests/system/Autoloader/AutoloaderTest.php index 094b4cfd6b38..b0dab8d8d841 100644 --- a/tests/system/Autoloader/AutoloaderTest.php +++ b/tests/system/Autoloader/AutoloaderTest.php @@ -16,13 +16,13 @@ use App\Controllers\Home; use Closure; use CodeIgniter\Exceptions\ConfigException; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\ReflectionHelper; use Config\Autoload; use Config\Modules; use Config\Services; -use InvalidArgumentException; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\PreserveGlobalState; use PHPUnit\Framework\Attributes\RunInSeparateProcess; diff --git a/tests/system/Config/FactoriesTest.php b/tests/system/Config/FactoriesTest.php index 11a5e77a433e..4bebc304f4fa 100644 --- a/tests/system/Config/FactoriesTest.php +++ b/tests/system/Config/FactoriesTest.php @@ -13,10 +13,10 @@ namespace CodeIgniter\Config; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Test\CIUnitTestCase; use Config\App; use Config\Database; -use InvalidArgumentException; use PHPUnit\Framework\Attributes\Depends; use PHPUnit\Framework\Attributes\Group; use ReflectionClass; diff --git a/tests/system/Database/Builder/InsertTest.php b/tests/system/Database/Builder/InsertTest.php index e021472b72fc..ca778a098408 100644 --- a/tests/system/Database/Builder/InsertTest.php +++ b/tests/system/Database/Builder/InsertTest.php @@ -16,9 +16,9 @@ use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Database\Query; use CodeIgniter\Database\RawSql; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Mock\MockConnection; -use InvalidArgumentException; use PHPUnit\Framework\Attributes\Group; /** diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index eb15b2c37c91..2bbe52897eb5 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -15,11 +15,11 @@ use CodeIgniter\Config\Factories; use CodeIgniter\Exceptions\ConfigException; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\HTTP\Exceptions\HTTPException; use CodeIgniter\HTTP\Files\UploadedFile; use CodeIgniter\Test\CIUnitTestCase; use Config\App; -use InvalidArgumentException; use JsonException; use PHPUnit\Framework\Attributes\BackupGlobals; use PHPUnit\Framework\Attributes\DataProvider; diff --git a/tests/system/HTTP/MessageTest.php b/tests/system/HTTP/MessageTest.php index d16224367a6f..79bfb2dcdf6f 100644 --- a/tests/system/HTTP/MessageTest.php +++ b/tests/system/HTTP/MessageTest.php @@ -13,9 +13,9 @@ namespace CodeIgniter\HTTP; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\HTTP\Exceptions\HTTPException; use CodeIgniter\Test\CIUnitTestCase; -use InvalidArgumentException; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; diff --git a/tests/system/Helpers/Array/ArrayHelperDotKeyExistsTest.php b/tests/system/Helpers/Array/ArrayHelperDotKeyExistsTest.php index ea7b379d0b44..0641c41b2bd0 100644 --- a/tests/system/Helpers/Array/ArrayHelperDotKeyExistsTest.php +++ b/tests/system/Helpers/Array/ArrayHelperDotKeyExistsTest.php @@ -13,8 +13,8 @@ namespace CodeIgniter\Helpers\Array; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Test\CIUnitTestCase; -use InvalidArgumentException; use PHPUnit\Framework\Attributes\Group; /** diff --git a/tests/system/Helpers/TextHelperTest.php b/tests/system/Helpers/TextHelperTest.php index e3acd7bb9c50..4033e3c72e1a 100755 --- a/tests/system/Helpers/TextHelperTest.php +++ b/tests/system/Helpers/TextHelperTest.php @@ -13,8 +13,8 @@ namespace CodeIgniter\Helpers; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Test\CIUnitTestCase; -use InvalidArgumentException; use PHPUnit\Framework\Attributes\Group; /** diff --git a/tests/system/Helpers/URLHelper/MiscUrlTest.php b/tests/system/Helpers/URLHelper/MiscUrlTest.php index 87c6a3940208..da25199bf957 100644 --- a/tests/system/Helpers/URLHelper/MiscUrlTest.php +++ b/tests/system/Helpers/URLHelper/MiscUrlTest.php @@ -15,6 +15,7 @@ use CodeIgniter\Config\Factories; use CodeIgniter\Config\Services; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\SiteURIFactory; use CodeIgniter\HTTP\UserAgent; @@ -22,7 +23,6 @@ use CodeIgniter\Superglobals; use CodeIgniter\Test\CIUnitTestCase; use Config\App; -use InvalidArgumentException; use PHPUnit\Framework\Attributes\BackupGlobals; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; diff --git a/tests/system/Models/UpdateModelTest.php b/tests/system/Models/UpdateModelTest.php index c7c8b9ad51d4..2f9cac3958df 100644 --- a/tests/system/Models/UpdateModelTest.php +++ b/tests/system/Models/UpdateModelTest.php @@ -16,8 +16,8 @@ use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Database\Exceptions\DataException; use CodeIgniter\Entity\Entity; +use CodeIgniter\Exceptions\InvalidArgumentException; use Config\Database; -use InvalidArgumentException; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use stdClass; diff --git a/tests/system/Validation/FileRulesTest.php b/tests/system/Validation/FileRulesTest.php index 6d59af537a06..ec016ee0dbeb 100644 --- a/tests/system/Validation/FileRulesTest.php +++ b/tests/system/Validation/FileRulesTest.php @@ -13,9 +13,9 @@ namespace CodeIgniter\Validation; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Test\CIUnitTestCase; use Config\Services; -use InvalidArgumentException; use PHPUnit\Framework\Attributes\Group; use Tests\Support\Validation\TestRules; diff --git a/tests/system/Validation/StrictRules/DatabaseRelatedRulesTest.php b/tests/system/Validation/StrictRules/DatabaseRelatedRulesTest.php index 1535d265f729..5f8472ec29f0 100644 --- a/tests/system/Validation/StrictRules/DatabaseRelatedRulesTest.php +++ b/tests/system/Validation/StrictRules/DatabaseRelatedRulesTest.php @@ -13,12 +13,12 @@ namespace CodeIgniter\Validation\StrictRules; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\DatabaseTestTrait; use CodeIgniter\Validation\Validation; use Config\Database; use Config\Services; -use InvalidArgumentException; use LogicException; use PHPUnit\Framework\Attributes\Group; use Tests\Support\Validation\TestRules; diff --git a/tests/system/Validation/StrictRules/FileRulesTest.php b/tests/system/Validation/StrictRules/FileRulesTest.php index 07dfc44ad8ce..ab31b5cff06b 100644 --- a/tests/system/Validation/StrictRules/FileRulesTest.php +++ b/tests/system/Validation/StrictRules/FileRulesTest.php @@ -13,10 +13,10 @@ namespace CodeIgniter\Validation\StrictRules; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Validation\Validation; use Config\Services; -use InvalidArgumentException; use PHPUnit\Framework\Attributes\Group; use Tests\Support\Validation\TestRules; From 29b8597bf7c87959771d43b4efaeb861b069cb33 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 29 Jan 2024 15:03:11 +0900 Subject: [PATCH 030/277] test: use CodeIgniter\Exceptions\LogicException --- tests/system/Cookie/CookieTest.php | 2 +- tests/system/Database/Live/ForgeTest.php | 2 +- tests/system/HTTP/RedirectExceptionTest.php | 2 +- tests/system/View/ControlledCellTest.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/system/Cookie/CookieTest.php b/tests/system/Cookie/CookieTest.php index 51016ca0061e..444812c31485 100644 --- a/tests/system/Cookie/CookieTest.php +++ b/tests/system/Cookie/CookieTest.php @@ -14,11 +14,11 @@ namespace CodeIgniter\Cookie; use CodeIgniter\Cookie\Exceptions\CookieException; +use CodeIgniter\Exceptions\LogicException; use CodeIgniter\Test\CIUnitTestCase; use Config\Cookie as CookieConfig; use DateTimeImmutable; use DateTimeZone; -use LogicException; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; diff --git a/tests/system/Database/Live/ForgeTest.php b/tests/system/Database/Live/ForgeTest.php index aa415a9d72a5..53c6a2144819 100644 --- a/tests/system/Database/Live/ForgeTest.php +++ b/tests/system/Database/Live/ForgeTest.php @@ -15,11 +15,11 @@ use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Database\Forge; +use CodeIgniter\Exceptions\LogicException; use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\DatabaseTestTrait; use Config\Database; -use LogicException; use PHPUnit\Framework\Attributes\Group; use stdClass; use Tests\Support\Database\Seeds\CITestSeeder; diff --git a/tests/system/HTTP/RedirectExceptionTest.php b/tests/system/HTTP/RedirectExceptionTest.php index e77c0ea060d3..b1f325199cc6 100644 --- a/tests/system/HTTP/RedirectExceptionTest.php +++ b/tests/system/HTTP/RedirectExceptionTest.php @@ -13,12 +13,12 @@ namespace CodeIgniter\HTTP; +use CodeIgniter\Exceptions\LogicException; use CodeIgniter\HTTP\Exceptions\RedirectException; use CodeIgniter\I18n\Time; use CodeIgniter\Log\Logger; use CodeIgniter\Test\Mock\MockLogger as LoggerConfig; use Config\Services; -use LogicException; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; use Tests\Support\Log\Handlers\TestHandler; diff --git a/tests/system/View/ControlledCellTest.php b/tests/system/View/ControlledCellTest.php index 8448aed22ced..96e3be2e3115 100644 --- a/tests/system/View/ControlledCellTest.php +++ b/tests/system/View/ControlledCellTest.php @@ -13,9 +13,9 @@ namespace CodeIgniter\View; +use CodeIgniter\Exceptions\LogicException; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\View\Exceptions\ViewException; -use LogicException; use PHPUnit\Framework\Attributes\Group; use Tests\Support\View\Cells\AdditionCell; use Tests\Support\View\Cells\AwesomeCell; From aae19e6b73433fd4345561cd3216a6d9a9a75508 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 29 Jan 2024 15:03:58 +0900 Subject: [PATCH 031/277] test: use CodeIgniter\Exceptions\BadMethodCallException --- tests/system/Database/Live/PreparedQueryTest.php | 2 +- tests/system/HTTP/SiteURITest.php | 2 +- tests/system/Models/GeneralModelTest.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/system/Database/Live/PreparedQueryTest.php b/tests/system/Database/Live/PreparedQueryTest.php index fd3b6cedb403..b44e127283c6 100644 --- a/tests/system/Database/Live/PreparedQueryTest.php +++ b/tests/system/Database/Live/PreparedQueryTest.php @@ -13,11 +13,11 @@ namespace CodeIgniter\Database\Live; -use BadMethodCallException; use CodeIgniter\Database\BasePreparedQuery; use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Database\Query; use CodeIgniter\Database\ResultInterface; +use CodeIgniter\Exceptions\BadMethodCallException; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\DatabaseTestTrait; use PHPUnit\Framework\Attributes\Group; diff --git a/tests/system/HTTP/SiteURITest.php b/tests/system/HTTP/SiteURITest.php index 644aaa795b6d..6aca302e7cc7 100644 --- a/tests/system/HTTP/SiteURITest.php +++ b/tests/system/HTTP/SiteURITest.php @@ -13,7 +13,7 @@ namespace CodeIgniter\HTTP; -use BadMethodCallException; +use CodeIgniter\Exceptions\BadMethodCallException; use CodeIgniter\Exceptions\ConfigException; use CodeIgniter\HTTP\Exceptions\HTTPException; use CodeIgniter\Test\CIUnitTestCase; diff --git a/tests/system/Models/GeneralModelTest.php b/tests/system/Models/GeneralModelTest.php index d0e50768457a..c0d3d806dbdd 100644 --- a/tests/system/Models/GeneralModelTest.php +++ b/tests/system/Models/GeneralModelTest.php @@ -13,8 +13,8 @@ namespace CodeIgniter\Models; -use BadMethodCallException; use CodeIgniter\Database\BaseConnection; +use CodeIgniter\Exceptions\BadMethodCallException; use CodeIgniter\Model; use CodeIgniter\Test\CIUnitTestCase; use PHPUnit\Framework\Attributes\Group; From ac9a7164486c7cd4fc0d14a1d70a294675df0328 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 29 Jan 2024 15:04:45 +0900 Subject: [PATCH 032/277] test: use narrower exceptions --- .../system/Cache/Handlers/MemcachedHandlerTest.php | 3 ++- tests/system/Cache/ResponseCacheTest.php | 4 ++-- tests/system/Log/LoggerTest.php | 6 +++--- tests/system/Test/ControllerTestTraitTest.php | 13 ++++++++----- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/tests/system/Cache/Handlers/MemcachedHandlerTest.php b/tests/system/Cache/Handlers/MemcachedHandlerTest.php index 4aad8288401c..2e3c69895d21 100644 --- a/tests/system/Cache/Handlers/MemcachedHandlerTest.php +++ b/tests/system/Cache/Handlers/MemcachedHandlerTest.php @@ -14,6 +14,7 @@ namespace CodeIgniter\Cache\Handlers; use CodeIgniter\CLI\CLI; +use CodeIgniter\Exceptions\BadMethodCallException; use CodeIgniter\I18n\Time; use Config\Cache; use Exception; @@ -125,7 +126,7 @@ public function testDelete(): void public function testDeleteMatching(): void { // Not implemented for Memcached, should throw an exception - $this->expectException(Exception::class); + $this->expectException(BadMethodCallException::class); $this->handler->deleteMatching('key*'); } diff --git a/tests/system/Cache/ResponseCacheTest.php b/tests/system/Cache/ResponseCacheTest.php index b0b73855231a..4888c78bfbcb 100644 --- a/tests/system/Cache/ResponseCacheTest.php +++ b/tests/system/Cache/ResponseCacheTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Cache; +use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\HTTP\CLIRequest; use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\Response; @@ -23,7 +24,6 @@ use Config\App as AppConfig; use Config\Cache as CacheConfig; use ErrorException; -use Exception; use PHPUnit\Framework\Attributes\BackupGlobals; use PHPUnit\Framework\Attributes\Group; @@ -245,7 +245,7 @@ public function testUnserializeError(): void public function testInvalidCacheError(): void { - $this->expectException(Exception::class); + $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Error unserializing page cache'); $cache = mock(CacheFactory::class); diff --git a/tests/system/Log/LoggerTest.php b/tests/system/Log/LoggerTest.php index 9464d4d286fd..09efa7be4a1d 100644 --- a/tests/system/Log/LoggerTest.php +++ b/tests/system/Log/LoggerTest.php @@ -14,11 +14,11 @@ namespace CodeIgniter\Log; use CodeIgniter\Exceptions\FrameworkException; +use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\I18n\Time; use CodeIgniter\Log\Exceptions\LogException; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Mock\MockLogger as LoggerConfig; -use Exception; use PHPUnit\Framework\Attributes\Group; use Tests\Support\Log\Handlers\TestHandler; use TypeError; @@ -241,8 +241,8 @@ public function testLogInterpolatesExceptions(): void $expected = 'ERROR - ' . Time::now()->format('Y-m-d') . ' --> [ERROR] These are not the droids you are looking for'; try { - throw new Exception('These are not the droids you are looking for'); - } catch (Exception $e) { + throw new RuntimeException('These are not the droids you are looking for'); + } catch (RuntimeException $e) { $logger->log('error', '[ERROR] {exception}', ['exception' => $e]); } diff --git a/tests/system/Test/ControllerTestTraitTest.php b/tests/system/Test/ControllerTestTraitTest.php index 8230dd9d6248..43fdf7a23b23 100644 --- a/tests/system/Test/ControllerTestTraitTest.php +++ b/tests/system/Test/ControllerTestTraitTest.php @@ -16,11 +16,12 @@ use App\Controllers\Home; use App\Controllers\NeverHeardOfIt; use CodeIgniter\Controller; +use CodeIgniter\Exceptions\InvalidArgumentException; +use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\Log\Logger; use CodeIgniter\Test\Mock\MockLogger as LoggerConfig; use Config\App; use Config\Services; -use Exception; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\PreserveGlobalState; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; @@ -50,7 +51,8 @@ protected function setUp(): void public function testBadController(): void { - $this->expectException('InvalidArgumentException'); + $this->expectException(InvalidArgumentException::class); + $logger = new Logger(new LoggerConfig()); $this->withURI('http://example.com') ->withLogger($logger) @@ -60,7 +62,8 @@ public function testBadController(): void public function testBadControllerMethod(): void { - $this->expectException('InvalidArgumentException'); + $this->expectException(InvalidArgumentException::class); + $logger = new Logger(new LoggerConfig()); $this->withURI('http://example.com') ->withLogger($logger) @@ -261,12 +264,12 @@ public function testUsesRequestBody(): void $this->controller = new class () extends Controller { public function throwsBody(): never { - throw new Exception($this->request->getBody()); + throw new RuntimeException($this->request->getBody()); } }; $this->controller->initController($this->request, $this->response, $this->logger); - $this->expectException(Exception::class); + $this->expectException(RuntimeException::class); $this->expectExceptionMessage('banana'); $this->withBody('banana')->execute('throwsBody'); From afe99687a8291b1bda90321ff181a134a37fe8bf Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 29 Jan 2024 16:07:56 +0900 Subject: [PATCH 033/277] refactor: use CodeIgniter\Exceptions\InvalidArgumentException --- system/Common.php | 1 + system/Helpers/filesystem_helper.php | 2 ++ 2 files changed, 3 insertions(+) diff --git a/system/Common.php b/system/Common.php index f96e9f100f00..96f9d5bb756f 100644 --- a/system/Common.php +++ b/system/Common.php @@ -20,6 +20,7 @@ use CodeIgniter\Database\BaseConnection; use CodeIgniter\Database\ConnectionInterface; use CodeIgniter\Debug\Timer; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Files\Exceptions\FileNotFoundException; use CodeIgniter\HTTP\CLIRequest; use CodeIgniter\HTTP\Exceptions\HTTPException; diff --git a/system/Helpers/filesystem_helper.php b/system/Helpers/filesystem_helper.php index 2b86654c960e..1ffabc34b056 100644 --- a/system/Helpers/filesystem_helper.php +++ b/system/Helpers/filesystem_helper.php @@ -11,6 +11,8 @@ * the LICENSE file that was distributed with this source code. */ +use CodeIgniter\Exceptions\InvalidArgumentException; + // CodeIgniter File System Helpers if (! function_exists('directory_map')) { From 55128157b6f561597765eadd527c9b169025f481 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 29 Jan 2024 16:08:24 +0900 Subject: [PATCH 034/277] refactor: use CodeIgniter\Exceptions\RuntimeException --- system/Common.php | 1 + 1 file changed, 1 insertion(+) diff --git a/system/Common.php b/system/Common.php index 96f9d5bb756f..56cbe0cd5233 100644 --- a/system/Common.php +++ b/system/Common.php @@ -21,6 +21,7 @@ use CodeIgniter\Database\ConnectionInterface; use CodeIgniter\Debug\Timer; use CodeIgniter\Exceptions\InvalidArgumentException; +use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\Files\Exceptions\FileNotFoundException; use CodeIgniter\HTTP\CLIRequest; use CodeIgniter\HTTP\Exceptions\HTTPException; From 99e2ba3ccaa3a815b3150c2af0e3d9811808123b Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 16 Apr 2024 11:46:45 +0900 Subject: [PATCH 035/277] refactor: use CodeIgniter\Exceptions\RuntimeException --- system/Commands/Utilities/Optimize.php | 2 +- system/HTTP/Exceptions/BadRequestException.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/system/Commands/Utilities/Optimize.php b/system/Commands/Utilities/Optimize.php index fa7612d524a6..46b24efb2da9 100644 --- a/system/Commands/Utilities/Optimize.php +++ b/system/Commands/Utilities/Optimize.php @@ -17,8 +17,8 @@ use CodeIgniter\Autoloader\FileLocatorCached; use CodeIgniter\CLI\BaseCommand; use CodeIgniter\CLI\CLI; +use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\Publisher\Publisher; -use RuntimeException; /** * Optimize for production. diff --git a/system/HTTP/Exceptions/BadRequestException.php b/system/HTTP/Exceptions/BadRequestException.php index c87f4de294e2..35933dcc1f5c 100644 --- a/system/HTTP/Exceptions/BadRequestException.php +++ b/system/HTTP/Exceptions/BadRequestException.php @@ -14,7 +14,7 @@ namespace CodeIgniter\HTTP\Exceptions; use CodeIgniter\Exceptions\HTTPExceptionInterface; -use RuntimeException; +use CodeIgniter\Exceptions\RuntimeException; /** * 400 Bad Request From 60fddfe7c31e7c41cdff0755dc9eb422834005c2 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 16 Apr 2024 11:47:22 +0900 Subject: [PATCH 036/277] refactor: use CodeIgniter\Exceptions\InvalidArgumentException --- system/Config/BaseService.php | 2 +- system/DataCaster/Cast/BaseCast.php | 2 +- system/DataCaster/Cast/DatetimeCast.php | 2 +- system/DataCaster/DataCaster.php | 2 +- system/Test/Fabricator.php | 5 +++-- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/system/Config/BaseService.php b/system/Config/BaseService.php index cd770dc667ed..551261eda215 100644 --- a/system/Config/BaseService.php +++ b/system/Config/BaseService.php @@ -29,6 +29,7 @@ use CodeIgniter\Debug\Toolbar; use CodeIgniter\Email\Email; use CodeIgniter\Encryption\EncrypterInterface; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Filters\Filters; use CodeIgniter\Format\Format; use CodeIgniter\Honeypot\Honeypot; @@ -78,7 +79,6 @@ use Config\Toolbar as ConfigToolbar; use Config\Validation as ConfigValidation; use Config\View as ConfigView; -use InvalidArgumentException; /** * Services Configuration file. diff --git a/system/DataCaster/Cast/BaseCast.php b/system/DataCaster/Cast/BaseCast.php index c3df0efee103..2a3ca1971674 100644 --- a/system/DataCaster/Cast/BaseCast.php +++ b/system/DataCaster/Cast/BaseCast.php @@ -13,7 +13,7 @@ namespace CodeIgniter\DataCaster\Cast; -use InvalidArgumentException; +use CodeIgniter\Exceptions\InvalidArgumentException; abstract class BaseCast implements CastInterface { diff --git a/system/DataCaster/Cast/DatetimeCast.php b/system/DataCaster/Cast/DatetimeCast.php index 83e66d02217c..b6fa97f9ddda 100644 --- a/system/DataCaster/Cast/DatetimeCast.php +++ b/system/DataCaster/Cast/DatetimeCast.php @@ -14,8 +14,8 @@ namespace CodeIgniter\DataCaster\Cast; use CodeIgniter\Database\BaseConnection; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\I18n\Time; -use InvalidArgumentException; /** * Class DatetimeCast diff --git a/system/DataCaster/DataCaster.php b/system/DataCaster/DataCaster.php index 854a0b50b32c..daeba4b0c0bc 100644 --- a/system/DataCaster/DataCaster.php +++ b/system/DataCaster/DataCaster.php @@ -26,7 +26,7 @@ use CodeIgniter\DataCaster\Cast\URICast; use CodeIgniter\Entity\Cast\CastInterface as EntityCastInterface; use CodeIgniter\Entity\Exceptions\CastException; -use InvalidArgumentException; +use CodeIgniter\Exceptions\InvalidArgumentException; final class DataCaster { diff --git a/system/Test/Fabricator.php b/system/Test/Fabricator.php index 1f61efef5c62..70f3c3adcc72 100644 --- a/system/Test/Fabricator.php +++ b/system/Test/Fabricator.php @@ -15,13 +15,14 @@ use Closure; use CodeIgniter\Exceptions\FrameworkException; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\I18n\Time; use CodeIgniter\Model; use Config\App; use Faker\Factory; use Faker\Generator; -use InvalidArgumentException; +use InvalidArgumentException as BaseInvalidArgumentException; /** * Fabricator @@ -359,7 +360,7 @@ protected function guessFormatter($field): string $this->faker->getFormatter($field); return $field; - } catch (InvalidArgumentException) { + } catch (BaseInvalidArgumentException) { // No match, keep going } From f1359ddc136cead22b4b55db2162f20160c83b78 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 16 Apr 2024 13:44:00 +0900 Subject: [PATCH 037/277] docs: add user guide --- user_guide_src/source/changelogs/v4.6.0.rst | 64 +++++++++++++++++++ user_guide_src/source/general/errors.rst | 30 ++++++++- .../source/installation/upgrade_460.rst | 9 +++ 3 files changed, 102 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index becb1ec0688c..99d7476299f0 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -23,6 +23,26 @@ BREAKING Behavior Changes ================ +.. _v460-behavior-changes-exceptions: + +Exceptions +---------- + +The exception class has been redesigned. See :ref:`exception-design` for details. +The following breaking changes have been made accordingly: + +- ``Validation::setRule()`` now throws ``CodeIgniter\Exceptions\InvalidArgumentException`` + instead of ``TypeError``. + +- ``CriticalError`` now extends ``CodeIgniter\Exceptions\RuntimeException`` + instead of ``Error``. +- ``DatabaseException`` now extends ``CodeIgniter\Exceptions\RuntimeException`` + instead of ``Error``. +- ``ConfigException`` now extends ``CodeIgniter\Exceptions\RuntimeException`` + instead of ``CodeIgniter\Exceptions\CriticalError``. +- ``TestException`` now extends ``CodeIgniter\Exceptions\LogicException`` + instead of ``CodeIgniter\Exceptions\CriticalError``. + Interface Changes ================= @@ -46,6 +66,24 @@ Removed Deprecated Items Enhancements ************ +Exceptions +========== + +The exception class has been redesigned. See :ref:`exception-design` for details. +The following new Exception classes have been added accordingly: + +- ``CodeIgniter\Exceptions\LogicException`` +- ``CodeIgniter\Exceptions\RuntimeException`` +- ``CodeIgniter\Exceptions\BadFunctionCallException`` +- ``CodeIgniter\Exceptions\BadMethodCallException`` +- ``CodeIgniter\Exceptions\InvalidArgumentException`` + +The following new Exception interfaces have been added: + +- ``CodeIgniter\Files\Exceptions\ExceptionInterface`` +- ``CodeIgniter\HTTP\Exceptions\ExceptionInterface`` +- ``CodeIgniter\Router\Exceptions\ExceptionInterface`` + Commands ======== @@ -84,6 +122,32 @@ Message Changes Changes ******* +Exceptions +========== + +The exception classes have been redesigned. See :ref:`exception-design` for details. +The following changes have been made accordingly: + +- The ``deleteMatching()`` method in Cache Handler classes now throws + ``CodeIgniter\Exceptions\BadMethodCallException`` instead of ``Exception``. +- ``Cache\ResponseCache::get()`` now throws ``CodeIgniter\Exceptions\RuntimeException`` + instead of ``Exception``. +- Classes that threw ``RuntimeException`` have been changed to throw + ``CodeIgniter\Exceptions\RuntimeException``. +- Classes that threw ``InvalidArgumentException`` have been changed to throw + ``CodeIgniter\Exceptions\InvalidArgumentException``. +- Classes that threw ``LogicException`` have been changed to throw + ``CodeIgniter\Exceptions\LogicException``. +- Classes that threw ``BadMethodCallException`` have been changed to throw + ``CodeIgniter\Exceptions\BadMethodCallException``. +- Classes that threw ``BadFunctionCallException`` have been changed to throw + ``CodeIgniter\Exceptions\BadFunctionCallException``. + +- ``RedirectException`` now extends ``CodeIgniter\Exceptions\RuntimeException`` + instead of ``Exception``. +- ``PageNotFoundException`` now extends ``CodeIgniter\Exceptions\RuntimeException`` + instead of ``OutOfBoundsException``. + ************ Deprecations ************ diff --git a/user_guide_src/source/general/errors.rst b/user_guide_src/source/general/errors.rst index 2b6eafce8c33..3d95498a7a28 100644 --- a/user_guide_src/source/general/errors.rst +++ b/user_guide_src/source/general/errors.rst @@ -121,7 +121,35 @@ setting the environment variable ``CODEIGNITER_SCREAM_DEPRECATIONS`` to a truthy Framework Exceptions ==================== -The following framework exceptions are available: +.. _exception-design: + +Exception Design +---------------- + +Staring with v4.6.0, all Exception classes that the framework throws: + +- implement ``CodeIgniter\Exceptions\ExceptionInterface`` +- extend ``CodeIgniter\Exceptions\LogicException`` or ``CodeIgniter\Exceptions\RuntimeException`` + +.. note:: The framework only throws the above kind of exception classes, but PHP + or other libraries that are used may throw other exceptions. + +There are two base Exception classes that the framework throws: + +LogicException +-------------- + +``CodeIgniter\Exceptions\LogicException`` extends ``\LogicException``. +This exception represents error in the program logic. This kind of exception +should lead directly to a fix in your code. + +RuntimeException +---------------- + +``CodeIgniter\Exceptions\RuntimeException`` extends ``\RuntimeException``. +This exception is thrown if an error which can only be found on runtime occurs. + +The following framework exceptions are also available: PageNotFoundException --------------------- diff --git a/user_guide_src/source/installation/upgrade_460.rst b/user_guide_src/source/installation/upgrade_460.rst index 4344f7565b55..94f69a2322f9 100644 --- a/user_guide_src/source/installation/upgrade_460.rst +++ b/user_guide_src/source/installation/upgrade_460.rst @@ -20,6 +20,15 @@ Mandatory File Changes Breaking Changes **************** +Exception Changes +================= + +Some classes have changed the exception classes that are thrown. Some exception +classes have changed parent classes. +See :ref:`ChangeLog ` for details. + +If you have code that catches these exceptions, change the exception classes. + Removed Deprecated Items ======================== From e1d3516208e6ed6a58e13e8a6cae76cc8c42c388 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 6 May 2024 19:27:45 +0900 Subject: [PATCH 038/277] refactor: by rector --- tests/system/Cache/Handlers/MemcachedHandlerTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/system/Cache/Handlers/MemcachedHandlerTest.php b/tests/system/Cache/Handlers/MemcachedHandlerTest.php index 2e3c69895d21..991e31d1b75c 100644 --- a/tests/system/Cache/Handlers/MemcachedHandlerTest.php +++ b/tests/system/Cache/Handlers/MemcachedHandlerTest.php @@ -17,7 +17,6 @@ use CodeIgniter\Exceptions\BadMethodCallException; use CodeIgniter\I18n\Time; use Config\Cache; -use Exception; use PHPUnit\Framework\Attributes\Group; /** From b71fc0682038bcd7213a3f9dda6080d9e5148e4a Mon Sep 17 00:00:00 2001 From: Christian Berkman Date: Mon, 10 Jun 2024 23:24:40 +0200 Subject: [PATCH 039/277] FileCollection add retainMultiplePatternsMethod Signed-off-by: Christian Berkman --- system/Files/FileCollection.php | 38 +++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/system/Files/FileCollection.php b/system/Files/FileCollection.php index 2c252018dd6c..a01deae73b04 100644 --- a/system/Files/FileCollection.php +++ b/system/Files/FileCollection.php @@ -340,6 +340,44 @@ public function retainPattern(string $pattern, ?string $scope = null) return $this->removeFiles(array_diff($files, self::matchFiles($files, $pattern))); } + /** + * Keeps only the files from the list that match multiple patterns + * (within the optional scope). + * + * @param array $patterns Array of regex or pseudo-regex strings + * @param string|null $scope A directory to limit the scope + * + * @return $this + */ + public function retainMultiplePatterns(array $patterns, ?string $scope = null) + { + if (count($patterns) === 0) { + return $this; + } + + if (count($patterns) === 1 && $patterns[0] === '') { + return $this; + } + + // Start with all files or those in scope + $files = $scope === null ? $this->files : self::filterFiles($this->files, $scope); + + // Add files to retain to array + $filesToRetain = []; + + foreach ($patterns as $pattern) { + if ($pattern === '') { + continue; + } + + // Matches the pattern within the scoped files + $filesToRetain = array_merge($filesToRetain, self::matchFIles($files, $pattern)); + } + + // Remove the inverse of files to retain + return $this->removeFiles(array_diff($files, $filesToRetain)); + } + // -------------------------------------------------------------------- // Interface Methods // -------------------------------------------------------------------- From f4e89991a02f430c4fefd3dbefd46c1f9007a0d7 Mon Sep 17 00:00:00 2001 From: Christian Berkman Date: Mon, 10 Jun 2024 23:27:39 +0200 Subject: [PATCH 040/277] Add tests for FileCollection::retainMultiplePatterns Signed-off-by: Christian Berkman --- tests/system/Files/FileCollectionTest.php | 70 +++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/tests/system/Files/FileCollectionTest.php b/tests/system/Files/FileCollectionTest.php index 81005356fee3..ddde8667eb3f 100644 --- a/tests/system/Files/FileCollectionTest.php +++ b/tests/system/Files/FileCollectionTest.php @@ -557,6 +557,76 @@ public function testRetainPatternScope(): void $this->assertSame($expected, $collection->get()); } + public function testRetainMultiplePatternsEmptyArray(): void + { + $collection = new FileCollection(); + $collection->addDirectory(SUPPORTPATH . 'Files', true); + + $files = $collection->get(); + + $collection->retainMultiplePatterns([]); + + $this->assertSame($files, $collection->get()); + } + + public function testRetainMultiplePatternsEmptyString(): void + { + $collection = new FileCollection(); + $collection->addDirectory(SUPPORTPATH . 'Files', true); + + $files = $collection->get(); + + $collection->retainMultiplePatterns(['']); + + $this->assertSame($files, $collection->get()); + } + + public function testRetainMultiplePatternsRegex(): void + { + $collection = new FileCollection(); + $collection->addDirectory(SUPPORTPATH . 'Files', true); + + $expected = [ + SUPPORTPATH . 'Files/able/apple.php', + SUPPORTPATH . 'Files/baker/banana.php', + ]; + + $collection->retainMultiplePatterns(['#(apple).*#', '#(banana).*#']); + + $this->assertSame($expected, $collection->get()); + } + + public function testRetainMultiplePatternsPseudo(): void + { + $collection = new FileCollection(); + $collection->addDirectory(SUPPORTPATH . 'Files', true); + + $expected = [ + SUPPORTPATH . 'Files/able/apple.php', + SUPPORTPATH . 'Files/baker/banana.php', + ]; + + $collection->retainMultiplePatterns(['apple*', 'banana*']); + + $this->assertSame($expected, $collection->get()); + } + + public function testRetainMultiplePatternsScope(): void + { + $collection = new FileCollection(); + $collection->addDirectory(SUPPORTPATH . 'Files', true); + + $expected = [ + $this->directory . 'fig_3.php', + SUPPORTPATH . 'Files/baker/banana.php', + SUPPORTPATH . 'Files/baker/fig_3.php.txt', + ]; + + $collection->retainMultiplePatterns(['*_?.php'], $this->directory); + + $this->assertSame($expected, $collection->get()); + } + public function testCount(): void { $collection = new FileCollection(); From a57398d0c2509f1d199b27dc75c3d3786cfa076b Mon Sep 17 00:00:00 2001 From: Christian Berkman Date: Mon, 10 Jun 2024 23:31:49 +0200 Subject: [PATCH 041/277] Updated docs and changelog Signed-off-by: Christian Berkman --- user_guide_src/source/changelogs/v4.6.0.rst | 3 +++ user_guide_src/source/libraries/file_collections.rst | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index 99d7476299f0..44726821d36f 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -66,6 +66,9 @@ Removed Deprecated Items Enhancements ************ +- Added `retainMultiplePatterns()` to `FileCollection` class. + + Exceptions ========== diff --git a/user_guide_src/source/libraries/file_collections.rst b/user_guide_src/source/libraries/file_collections.rst index 0e7b7b46a362..005fb14c6c5e 100644 --- a/user_guide_src/source/libraries/file_collections.rst +++ b/user_guide_src/source/libraries/file_collections.rst @@ -118,6 +118,15 @@ Examples: .. literalinclude:: files/015.php +retainMultiplePatterns(array $pattern, string $scope = null) +============================================================ + +Provides the same functionality as ``retainPattern()`` but accepts an array of patterns to retain files from all patterns. + +Example: + +.. literalinclude:: files/016.php + **************** Retrieving Files **************** From 61dc880edc560ead12a28a3713160043930f2747 Mon Sep 17 00:00:00 2001 From: Christian Berkman Date: Mon, 10 Jun 2024 23:43:45 +0200 Subject: [PATCH 042/277] Doc: add literal include file Signed-off-by: Christian Berkman --- user_guide_src/source/libraries/files/016.php | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 user_guide_src/source/libraries/files/016.php diff --git a/user_guide_src/source/libraries/files/016.php b/user_guide_src/source/libraries/files/016.php new file mode 100644 index 000000000000..caa78e3b4ca3 --- /dev/null +++ b/user_guide_src/source/libraries/files/016.php @@ -0,0 +1,3 @@ +retainMultiplePatterns(['*.css*', '*.js']); // Would keep only *.css and *.js files From 8056eb38819e2f4b31465dd7aa53c687b7155e7d Mon Sep 17 00:00:00 2001 From: Christian Berkman Date: Wed, 12 Jun 2024 06:54:40 +0200 Subject: [PATCH 043/277] fix: FIleCollection::retainMultiplePatterns phpstan errors Signed-off-by: Christian Berkman --- system/Files/FileCollection.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/Files/FileCollection.php b/system/Files/FileCollection.php index a01deae73b04..0f1666221f94 100644 --- a/system/Files/FileCollection.php +++ b/system/Files/FileCollection.php @@ -344,14 +344,14 @@ public function retainPattern(string $pattern, ?string $scope = null) * Keeps only the files from the list that match multiple patterns * (within the optional scope). * - * @param array $patterns Array of regex or pseudo-regex strings + * @param array $patterns Array of regex or pseudo-regex strings * @param string|null $scope A directory to limit the scope * * @return $this */ public function retainMultiplePatterns(array $patterns, ?string $scope = null) { - if (count($patterns) === 0) { + if ($patterns === []) { return $this; } From beb09a91d3b4d5039e244e49923625b66048d8d8 Mon Sep 17 00:00:00 2001 From: Christian Berkman Date: Wed, 12 Jun 2024 06:58:57 +0200 Subject: [PATCH 044/277] docs: move changelog entry to libraries section Signed-off-by: Christian Berkman --- user_guide_src/source/changelogs/v4.6.0.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index 44726821d36f..d73917af42b9 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -66,9 +66,6 @@ Removed Deprecated Items Enhancements ************ -- Added `retainMultiplePatterns()` to `FileCollection` class. - - Exceptions ========== @@ -111,6 +108,8 @@ Model Libraries ========= +- Added `retainMultiplePatterns()` to `FileCollection` class. + Helpers and Functions ===================== From 0206e99a249887055489110af8207b6e5078778b Mon Sep 17 00:00:00 2001 From: christianberkman <39840601+christianberkman@users.noreply.github.com> Date: Wed, 12 Jun 2024 09:21:20 +0300 Subject: [PATCH 045/277] fix: system/Files/FileCollection.php PHPStan Co-authored-by: kenjis --- system/Files/FileCollection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Files/FileCollection.php b/system/Files/FileCollection.php index 0f1666221f94..5faa77fc194a 100644 --- a/system/Files/FileCollection.php +++ b/system/Files/FileCollection.php @@ -371,7 +371,7 @@ public function retainMultiplePatterns(array $patterns, ?string $scope = null) } // Matches the pattern within the scoped files - $filesToRetain = array_merge($filesToRetain, self::matchFIles($files, $pattern)); + $filesToRetain = array_merge($filesToRetain, self::matchFiles($files, $pattern)); } // Remove the inverse of files to retain From 8c6d7e525e6458becd047134077b040b4014db4a Mon Sep 17 00:00:00 2001 From: Christian Berkman Date: Wed, 12 Jun 2024 08:26:56 +0200 Subject: [PATCH 046/277] FileCollection::retainMultiplePatterns fix docblock param Signed-off-by: Christian Berkman --- system/Files/FileCollection.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/system/Files/FileCollection.php b/system/Files/FileCollection.php index 5faa77fc194a..5b8147480e3b 100644 --- a/system/Files/FileCollection.php +++ b/system/Files/FileCollection.php @@ -27,7 +27,7 @@ * filtering, and ordering them. * * @template-implements IteratorAggregate - * @see \CodeIgniter\Files\FileCollectionTest + * @see FileCollectionTest */ class FileCollection implements Countable, IteratorAggregate { @@ -344,8 +344,8 @@ public function retainPattern(string $pattern, ?string $scope = null) * Keeps only the files from the list that match multiple patterns * (within the optional scope). * - * @param array $patterns Array of regex or pseudo-regex strings - * @param string|null $scope A directory to limit the scope + * @param list $patterns Array of regex or pseudo-regex strings + * @param string|null $scope A directory to limit the scope * * @return $this */ From 13b8a546a10ab9432aa799e43b303161651aa826 Mon Sep 17 00:00:00 2001 From: Christian Berkman Date: Wed, 12 Jun 2024 09:25:57 +0200 Subject: [PATCH 047/277] fix: FileCollection.php AddSeeTestAnnotationRector Signed-off-by: Christian Berkman --- system/Files/FileCollection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Files/FileCollection.php b/system/Files/FileCollection.php index 5b8147480e3b..5ba9a925cc5f 100644 --- a/system/Files/FileCollection.php +++ b/system/Files/FileCollection.php @@ -27,7 +27,7 @@ * filtering, and ordering them. * * @template-implements IteratorAggregate - * @see FileCollectionTest + * @see \CodeIgniter\Files\FileCollectionTest */ class FileCollection implements Countable, IteratorAggregate { From 4fab1dad7585a28e9bccebb429b3e37490e617ae Mon Sep 17 00:00:00 2001 From: Christian Berkman Date: Wed, 12 Jun 2024 11:52:57 +0200 Subject: [PATCH 048/277] docs: add reference to new function Signed-off-by: Christian Berkman --- user_guide_src/source/changelogs/v4.6.0.rst | 2 +- user_guide_src/source/libraries/file_collections.rst | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index d73917af42b9..ba9df64b69d1 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -108,7 +108,7 @@ Model Libraries ========= -- Added `retainMultiplePatterns()` to `FileCollection` class. +- Added `retainMultiplePatterns()` to `FileCollection` class. See :ref:`FileCollection::retainMultiplePatterns() ` Helpers and Functions ===================== diff --git a/user_guide_src/source/libraries/file_collections.rst b/user_guide_src/source/libraries/file_collections.rst index 005fb14c6c5e..7cf25a9cbd62 100644 --- a/user_guide_src/source/libraries/file_collections.rst +++ b/user_guide_src/source/libraries/file_collections.rst @@ -118,6 +118,9 @@ Examples: .. literalinclude:: files/015.php + +.. _file-collections-retain-multiple-patterns + retainMultiplePatterns(array $pattern, string $scope = null) ============================================================ From 549cc354b1646c59e269bb7f0a53a59c51ce1b61 Mon Sep 17 00:00:00 2001 From: christianberkman <39840601+christianberkman@users.noreply.github.com> Date: Wed, 12 Jun 2024 13:04:56 +0300 Subject: [PATCH 049/277] docs: fix reference Co-authored-by: kenjis --- user_guide_src/source/changelogs/v4.6.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index ba9df64b69d1..ed4b591aa334 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -108,7 +108,7 @@ Model Libraries ========= -- Added `retainMultiplePatterns()` to `FileCollection` class. See :ref:`FileCollection::retainMultiplePatterns() ` +- Added `retainMultiplePatterns()` to `FileCollection` class. See :ref:`FileCollection::retainMultiplePatterns() `. Helpers and Functions ===================== From fcd33801d413f73f63d167c651eb0d893b969e98 Mon Sep 17 00:00:00 2001 From: christianberkman <39840601+christianberkman@users.noreply.github.com> Date: Wed, 12 Jun 2024 13:05:40 +0300 Subject: [PATCH 050/277] docs: fix section label Co-authored-by: kenjis --- user_guide_src/source/libraries/file_collections.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/libraries/file_collections.rst b/user_guide_src/source/libraries/file_collections.rst index 7cf25a9cbd62..77c046005f62 100644 --- a/user_guide_src/source/libraries/file_collections.rst +++ b/user_guide_src/source/libraries/file_collections.rst @@ -119,7 +119,7 @@ Examples: .. literalinclude:: files/015.php -.. _file-collections-retain-multiple-patterns +.. _file-collections-retain-multiple-patterns: retainMultiplePatterns(array $pattern, string $scope = null) ============================================================ From 655736d2230121b1a29d9b0624c0bfabbac94bda Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 24 May 2024 10:26:38 +0900 Subject: [PATCH 051/277] fix!: add missing methods to RouteCollectionInterface --- system/Router/RouteCollectionInterface.php | 80 ++++++++++++++++++++-- 1 file changed, 75 insertions(+), 5 deletions(-) diff --git a/system/Router/RouteCollectionInterface.php b/system/Router/RouteCollectionInterface.php index bc6ca022bf93..3c34a9be2d8a 100644 --- a/system/Router/RouteCollectionInterface.php +++ b/system/Router/RouteCollectionInterface.php @@ -22,9 +22,6 @@ * A Route Collection's sole job is to hold a series of routes. The required * number of methods is kept very small on purpose, but implementors may * add a number of additional methods to customize how the routes are defined. - * - * The RouteCollection provides the Router with the routes so that it can determine - * which controller should be run. */ interface RouteCollectionInterface { @@ -62,11 +59,19 @@ public function addPlaceholder($placeholder, ?string $pattern = null); */ public function setDefaultNamespace(string $value); + /** + * Returns the default namespace. + */ + public function getDefaultNamespace(): string; + /** * Sets the default controller to use when no other controller has been * specified. * * @return RouteCollectionInterface + * + * @TODO The default controller is only for auto-routing. So this should be + * removed in the future. */ public function setDefaultController(string $value); @@ -86,6 +91,9 @@ public function setDefaultMethod(string $value); * doesn't work well with PHP method names.... * * @return RouteCollectionInterface + * + * @TODO This method is only for auto-routing. So this should be removed in + * the future. */ public function setTranslateURIDashes(bool $value); @@ -96,6 +104,9 @@ public function setTranslateURIDashes(bool $value); * defined routes. * * If FALSE, will stop searching and do NO automatic routing. + * + * @TODO This method is only for auto-routing. So this should be removed in + * the future. */ public function setAutoRoute(bool $value): self; @@ -107,6 +118,9 @@ public function setAutoRoute(bool $value): self; * This setting is passed to the Router class and handled there. * * @param callable|null $callable + * + * @TODO This method is not related to the route collection. So this should + * be removed in the future. */ public function set404Override($callable = null): self; @@ -115,6 +129,9 @@ public function set404Override($callable = null): self; * or the controller/string. * * @return (Closure(string): (ResponseInterface|string|void))|string|null + * + * @TODO This method is not related to the route collection. So this should + * be removed in the future. */ public function get404Override(); @@ -122,6 +139,9 @@ public function get404Override(); * Returns the name of the default controller. With Namespace. * * @return string + * + * @TODO The default controller is only for auto-routing. So this should be + * removed in the future. */ public function getDefaultController(); @@ -136,6 +156,9 @@ public function getDefaultMethod(); * Returns the current value of the translateURIDashes setting. * * @return bool + * + * @TODO This method is only for auto-routing. So this should be removed in + * the future. */ public function shouldTranslateURIDashes(); @@ -143,15 +166,38 @@ public function shouldTranslateURIDashes(); * Returns the flag that tells whether to autoRoute URI against Controllers. * * @return bool + * + * @TODO This method is only for auto-routing. So this should be removed in + * the future. */ public function shouldAutoRoute(); /** * Returns the raw array of available routes. * - * @return array + * @param non-empty-string|null $verb HTTP verb like `GET`,`POST` or `*` or `CLI`. + * @param bool $includeWildcard Whether to include '*' routes. + */ + public function getRoutes(?string $verb = null, bool $includeWildcard = true): array; + + /** + * Returns one or all routes options + * + * @param string|null $from The route path (with placeholders or regex) + * @param string|null $verb HTTP verb like `GET`,`POST` or `*` or `CLI`. + * + * @return array [key => value] */ - public function getRoutes(); + public function getRoutesOptions(?string $from = null, ?string $verb = null): array; + + /** + * Sets the current HTTP verb. + * + * @param string $verb HTTP verb + * + * @return $this + */ + public function setHTTPVerb(string $verb); /** * Returns the current HTTP Verb being used. @@ -194,4 +240,28 @@ public function getRedirectCode(string $routeKey): int; * Get the flag that limit or not the routes with {locale} placeholder to App::$supportedLocales */ public function shouldUseSupportedLocalesOnly(): bool; + + /** + * Checks a route (using the "from") to see if it's filtered or not. + * + * @param string|null $verb HTTP verb like `GET`,`POST` or `*` or `CLI`. + */ + public function isFiltered(string $search, ?string $verb = null): bool; + + /** + * Returns the filters that should be applied for a single route, along + * with any parameters it might have. Parameters are found by splitting + * the parameter name on a colon to separate the filter name from the parameter list, + * and the splitting the result on commas. So: + * + * 'role:admin,manager' + * + * has a filter of "role", with parameters of ['admin', 'manager']. + * + * @param string $search routeKey + * @param string|null $verb HTTP verb like `GET`,`POST` or `*` or `CLI`. + * + * @return list filter_name or filter_name:arguments like 'role:admin,manager' + */ + public function getFiltersForRoute(string $search, ?string $verb = null): array; } From 3b1b93cd409a2c0b8df7ced5b5dba97c313f3cf0 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 24 May 2024 10:27:41 +0900 Subject: [PATCH 052/277] chore: update phpstan-baseline --- phpstan-baseline.php | 31 +++---------------------------- system/Router/Router.php | 3 +-- 2 files changed, 4 insertions(+), 30 deletions(-) diff --git a/phpstan-baseline.php b/phpstan-baseline.php index 418430fa6145..423fd761c4f9 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -7197,34 +7197,9 @@ 'path' => __DIR__ . '/system/Router/Router.php', ]; $ignoreErrors[] = [ - 'message' => '#^Call to an undefined method CodeIgniter\\\\Router\\\\RouteCollectionInterface\\:\\:getDefaultNamespace\\(\\)\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/system/Router/Router.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method CodeIgniter\\\\Router\\\\RouteCollectionInterface\\:\\:getFiltersForRoute\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Router/Router.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method CodeIgniter\\\\Router\\\\RouteCollectionInterface\\:\\:getRegisteredControllers\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Router/Router.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method CodeIgniter\\\\Router\\\\RouteCollectionInterface\\:\\:getRoutesOptions\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Router/Router.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method CodeIgniter\\\\Router\\\\RouteCollectionInterface\\:\\:isFiltered\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Router/Router.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method CodeIgniter\\\\Router\\\\RouteCollectionInterface\\:\\:setHTTPVerb\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Router/Router.php', + 'message' => '#^Call to an undefined method CodeIgniter\\\\Router\\\\RouteCollectionInterface\\:\\:getRegisteredControllers\\(\\)\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Router/Router.php', ]; $ignoreErrors[] = [ 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', diff --git a/system/Router/Router.php b/system/Router/Router.php index de996f534618..2922d367dcdd 100644 --- a/system/Router/Router.php +++ b/system/Router/Router.php @@ -170,7 +170,7 @@ public function __construct(RouteCollectionInterface $routes, ?Request $request ); } else { $this->autoRouter = new AutoRouter( - $this->collection->getRoutes('CLI', false), // @phpstan-ignore-line + $this->collection->getRoutes('CLI', false), $this->collection->getDefaultNamespace(), $this->collection->getDefaultController(), $this->collection->getDefaultMethod(), @@ -400,7 +400,6 @@ public function getLocale() */ protected function checkRoutes(string $uri): bool { - // @phpstan-ignore-next-line $routes = $this->collection->getRoutes($this->collection->getHTTPVerb()); // Don't waste any time From 1994d1e1974728900cad5406b81c6758dba79c81 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 24 May 2024 10:27:59 +0900 Subject: [PATCH 053/277] fix: DefinedRouteCollector to use RouteCollectionInterface --- system/Router/DefinedRouteCollector.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Router/DefinedRouteCollector.php b/system/Router/DefinedRouteCollector.php index 8d4f1dc5b1b7..4c9c853272ae 100644 --- a/system/Router/DefinedRouteCollector.php +++ b/system/Router/DefinedRouteCollector.php @@ -23,7 +23,7 @@ */ final class DefinedRouteCollector { - public function __construct(private readonly RouteCollection $routeCollection) + public function __construct(private readonly RouteCollectionInterface $routeCollection) { } From 374b210651ee7fdd0c1b3d68a16747d8b6ce0d66 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 13 Jun 2024 10:24:55 +0900 Subject: [PATCH 054/277] refactor: add assert() to suppress PHPStan error --- phpstan-baseline.php | 5 ----- system/Router/Router.php | 2 ++ 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/phpstan-baseline.php b/phpstan-baseline.php index 423fd761c4f9..573ace3c93c0 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -7196,11 +7196,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/Router/Router.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method CodeIgniter\\\\Router\\\\RouteCollectionInterface\\:\\:getRegisteredControllers\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Router/Router.php', -]; $ignoreErrors[] = [ 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', 'count' => 2, diff --git a/system/Router/Router.php b/system/Router/Router.php index 2922d367dcdd..ab9ff7d381c5 100644 --- a/system/Router/Router.php +++ b/system/Router/Router.php @@ -161,6 +161,8 @@ public function __construct(RouteCollectionInterface $routes, ?Request $request if ($this->collection->shouldAutoRoute()) { $autoRoutesImproved = config(Feature::class)->autoRoutesImproved ?? false; if ($autoRoutesImproved) { + assert($this->collection instanceof RouteCollection); + $this->autoRouter = new AutoRouterImproved( $this->collection->getRegisteredControllers('*'), $this->collection->getDefaultNamespace(), From 6615eb7a3d8cada0bf10873f9da9323aaf640779 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 13 Jun 2024 10:40:13 +0900 Subject: [PATCH 055/277] docs: add changelog --- user_guide_src/source/changelogs/v4.6.0.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index 99d7476299f0..77107acab3b7 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -46,9 +46,24 @@ The following breaking changes have been made accordingly: Interface Changes ================= +.. note:: As long as you have not extended the relevant CodeIgniter core classes + or implemented these interfaces, all these changes are backward compatible + and require no intervention. + +- **Router:** The following methods have been added in ``RouteCollectionInterface``: + + - ``getDefaultNamespace()`` + - ``getRoutesOptions()`` + - ``setHTTPVerb()`` + - ``isFiltered()`` + - ``getFiltersForRoute()`` + Method Signature Changes ======================== +- **Router:** The constructor of the ``DefinedRouteCollector`` has been + changed. The ``RouteCollection`` typehint has been changed to ``RouteCollectionInterface``. + .. _v460-removed-deprecated-items: Removed Deprecated Items From 7819f65b3257b7d372eaa6cfea14bd65022722cd Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 13 Jun 2024 10:45:28 +0900 Subject: [PATCH 056/277] docs: add upgrad guide --- user_guide_src/source/changelogs/v4.6.0.rst | 4 ++++ user_guide_src/source/installation/upgrade_460.rst | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index 77107acab3b7..0efdc3b326d7 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -43,6 +43,8 @@ The following breaking changes have been made accordingly: - ``TestException`` now extends ``CodeIgniter\Exceptions\LogicException`` instead of ``CodeIgniter\Exceptions\CriticalError``. +.. _v460-interface-changes: + Interface Changes ================= @@ -58,6 +60,8 @@ Interface Changes - ``isFiltered()`` - ``getFiltersForRoute()`` +.. _v460-method-signature-changes: + Method Signature Changes ======================== diff --git a/user_guide_src/source/installation/upgrade_460.rst b/user_guide_src/source/installation/upgrade_460.rst index 94f69a2322f9..0aa5882e889d 100644 --- a/user_guide_src/source/installation/upgrade_460.rst +++ b/user_guide_src/source/installation/upgrade_460.rst @@ -29,6 +29,20 @@ See :ref:`ChangeLog ` for details. If you have code that catches these exceptions, change the exception classes. +Interface Changes +================= + +Some interface changes have been made. Classes that implement them should update +their APIs to reflect the changes. See :ref:`ChangeLog ` +for details. + +Method Signature Changes +======================== + +Some method signature changes have been made. Classes that extend them should +update their APIs to reflect the changes. See :ref:`ChangeLog ` +for details. + Removed Deprecated Items ======================== From bb1494a42b7b70eba9079e7369126edf6f0992c5 Mon Sep 17 00:00:00 2001 From: christianberkman <39840601+christianberkman@users.noreply.github.com> Date: Thu, 13 Jun 2024 06:15:40 +0300 Subject: [PATCH 057/277] docs: fix typo Co-authored-by: Michal Sniatala --- user_guide_src/source/libraries/files/016.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/libraries/files/016.php b/user_guide_src/source/libraries/files/016.php index caa78e3b4ca3..b40498cf5786 100644 --- a/user_guide_src/source/libraries/files/016.php +++ b/user_guide_src/source/libraries/files/016.php @@ -1,3 +1,3 @@ retainMultiplePatterns(['*.css*', '*.js']); // Would keep only *.css and *.js files +$files->retainMultiplePatterns(['*.css', '*.js']); // Would keep only *.css and *.js files From 6c7f63cc0fd5a36f95107a883b2c9b6343bb1511 Mon Sep 17 00:00:00 2001 From: Christian Berkman Date: Sat, 15 Jun 2024 07:58:14 +0200 Subject: [PATCH 058/277] [FileRules] add min_dims validation rule Signed-off-by: Christian Berkman --- system/Validation/FileRules.php | 47 +++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/system/Validation/FileRules.php b/system/Validation/FileRules.php index 2171858c1582..c046375b0459 100644 --- a/system/Validation/FileRules.php +++ b/system/Validation/FileRules.php @@ -260,4 +260,51 @@ public function max_dims(?string $blank, string $params): bool return true; } + + /** + * Checks an uploaded file to verify that the dimensions are greater than + * a specified dimension. + */ + public function min_dims(?string $blank, string $params): bool + { + // Grab the file name off the top of the $params + // after we split it. + $params = explode(',', $params); + $name = array_shift($params); + + if (! ($files = $this->request->getFileMultiple($name))) { + $files = [$this->request->getFile($name)]; + } + + foreach ($files as $file) { + if ($file === null) { + return false; + } + + if ($file->getError() === UPLOAD_ERR_NO_FILE) { + return true; + } + + // Get Parameter sizes + $minimumWidth = $params[0] ?? 0; + $minimumHeight = $params[1] ?? 0; + + // Get uploaded image size + $info = getimagesize($file->getTempName()); + + if ($info === false) { + // Cannot get the image size. + return false; + } + + $fileWidth = $info[0]; + $fileHeight = $info[1]; + + if ($fileWidth < $minimumWidth || $fileHeight < $minimumHeight) { + return false; + } + } + + return true; + } } From f2e3940c2f0a5a430cbc59bcb762c63b192e953a Mon Sep 17 00:00:00 2001 From: Christian Berkman Date: Sat, 15 Jun 2024 08:00:54 +0200 Subject: [PATCH 059/277] Add tests for FileRules::min_dims() Signed-off-by: Christian Berkman --- tests/system/Validation/FileRulesTest.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/system/Validation/FileRulesTest.php b/tests/system/Validation/FileRulesTest.php index ec016ee0dbeb..421c9c8fad1c 100644 --- a/tests/system/Validation/FileRulesTest.php +++ b/tests/system/Validation/FileRulesTest.php @@ -230,6 +230,24 @@ public function testMaxDimsBad(): void $this->assertFalse($this->validation->run([])); } + public function testMinDims(): void + { + $this->validation->setRules(['avatar' => 'min_dims[avatar,320,240]']); + $this->assertTrue($this->validation->run([])); + } + + public function testMinDimsFail(): void + { + $this->validation->setRules(['avatar' => 'min_dims[avatar,800,600]']); + $this->assertFalse($this->validation->run([])); + } + + public function testMinDimsBad(): void + { + $this->validation->setRules(['avatar' => 'min_dims[unknown,640,480]']); + $this->assertFalse($this->validation->run([])); + } + public function testIsImage(): void { $this->validation->setRules(['avatar' => 'is_image[avatar]']); From 2a2cdfdae5423304696c9ba564666def33ce9f0f Mon Sep 17 00:00:00 2001 From: Christian Berkman Date: Sat, 15 Jun 2024 08:09:45 +0200 Subject: [PATCH 060/277] docs: [FileRules] add min_dims validation rule Signed-off-by: Christian Berkman --- user_guide_src/source/changelogs/v4.6.0.rst | 3 ++- user_guide_src/source/libraries/validation.rst | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index 4749cb4041c4..c98065e32805 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -119,7 +119,7 @@ Forge ----- Others ------- +------ma Model ===== @@ -128,6 +128,7 @@ Libraries ========= - Added `retainMultiplePatterns()` to `FileCollection` class. See :ref:`FileCollection::retainMultiplePatterns() `. +- Added `min_dims` validation rule to `FileRules` class. See :ref`Validation `. Helpers and Functions ===================== diff --git a/user_guide_src/source/libraries/validation.rst b/user_guide_src/source/libraries/validation.rst index 899389d2d004..26abdd64c150 100644 --- a/user_guide_src/source/libraries/validation.rst +++ b/user_guide_src/source/libraries/validation.rst @@ -1082,6 +1082,12 @@ max_dims Yes Fails if the maximum width and height of an the width, and the third is the height. Will also fail if the file cannot be determined to be an image. +min_dims Yes Fails if the maximum width and height of an ``min_dims[field_name,300,150]`` + uploaded image not meet values. The first + parameter is the field name. The second is + the width, and the third is the height. Will + also fail if the file cannot be determined + to be an image. mime_in Yes Fails if the file's mime type is not one ``mime_in[field_name,image/png,image/jpeg]`` listed in the parameters. ext_in Yes Fails if the file's extension is not one ``ext_in[field_name,png,jpg,gif]`` From 490bd4224223b863ba1f37cf4a6216119766bcab Mon Sep 17 00:00:00 2001 From: christianberkman <39840601+christianberkman@users.noreply.github.com> Date: Sat, 15 Jun 2024 09:29:31 +0300 Subject: [PATCH 061/277] docs: fix typo --- user_guide_src/source/changelogs/v4.6.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index c98065e32805..5efd08445411 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -119,7 +119,7 @@ Forge ----- Others -------ma +------ Model ===== From f973e8c179b6d77f80614ef66be09bfec6b7c313 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 15 Jun 2024 10:42:55 +0900 Subject: [PATCH 062/277] fix!: View::renderSection() return type --- system/View/View.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/system/View/View.php b/system/View/View.php index 57b2333d901d..b54472ed0921 100644 --- a/system/View/View.php +++ b/system/View/View.php @@ -457,23 +457,23 @@ public function endSection() * * @param bool $saveData If true, saves data for subsequent calls, * if false, cleans the data after displaying. - * - * @return void */ - public function renderSection(string $sectionName, bool $saveData = false) + public function renderSection(string $sectionName, bool $saveData = false): string { if (! isset($this->sections[$sectionName])) { - echo ''; - - return; + return ''; } + $output = ''; + foreach ($this->sections[$sectionName] as $key => $contents) { - echo $contents; + $output .= $contents; if ($saveData === false) { unset($this->sections[$sectionName][$key]); } } + + return $output; } /** From bff08417a4eed203dcdf7917a375586fdf917f26 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 15 Jun 2024 10:50:34 +0900 Subject: [PATCH 063/277] docs: add changelog --- user_guide_src/source/changelogs/v4.6.0.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index 4749cb4041c4..4af821e4aa1f 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -67,6 +67,8 @@ Method Signature Changes - **Router:** The constructor of the ``DefinedRouteCollector`` has been changed. The ``RouteCollection`` typehint has been changed to ``RouteCollectionInterface``. +- **View:** The return type of the ``renderSection()`` method has been + changed to ``string``, and now the method does not call ``echo``. .. _v460-removed-deprecated-items: From 7602f3dbce14648f97a3bae6a3c68fb8c202343a Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 17 Jun 2024 11:27:35 +0900 Subject: [PATCH 064/277] test: add test for renderSection() --- tests/system/View/ViewTest.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/system/View/ViewTest.php b/tests/system/View/ViewTest.php index 98ce11245a4d..08ba4311616c 100644 --- a/tests/system/View/ViewTest.php +++ b/tests/system/View/ViewTest.php @@ -390,6 +390,13 @@ public function testRenderNestedSections(): void $this->assertStringContainsString('

Third

', $content); } + public function testRenderSectionReturnsEmptyStringToNonExistentSection(): void + { + $view = new View($this->config, $this->viewsDir, $this->loader); + + $this->assertSame('', $view->renderSection('does_not_exist')); + } + public function testRenderSectionSavingData(): void { $view = new View($this->config, $this->viewsDir, $this->loader); From bfb42ea8bf4b13656cf30b6e35911f8bbd3376a6 Mon Sep 17 00:00:00 2001 From: Christian Berkman Date: Wed, 19 Jun 2024 22:24:27 +0200 Subject: [PATCH 065/277] docs: fix type and add version Signed-off-by: Christian Berkman --- user_guide_src/source/libraries/validation.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/libraries/validation.rst b/user_guide_src/source/libraries/validation.rst index 26abdd64c150..61107b8b5ff6 100644 --- a/user_guide_src/source/libraries/validation.rst +++ b/user_guide_src/source/libraries/validation.rst @@ -1082,12 +1082,13 @@ max_dims Yes Fails if the maximum width and height of an the width, and the third is the height. Will also fail if the file cannot be determined to be an image. -min_dims Yes Fails if the maximum width and height of an ``min_dims[field_name,300,150]`` +min_dims Yes Fails if the minimum width and height of an ``min_dims[field_name,300,150]`` uploaded image not meet values. The first parameter is the field name. The second is the width, and the third is the height. Will also fail if the file cannot be determined - to be an image. + to be an image. (This rule was added in + v4.6.0.) mime_in Yes Fails if the file's mime type is not one ``mime_in[field_name,image/png,image/jpeg]`` listed in the parameters. ext_in Yes Fails if the file's extension is not one ``ext_in[field_name,png,jpg,gif]`` From fcbf7dd13e1d40b133f32981ce3c0ef820c572be Mon Sep 17 00:00:00 2001 From: Christian Berkman Date: Wed, 19 Jun 2024 22:36:23 +0200 Subject: [PATCH 066/277] rewrie if-ste if-statement Signed-off-by: Christian Berkman --- system/Validation/FileRules.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/system/Validation/FileRules.php b/system/Validation/FileRules.php index c046375b0459..4f846e8ed4fd 100644 --- a/system/Validation/FileRules.php +++ b/system/Validation/FileRules.php @@ -272,7 +272,8 @@ public function min_dims(?string $blank, string $params): bool $params = explode(',', $params); $name = array_shift($params); - if (! ($files = $this->request->getFileMultiple($name))) { + $files = $this->request->getFileMultiple($name); + if ($files === null) { $files = [$this->request->getFile($name)]; } From eec899fd14a5fde8c221b227d635bef7e754e4c8 Mon Sep 17 00:00:00 2001 From: Christian Berkman Date: Wed, 19 Jun 2024 22:40:45 +0200 Subject: [PATCH 067/277] Add tests for min_dims in StrictRules/FileRules Signed-off-by: Christian Berkman --- .../Validation/StrictRules/FileRulesTest.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/system/Validation/StrictRules/FileRulesTest.php b/tests/system/Validation/StrictRules/FileRulesTest.php index ab31b5cff06b..d9d980bad819 100644 --- a/tests/system/Validation/StrictRules/FileRulesTest.php +++ b/tests/system/Validation/StrictRules/FileRulesTest.php @@ -231,6 +231,24 @@ public function testMaxDimsBad(): void $this->assertFalse($this->validation->run([])); } + public function testMinDims(): void + { + $this->validation->setRules(['avatar' => 'min_dims[avatar,320,240]']); + $this->assertTrue($this->validation->run([])); + } + + public function testMinDimsFail(): void + { + $this->validation->setRules(['avatar' => 'min_dims[avatar,800,600]']); + $this->assertFalse($this->validation->run([])); + } + + public function testMinDimsBad(): void + { + $this->validation->setRules(['avatar' => 'min_dims[unknown,640,480]']); + $this->assertFalse($this->validation->run([])); + } + public function testIsImage(): void { $this->validation->setRules(['avatar' => 'is_image[avatar]']); From 967551970d1a218a78af52a2cb5a08241074863e Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 20 Jun 2024 09:19:53 +0900 Subject: [PATCH 068/277] test: refactor FileRulesTest --- tests/system/Validation/FileRulesTest.php | 282 +----------------- .../Validation/StrictRules/FileRulesTest.php | 8 +- 2 files changed, 9 insertions(+), 281 deletions(-) diff --git a/tests/system/Validation/FileRulesTest.php b/tests/system/Validation/FileRulesTest.php index 421c9c8fad1c..f19552463c2d 100644 --- a/tests/system/Validation/FileRulesTest.php +++ b/tests/system/Validation/FileRulesTest.php @@ -13,9 +13,7 @@ namespace CodeIgniter\Validation; -use CodeIgniter\Exceptions\InvalidArgumentException; -use CodeIgniter\Test\CIUnitTestCase; -use Config\Services; +use CodeIgniter\Validation\StrictRules\FileRulesTest as StrictFileRulesTest; use PHPUnit\Framework\Attributes\Group; use Tests\Support\Validation\TestRules; @@ -23,10 +21,10 @@ * @internal */ #[Group('Others')] -final class FileRulesTest extends CIUnitTestCase +final class FileRulesTest extends StrictFileRulesTest { - private Validation $validation; - private array $config = [ + protected Validation $validation; + protected array $config = [ 'ruleSets' => [ Rules::class, FormatRules::class, @@ -43,276 +41,4 @@ final class FileRulesTest extends CIUnitTestCase ], ], ]; - - protected function setUp(): void - { - $this->resetServices(); - parent::setUp(); - - $this->validation = new Validation((object) $this->config, Services::renderer()); - $this->validation->reset(); - - $_FILES = [ - 'avatar' => [ - 'tmp_name' => TESTPATH . '_support/Validation/uploads/phpUxc0ty', - 'name' => 'my-avatar.png', - 'size' => 4614, - 'type' => 'image/png', - 'error' => 0, - 'width' => 640, - 'height' => 400, - ], - 'bigfile' => [ - 'tmp_name' => TESTPATH . '_support/Validation/uploads/phpUxc0ty', - 'name' => 'my-big-file.png', - 'size' => 1_024_000, - 'type' => 'image/png', - 'error' => UPLOAD_ERR_OK, - 'width' => 640, - 'height' => 400, - ], - 'photo' => [ - 'tmp_name' => TESTPATH . '_support/Validation/uploads/phpUxc0ty', - 'name' => 'my-photo.png', - 'size' => 4614, - 'type' => 'image/png', - 'error' => UPLOAD_ERR_INI_SIZE, - 'width' => 640, - 'height' => 400, - ], - 'images' => [ - 'tmp_name' => [ - TESTPATH . '_support/Validation/uploads/phpUxc0ty', - TESTPATH . '_support/Validation/uploads/phpUxc0ty', - ], - 'name' => [ - 'my_avatar.png', - 'my_bigfile.png', - ], - 'size' => [ - 4614, - 1_024_000, - ], - 'type' => [ - 'image/png', - 'image/png', - ], - 'error' => [ - UPLOAD_ERR_OK, - UPLOAD_ERR_OK, - ], - 'width' => [ - 640, - 640, - ], - 'height' => [ - 400, - 400, - ], - ], - 'photos' => [ - 'tmp_name' => [ - TESTPATH . '_support/Validation/uploads/phpUxc0ty', - TESTPATH . '_support/Validation/uploads/phpUxc0ty', - ], - 'name' => [ - 'my_avatar.png', - 'my_bigfile.png', - ], - 'size' => [ - 4614, - 1_024_000, - ], - 'type' => [ - 'image/png', - 'image/png', - ], - 'error' => [ - UPLOAD_ERR_INI_SIZE, - UPLOAD_ERR_OK, - ], - 'width' => [ - 640, - 640, - ], - 'height' => [ - 400, - 400, - ], - ], - ]; - } - - public function testUploadedTrue(): void - { - $this->validation->setRules(['avatar' => 'uploaded[avatar]']); - $this->assertTrue($this->validation->run([])); - } - - public function testUploadedFalse(): void - { - $this->validation->setRules(['avatar' => 'uploaded[userfile]']); - $this->assertFalse($this->validation->run([])); - } - - public function testUploadedArrayReturnsTrue(): void - { - $this->validation->setRules(['images' => 'uploaded[images]']); - $this->assertTrue($this->validation->run([])); - } - - public function testUploadedArrayReturnsFalse(): void - { - $this->validation->setRules(['photos' => 'uploaded[photos]']); - $this->assertFalse($this->validation->run([])); - } - - public function testMaxSize(): void - { - $this->validation->setRules(['avatar' => 'max_size[avatar,100]']); - $this->assertTrue($this->validation->run([])); - } - - public function testMaxSizeBigFile(): void - { - $this->validation->setRules(['bigfile' => 'max_size[bigfile,9999]']); - $this->assertTrue($this->validation->run([])); - } - - public function testMaxSizeFail(): void - { - $this->validation->setRules(['avatar' => 'max_size[avatar,4]']); - $this->assertFalse($this->validation->run([])); - } - - public function testMaxSizeFailDueToUploadMaxFilesizeExceededInPhpIni(): void - { - $this->validation->setRules(['photo' => 'max_size[photo,100]']); - $this->assertFalse($this->validation->run([])); - } - - public function testMaxSizeBigFileFail(): void - { - $this->validation->setRules(['bigfile' => 'max_size[bigfile,10]']); - $this->assertFalse($this->validation->run([])); - } - - public function testMaxSizeBad(): void - { - $this->validation->setRules(['avatar' => 'max_size[userfile,50]']); - $this->assertFalse($this->validation->run([])); - } - - public function testMaxSizeInvalidParam(): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Invalid max_size parameter: "avatar.100"'); - - $this->validation->setRules(['avatar' => 'max_size[avatar.100]']); - $this->validation->run([]); - } - - public function testMaxDims(): void - { - $this->validation->setRules(['avatar' => 'max_dims[avatar,640,480]']); - $this->assertTrue($this->validation->run([])); - } - - public function testMaxDimsFail(): void - { - $this->validation->setRules(['avatar' => 'max_dims[avatar,600,480]']); - $this->assertFalse($this->validation->run([])); - } - - public function testMaxDimsBad(): void - { - $this->validation->setRules(['avatar' => 'max_dims[unknown,640,480]']); - $this->assertFalse($this->validation->run([])); - } - - public function testMinDims(): void - { - $this->validation->setRules(['avatar' => 'min_dims[avatar,320,240]']); - $this->assertTrue($this->validation->run([])); - } - - public function testMinDimsFail(): void - { - $this->validation->setRules(['avatar' => 'min_dims[avatar,800,600]']); - $this->assertFalse($this->validation->run([])); - } - - public function testMinDimsBad(): void - { - $this->validation->setRules(['avatar' => 'min_dims[unknown,640,480]']); - $this->assertFalse($this->validation->run([])); - } - - public function testIsImage(): void - { - $this->validation->setRules(['avatar' => 'is_image[avatar]']); - $this->assertTrue($this->validation->run([])); - } - - public function testIsntImage(): void - { - $_FILES['stuff'] = [ - 'tmp_name' => TESTPATH . '_support/Validation/uploads/abc77tz', - 'name' => 'address.book', - 'size' => 12345, - 'type' => 'application/address', - 'error' => UPLOAD_ERR_OK, - ]; - - $this->validation->setRules(['avatar' => 'is_image[stuff]']); - $this->assertFalse($this->validation->run([])); - } - - public function testAlsoIsntImage(): void - { - $this->validation->setRules(['avatar' => 'is_image[unknown]']); - $this->assertFalse($this->validation->run([])); - } - - public function testMimeTypeOk(): void - { - $this->validation->setRules([ - 'avatar' => 'mime_in[avatar,image/jpg,image/jpeg,image/gif,image/png]', - ]); - $this->assertTrue($this->validation->run([])); - } - - public function testMimeTypeNotOk(): void - { - $this->validation->setRules([ - 'avatar' => 'mime_in[avatar,application/xls,application/doc,application/ppt]', - ]); - $this->assertFalse($this->validation->run([])); - } - - public function testMimeTypeImpossible(): void - { - $this->validation->setRules([ - 'avatar' => 'mime_in[unknown,application/xls,application/doc,application/ppt]', - ]); - $this->assertFalse($this->validation->run([])); - } - - public function testExtensionOk(): void - { - $this->validation->setRules(['avatar' => 'ext_in[avatar,jpg,jpeg,gif,png]']); - $this->assertTrue($this->validation->run([])); - } - - public function testExtensionNotOk(): void - { - $this->validation->setRules(['avatar' => 'ext_in[avatar,xls,doc,ppt]']); - $this->assertFalse($this->validation->run([])); - } - - public function testExtensionImpossible(): void - { - $this->validation->setRules(['avatar' => 'ext_in[unknown,xls,doc,ppt]']); - $this->assertFalse($this->validation->run([])); - } } diff --git a/tests/system/Validation/StrictRules/FileRulesTest.php b/tests/system/Validation/StrictRules/FileRulesTest.php index d9d980bad819..fd851acd5a24 100644 --- a/tests/system/Validation/StrictRules/FileRulesTest.php +++ b/tests/system/Validation/StrictRules/FileRulesTest.php @@ -22,12 +22,14 @@ /** * @internal + * + * @no-final */ #[Group('Others')] -final class FileRulesTest extends CIUnitTestCase +class FileRulesTest extends CIUnitTestCase { - private Validation $validation; - private array $config = [ + protected Validation $validation; + protected array $config = [ 'ruleSets' => [ Rules::class, FormatRules::class, From 59f1e29bee5bfc699cd766cb046d7b43a98f1805 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 20 Jun 2024 09:31:56 +0900 Subject: [PATCH 069/277] test: refactor CreditCardRulesTest --- .../system/Validation/CreditCardRulesTest.php | 1209 +---------------- .../StrictRules/CreditCardRulesTest.php | 12 +- 2 files changed, 11 insertions(+), 1210 deletions(-) diff --git a/tests/system/Validation/CreditCardRulesTest.php b/tests/system/Validation/CreditCardRulesTest.php index 9e6b58cdc192..b6a0b35be294 100644 --- a/tests/system/Validation/CreditCardRulesTest.php +++ b/tests/system/Validation/CreditCardRulesTest.php @@ -13,9 +13,7 @@ namespace CodeIgniter\Validation; -use CodeIgniter\Test\CIUnitTestCase; -use Config\Services; -use PHPUnit\Framework\Attributes\DataProvider; +use CodeIgniter\Validation\StrictRules\CreditCardRulesTest as StrictCreditCardRulesTest; use PHPUnit\Framework\Attributes\Group; use Tests\Support\Validation\TestRules; @@ -23,10 +21,10 @@ * @internal */ #[Group('Others')] -final class CreditCardRulesTest extends CIUnitTestCase +final class CreditCardRulesTest extends StrictCreditCardRulesTest { - private Validation $validation; - private array $config = [ + protected Validation $validation; + protected array $config = [ 'ruleSets' => [ Rules::class, FormatRules::class, @@ -43,1203 +41,4 @@ final class CreditCardRulesTest extends CIUnitTestCase ], ], ]; - - protected function setUp(): void - { - parent::setUp(); - $this->validation = new Validation((object) $this->config, Services::renderer()); - $this->validation->reset(); - } - - #[DataProvider('provideValidCCNumber')] - public function testValidCCNumber(string $type, ?string $number, bool $expected): void - { - $data = ['cc' => $number]; - $this->validation->setRules(['cc' => "valid_cc_number[{$type}]"]); - $this->assertSame($expected, $this->validation->run($data)); - } - - /** - * Cards shown are test cards found around the web. - * - * @see https://www.paypalobjects.com/en_US/vhelp/paypalmanager_help/credit_card_numbers.htm - */ - public static function provideValidCCNumber(): iterable - { - yield from [ - 'null_test' => [ - 'amex', - null, - false, - ], - 'random_test' => [ - 'amex', - self::generateCardNumber(37, 16), - false, - ], - 'invalid_type' => [ - 'shorty', - '1111 1111 1111 1111', - false, - ], - 'invalid_length' => [ - 'amex', - '', - false, - ], - 'not_numeric' => [ - 'amex', - 'abcd efgh ijkl mnop', - false, - ], - 'bad_length' => [ - 'amex', - '3782 8224 6310 0051', - false, - ], - 'bad_prefix' => [ - 'amex', - '3582 8224 6310 0051', - false, - ], - 'amex1' => [ - 'amex', - '3782 8224 6310 005', - true, - ], - 'amex2' => [ - 'amex', - '3714 4963 5398 431', - true, - ], - 'dinersclub1' => [ - 'dinersclub', - '3056 9309 0259 04', - true, - ], - 'dinersculb2' => [ - 'dinersclub', - '3852 0000 0232 37', - true, - ], - 'discover1' => [ - 'discover', - '6011 1111 1111 1117', - true, - ], - 'discover2' => [ - 'discover', - '6011 0009 9013 9424', - true, - ], - 'jcb8' => [ - 'jcb', - '3530 1113 3330 0000', - true, - ], - 'jcb9' => [ - 'jcb', - '3566 0020 2036 0505', - true, - ], - 'mastercard12' => [ - 'mastercard', - '5555 5555 5555 4444', - true, - ], - 'mastercard13' => [ - 'mastercard', - '5105 1051 0510 5100', - true, - ], - 'visa4' => [ - 'visa', - '4111 1111 1111 1111', - true, - ], - 'visa5' => [ - 'visa', - '4012 8888 8888 1881', - true, - ], - 'visa6' => [ - 'visa', - '4222 2222 2222 2', - true, - ], - 'dankort5' => [ - 'dankort', - '5019 7170 1010 3742', - true, - ], - 'unionpay1' => [ - 'unionpay', - self::generateCardNumber(62, 16), - true, - ], - 'unionpay2' => [ - 'unionpay', - self::generateCardNumber(62, 17), - true, - ], - 'unionpay3' => [ - 'unionpay', - self::generateCardNumber(62, 18), - true, - ], - 'unionpay4' => [ - 'unionpay', - self::generateCardNumber(62, 19), - true, - ], - 'unionpay5' => [ - 'unionpay', - self::generateCardNumber(63, 19), - false, - ], - 'carteblanche1' => [ - 'carteblanche', - self::generateCardNumber(300, 14), - true, - ], - 'carteblanche2' => [ - 'carteblanche', - self::generateCardNumber(301, 14), - true, - ], - 'carteblanche3' => [ - 'carteblanche', - self::generateCardNumber(302, 14), - true, - ], - 'carteblanche4' => [ - 'carteblanche', - self::generateCardNumber(303, 14), - true, - ], - 'carteblanche5' => [ - 'carteblanche', - self::generateCardNumber(304, 14), - true, - ], - 'carteblanche6' => [ - 'carteblanche', - self::generateCardNumber(305, 14), - true, - ], - 'carteblanche7' => [ - 'carteblanche', - self::generateCardNumber(306, 14), - false, - ], - 'dinersclub3' => [ - 'dinersclub', - self::generateCardNumber(300, 14), - true, - ], - 'dinersclub4' => [ - 'dinersclub', - self::generateCardNumber(301, 14), - true, - ], - 'dinersclub5' => [ - 'dinersclub', - self::generateCardNumber(302, 14), - true, - ], - 'dinersclub6' => [ - 'dinersclub', - self::generateCardNumber(303, 14), - true, - ], - 'dinersclub7' => [ - 'dinersclub', - self::generateCardNumber(304, 14), - true, - ], - 'dinersclub8' => [ - 'dinersclub', - self::generateCardNumber(305, 14), - true, - ], - 'dinersclub9' => [ - 'dinersclub', - self::generateCardNumber(309, 14), - true, - ], - 'dinersclub10' => [ - 'dinersclub', - self::generateCardNumber(36, 14), - true, - ], - 'dinersclub11' => [ - 'dinersclub', - self::generateCardNumber(38, 14), - true, - ], - 'dinersclub12' => [ - 'dinersclub', - self::generateCardNumber(39, 14), - true, - ], - 'dinersclub13' => [ - 'dinersclub', - self::generateCardNumber(54, 14), - true, - ], - 'dinersclub14' => [ - 'dinersclub', - self::generateCardNumber(55, 14), - true, - ], - 'dinersclub15' => [ - 'dinersclub', - self::generateCardNumber(300, 16), - true, - ], - 'dinersclub16' => [ - 'dinersclub', - self::generateCardNumber(301, 16), - true, - ], - 'dinersclub17' => [ - 'dinersclub', - self::generateCardNumber(302, 16), - true, - ], - 'dinersclub18' => [ - 'dinersclub', - self::generateCardNumber(303, 16), - true, - ], - 'dinersclub19' => [ - 'dinersclub', - self::generateCardNumber(304, 16), - true, - ], - 'dinersclub20' => [ - 'dinersclub', - self::generateCardNumber(305, 16), - true, - ], - 'dinersclub21' => [ - 'dinersclub', - self::generateCardNumber(309, 16), - true, - ], - 'dinersclub22' => [ - 'dinersclub', - self::generateCardNumber(36, 16), - true, - ], - 'dinersclub23' => [ - 'dinersclub', - self::generateCardNumber(38, 16), - true, - ], - 'dinersclub24' => [ - 'dinersclub', - self::generateCardNumber(39, 16), - true, - ], - 'dinersclub25' => [ - 'dinersclub', - self::generateCardNumber(54, 16), - true, - ], - 'dinersclub26' => [ - 'dinersclub', - self::generateCardNumber(55, 16), - true, - ], - 'discover3' => [ - 'discover', - self::generateCardNumber(6011, 16), - true, - ], - 'discover4' => [ - 'discover', - self::generateCardNumber(622, 16), - true, - ], - 'discover5' => [ - 'discover', - self::generateCardNumber(644, 16), - true, - ], - 'discover6' => [ - 'discover', - self::generateCardNumber(645, 16), - true, - ], - 'discover7' => [ - 'discover', - self::generateCardNumber(656, 16), - true, - ], - 'discover8' => [ - 'discover', - self::generateCardNumber(647, 16), - true, - ], - 'discover9' => [ - 'discover', - self::generateCardNumber(648, 16), - true, - ], - 'discover10' => [ - 'discover', - self::generateCardNumber(649, 16), - true, - ], - 'discover11' => [ - 'discover', - self::generateCardNumber(65, 16), - true, - ], - 'discover12' => [ - 'discover', - self::generateCardNumber(6011, 19), - true, - ], - 'discover13' => [ - 'discover', - self::generateCardNumber(622, 19), - true, - ], - 'discover14' => [ - 'discover', - self::generateCardNumber(644, 19), - true, - ], - 'discover15' => [ - 'discover', - self::generateCardNumber(645, 19), - true, - ], - 'discover16' => [ - 'discover', - self::generateCardNumber(656, 19), - true, - ], - 'discover17' => [ - 'discover', - self::generateCardNumber(647, 19), - true, - ], - 'discover18' => [ - 'discover', - self::generateCardNumber(648, 19), - true, - ], - 'discover19' => [ - 'discover', - self::generateCardNumber(649, 19), - true, - ], - 'discover20' => [ - 'discover', - self::generateCardNumber(65, 19), - true, - ], - 'interpayment1' => [ - 'interpayment', - self::generateCardNumber(4, 16), - true, - ], - 'interpayment2' => [ - 'interpayment', - self::generateCardNumber(4, 17), - true, - ], - 'interpayment3' => [ - 'interpayment', - self::generateCardNumber(4, 18), - true, - ], - 'interpayment4' => [ - 'interpayment', - self::generateCardNumber(4, 19), - true, - ], - 'jcb1' => [ - 'jcb', - self::generateCardNumber(352, 16), - true, - ], - 'jcb2' => [ - 'jcb', - self::generateCardNumber(353, 16), - true, - ], - 'jcb3' => [ - 'jcb', - self::generateCardNumber(354, 16), - true, - ], - 'jcb4' => [ - 'jcb', - self::generateCardNumber(355, 16), - true, - ], - 'jcb5' => [ - 'jcb', - self::generateCardNumber(356, 16), - true, - ], - 'jcb6' => [ - 'jcb', - self::generateCardNumber(357, 16), - true, - ], - 'jcb7' => [ - 'jcb', - self::generateCardNumber(358, 16), - true, - ], - 'maestro1' => [ - 'maestro', - self::generateCardNumber(50, 12), - true, - ], - 'maestro2' => [ - 'maestro', - self::generateCardNumber(56, 12), - true, - ], - 'maestro3' => [ - 'maestro', - self::generateCardNumber(57, 12), - true, - ], - 'maestro4' => [ - 'maestro', - self::generateCardNumber(58, 12), - true, - ], - 'maestro5' => [ - 'maestro', - self::generateCardNumber(59, 12), - true, - ], - 'maestro6' => [ - 'maestro', - self::generateCardNumber(60, 12), - true, - ], - 'maestro7' => [ - 'maestro', - self::generateCardNumber(61, 12), - true, - ], - 'maestro8' => [ - 'maestro', - self::generateCardNumber(62, 12), - true, - ], - 'maestro9' => [ - 'maestro', - self::generateCardNumber(63, 12), - true, - ], - 'maestro10' => [ - 'maestro', - self::generateCardNumber(64, 12), - true, - ], - 'maestro11' => [ - 'maestro', - self::generateCardNumber(65, 12), - true, - ], - 'maestro12' => [ - 'maestro', - self::generateCardNumber(66, 12), - true, - ], - 'maestro13' => [ - 'maestro', - self::generateCardNumber(67, 12), - true, - ], - 'maestro14' => [ - 'maestro', - self::generateCardNumber(68, 12), - true, - ], - 'maestro15' => [ - 'maestro', - self::generateCardNumber(69, 12), - true, - ], - 'maestro16' => [ - 'maestro', - self::generateCardNumber(50, 13), - true, - ], - 'maestro17' => [ - 'maestro', - self::generateCardNumber(56, 13), - true, - ], - 'maestro18' => [ - 'maestro', - self::generateCardNumber(57, 13), - true, - ], - 'maestro19' => [ - 'maestro', - self::generateCardNumber(58, 13), - true, - ], - 'maestro20' => [ - 'maestro', - self::generateCardNumber(59, 13), - true, - ], - 'maestro21' => [ - 'maestro', - self::generateCardNumber(60, 13), - true, - ], - 'maestro22' => [ - 'maestro', - self::generateCardNumber(61, 13), - true, - ], - 'maestro23' => [ - 'maestro', - self::generateCardNumber(62, 13), - true, - ], - 'maestro24' => [ - 'maestro', - self::generateCardNumber(63, 13), - true, - ], - 'maestro25' => [ - 'maestro', - self::generateCardNumber(64, 13), - true, - ], - 'maestro26' => [ - 'maestro', - self::generateCardNumber(65, 13), - true, - ], - 'maestro27' => [ - 'maestro', - self::generateCardNumber(66, 13), - true, - ], - 'maestro28' => [ - 'maestro', - self::generateCardNumber(67, 13), - true, - ], - 'maestro29' => [ - 'maestro', - self::generateCardNumber(68, 13), - true, - ], - 'maestro30' => [ - 'maestro', - self::generateCardNumber(69, 13), - true, - ], - 'maestro31' => [ - 'maestro', - self::generateCardNumber(50, 14), - true, - ], - 'maestro32' => [ - 'maestro', - self::generateCardNumber(56, 14), - true, - ], - 'maestro33' => [ - 'maestro', - self::generateCardNumber(57, 14), - true, - ], - 'maestro34' => [ - 'maestro', - self::generateCardNumber(58, 14), - true, - ], - 'maestro35' => [ - 'maestro', - self::generateCardNumber(59, 14), - true, - ], - 'maestro36' => [ - 'maestro', - self::generateCardNumber(60, 14), - true, - ], - 'maestro37' => [ - 'maestro', - self::generateCardNumber(61, 14), - true, - ], - 'maestro38' => [ - 'maestro', - self::generateCardNumber(62, 14), - true, - ], - 'maestro39' => [ - 'maestro', - self::generateCardNumber(63, 14), - true, - ], - 'maestro40' => [ - 'maestro', - self::generateCardNumber(64, 14), - true, - ], - 'maestro41' => [ - 'maestro', - self::generateCardNumber(65, 14), - true, - ], - 'maestro42' => [ - 'maestro', - self::generateCardNumber(66, 14), - true, - ], - 'maestro43' => [ - 'maestro', - self::generateCardNumber(67, 14), - true, - ], - 'maestro44' => [ - 'maestro', - self::generateCardNumber(68, 14), - true, - ], - 'maestro45' => [ - 'maestro', - self::generateCardNumber(69, 14), - true, - ], - 'maestro46' => [ - 'maestro', - self::generateCardNumber(50, 15), - true, - ], - 'maestro47' => [ - 'maestro', - self::generateCardNumber(56, 15), - true, - ], - 'maestro48' => [ - 'maestro', - self::generateCardNumber(57, 15), - true, - ], - 'maestro49' => [ - 'maestro', - self::generateCardNumber(58, 15), - true, - ], - 'maestro50' => [ - 'maestro', - self::generateCardNumber(59, 15), - true, - ], - 'maestro51' => [ - 'maestro', - self::generateCardNumber(60, 15), - true, - ], - 'maestro52' => [ - 'maestro', - self::generateCardNumber(61, 15), - true, - ], - 'maestro53' => [ - 'maestro', - self::generateCardNumber(62, 15), - true, - ], - 'maestro54' => [ - 'maestro', - self::generateCardNumber(63, 15), - true, - ], - 'maestro55' => [ - 'maestro', - self::generateCardNumber(64, 15), - true, - ], - 'maestro56' => [ - 'maestro', - self::generateCardNumber(65, 15), - true, - ], - 'maestro57' => [ - 'maestro', - self::generateCardNumber(66, 15), - true, - ], - 'maestro58' => [ - 'maestro', - self::generateCardNumber(67, 15), - true, - ], - 'maestro59' => [ - 'maestro', - self::generateCardNumber(68, 15), - true, - ], - 'maestro60' => [ - 'maestro', - self::generateCardNumber(69, 15), - true, - ], - 'maestro61' => [ - 'maestro', - self::generateCardNumber(50, 16), - true, - ], - 'maestro62' => [ - 'maestro', - self::generateCardNumber(56, 16), - true, - ], - 'maestro63' => [ - 'maestro', - self::generateCardNumber(57, 16), - true, - ], - 'maestro64' => [ - 'maestro', - self::generateCardNumber(58, 16), - true, - ], - 'maestro65' => [ - 'maestro', - self::generateCardNumber(59, 16), - true, - ], - 'maestro66' => [ - 'maestro', - self::generateCardNumber(60, 16), - true, - ], - 'maestro67' => [ - 'maestro', - self::generateCardNumber(61, 16), - true, - ], - 'maestro68' => [ - 'maestro', - self::generateCardNumber(62, 16), - true, - ], - 'maestro69' => [ - 'maestro', - self::generateCardNumber(63, 16), - true, - ], - 'maestro70' => [ - 'maestro', - self::generateCardNumber(64, 16), - true, - ], - 'maestro71' => [ - 'maestro', - self::generateCardNumber(65, 16), - true, - ], - 'maestro72' => [ - 'maestro', - self::generateCardNumber(66, 16), - true, - ], - 'maestro73' => [ - 'maestro', - self::generateCardNumber(67, 16), - true, - ], - 'maestro74' => [ - 'maestro', - self::generateCardNumber(68, 16), - true, - ], - 'maestro75' => [ - 'maestro', - self::generateCardNumber(69, 16), - true, - ], - 'maestro91' => [ - 'maestro', - self::generateCardNumber(50, 18), - true, - ], - 'maestro92' => [ - 'maestro', - self::generateCardNumber(56, 18), - true, - ], - 'maestro93' => [ - 'maestro', - self::generateCardNumber(57, 18), - true, - ], - 'maestro94' => [ - 'maestro', - self::generateCardNumber(58, 18), - true, - ], - 'maestro95' => [ - 'maestro', - self::generateCardNumber(59, 18), - true, - ], - 'maestro96' => [ - 'maestro', - self::generateCardNumber(60, 18), - true, - ], - 'maestro97' => [ - 'maestro', - self::generateCardNumber(61, 18), - true, - ], - 'maestro98' => [ - 'maestro', - self::generateCardNumber(62, 18), - true, - ], - 'maestro99' => [ - 'maestro', - self::generateCardNumber(63, 18), - true, - ], - 'maestro100' => [ - 'maestro', - self::generateCardNumber(64, 18), - true, - ], - 'maestro101' => [ - 'maestro', - self::generateCardNumber(65, 18), - true, - ], - 'maestro102' => [ - 'maestro', - self::generateCardNumber(66, 18), - true, - ], - 'maestro103' => [ - 'maestro', - self::generateCardNumber(67, 18), - true, - ], - 'maestro104' => [ - 'maestro', - self::generateCardNumber(68, 18), - true, - ], - 'maestro105' => [ - 'maestro', - self::generateCardNumber(69, 18), - true, - ], - 'maestro106' => [ - 'maestro', - self::generateCardNumber(50, 19), - true, - ], - 'maestro107' => [ - 'maestro', - self::generateCardNumber(56, 19), - true, - ], - 'maestro108' => [ - 'maestro', - self::generateCardNumber(57, 19), - true, - ], - 'maestro109' => [ - 'maestro', - self::generateCardNumber(58, 19), - true, - ], - 'maestro110' => [ - 'maestro', - self::generateCardNumber(59, 19), - true, - ], - 'maestro111' => [ - 'maestro', - self::generateCardNumber(60, 19), - true, - ], - 'maestro112' => [ - 'maestro', - self::generateCardNumber(61, 19), - true, - ], - 'maestro113' => [ - 'maestro', - self::generateCardNumber(62, 19), - true, - ], - 'maestro114' => [ - 'maestro', - self::generateCardNumber(63, 19), - true, - ], - 'maestro115' => [ - 'maestro', - self::generateCardNumber(64, 19), - true, - ], - 'maestro116' => [ - 'maestro', - self::generateCardNumber(65, 19), - true, - ], - 'maestro117' => [ - 'maestro', - self::generateCardNumber(66, 19), - true, - ], - 'maestro118' => [ - 'maestro', - self::generateCardNumber(67, 19), - true, - ], - 'maestro119' => [ - 'maestro', - self::generateCardNumber(68, 19), - true, - ], - 'maestro120' => [ - 'maestro', - self::generateCardNumber(69, 19), - true, - ], - 'dankort1' => [ - 'dankort', - self::generateCardNumber(5019, 16), - true, - ], - 'dankort2' => [ - 'dankort', - self::generateCardNumber(4175, 16), - true, - ], - 'dankort3' => [ - 'dankort', - self::generateCardNumber(4571, 16), - true, - ], - 'dankort4' => [ - 'dankort', - self::generateCardNumber(4, 16), - true, - ], - 'mir1' => [ - 'mir', - self::generateCardNumber(2200, 16), - true, - ], - 'mir2' => [ - 'mir', - self::generateCardNumber(2201, 16), - true, - ], - 'mir3' => [ - 'mir', - self::generateCardNumber(2202, 16), - true, - ], - 'mir4' => [ - 'mir', - self::generateCardNumber(2203, 16), - true, - ], - 'mir5' => [ - 'mir', - self::generateCardNumber(2204, 16), - true, - ], - 'mastercard1' => [ - 'mastercard', - self::generateCardNumber(51, 16), - true, - ], - 'mastercard2' => [ - 'mastercard', - self::generateCardNumber(52, 16), - true, - ], - 'mastercard3' => [ - 'mastercard', - self::generateCardNumber(53, 16), - true, - ], - 'mastercard4' => [ - 'mastercard', - self::generateCardNumber(54, 16), - true, - ], - 'mastercard5' => [ - 'mastercard', - self::generateCardNumber(55, 16), - true, - ], - 'mastercard6' => [ - 'mastercard', - self::generateCardNumber(22, 16), - true, - ], - 'mastercard7' => [ - 'mastercard', - self::generateCardNumber(23, 16), - true, - ], - 'mastercard8' => [ - 'mastercard', - self::generateCardNumber(24, 16), - true, - ], - 'mastercard9' => [ - 'mastercard', - self::generateCardNumber(25, 16), - true, - ], - 'mastercard10' => [ - 'mastercard', - self::generateCardNumber(26, 16), - true, - ], - 'mastercard11' => [ - 'mastercard', - self::generateCardNumber(27, 16), - true, - ], - 'visa1' => [ - 'visa', - self::generateCardNumber(4, 13), - true, - ], - 'visa2' => [ - 'visa', - self::generateCardNumber(4, 16), - true, - ], - 'visa3' => [ - 'visa', - self::generateCardNumber(4, 19), - true, - ], - 'uatp' => [ - 'uatp', - self::generateCardNumber(1, 15), - true, - ], - 'verve1' => [ - 'verve', - self::generateCardNumber(506, 16), - true, - ], - 'verve2' => [ - 'verve', - self::generateCardNumber(650, 16), - true, - ], - 'verve3' => [ - 'verve', - self::generateCardNumber(506, 19), - true, - ], - 'verve4' => [ - 'verve', - self::generateCardNumber(650, 19), - true, - ], - 'cibc1' => [ - 'cibc', - self::generateCardNumber(4506, 16), - true, - ], - 'rbc1' => [ - 'rbc', - self::generateCardNumber(45, 16), - true, - ], - 'tdtrust' => [ - 'tdtrust', - self::generateCardNumber(589297, 16), - true, - ], - 'scotia1' => [ - 'scotia', - self::generateCardNumber(4536, 16), - true, - ], - 'bmoabm1' => [ - 'bmoabm', - self::generateCardNumber(500, 16), - true, - ], - 'hsbc1' => [ - 'hsbc', - self::generateCardNumber(56, 16), - true, - ], - 'hsbc2' => [ - 'hsbc', - self::generateCardNumber(57, 16), - false, - ], - ]; - } - - /** - * Generate a fake credit card number that still passes the Luhn algorithm. - */ - private static function generateCardNumber(int $prefix, int $length): string - { - $prefix = (string) $prefix; - $cursor = strlen($prefix); - - $digits = str_split($prefix); - $digits = array_pad($digits, $length, '0'); - - while ($cursor < $length - 1) { - $digits[$cursor++] = (string) random_int(0, 9); - } - - $digits[$length - 1] = (string) self::calculateLuhnChecksum($digits, $length); - - return implode('', $digits); - } - - private static function calculateLuhnChecksum(array $digits, int $length): int - { - $parity = $length % 2; - - $sum = 0; - - for ($i = $length - 1; $i >= 0; $i--) { - $current = $digits[$i]; - - if ($i % 2 === $parity) { - $current *= 2; - - if ($current > 9) { - $current -= 9; - } - } - - $sum += $current; - } - - return ($sum * 9) % 10; - } } diff --git a/tests/system/Validation/StrictRules/CreditCardRulesTest.php b/tests/system/Validation/StrictRules/CreditCardRulesTest.php index 4705cdcb9c90..9f79ada9c1f3 100644 --- a/tests/system/Validation/StrictRules/CreditCardRulesTest.php +++ b/tests/system/Validation/StrictRules/CreditCardRulesTest.php @@ -22,12 +22,14 @@ /** * @internal + * + * @no-final */ #[Group('Others')] -final class CreditCardRulesTest extends CIUnitTestCase +class CreditCardRulesTest extends CIUnitTestCase { - private Validation $validation; - private array $config = [ + protected Validation $validation; + protected array $config = [ 'ruleSets' => [ Rules::class, FormatRules::class, @@ -1204,7 +1206,7 @@ public static function provideValidCCNumber(): iterable /** * Generate a fake credit card number that still passes the Luhn algorithm. */ - private static function generateCardNumber(int $prefix, int $length): string + protected static function generateCardNumber(int $prefix, int $length): string { $prefix = (string) $prefix; $cursor = strlen($prefix); @@ -1221,7 +1223,7 @@ private static function generateCardNumber(int $prefix, int $length): string return implode('', $digits); } - private static function calculateLuhnChecksum(array $digits, int $length): int + protected static function calculateLuhnChecksum(array $digits, int $length): int { $parity = $length % 2; From 5ab9e9c63447a0afd01391a3b8ad005de06927d3 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 20 Jun 2024 09:36:48 +0900 Subject: [PATCH 070/277] refactor: move FileRules code to StrictRules\FileRules StrictRules are now the default. --- system/Validation/FileRules.php | 290 +------------------- system/Validation/StrictRules/FileRules.php | 290 +++++++++++++++++++- 2 files changed, 290 insertions(+), 290 deletions(-) diff --git a/system/Validation/FileRules.php b/system/Validation/FileRules.php index 4f846e8ed4fd..b65b4da63085 100644 --- a/system/Validation/FileRules.php +++ b/system/Validation/FileRules.php @@ -13,299 +13,13 @@ namespace CodeIgniter\Validation; -use CodeIgniter\Exceptions\InvalidArgumentException; -use CodeIgniter\HTTP\CLIRequest; -use CodeIgniter\HTTP\IncomingRequest; -use CodeIgniter\HTTP\RequestInterface; -use Config\Mimes; +use CodeIgniter\Validation\StrictRules\FileRules as StrictFileRules; /** * File validation rules * * @see \CodeIgniter\Validation\FileRulesTest */ -class FileRules +class FileRules extends StrictFileRules { - /** - * Request instance. So we can get access to the files. - * - * @var IncomingRequest - */ - protected $request; - - /** - * Constructor. - */ - public function __construct(?RequestInterface $request = null) - { - if ($request === null) { - $request = service('request'); - } - - assert($request instanceof IncomingRequest || $request instanceof CLIRequest); - - $this->request = $request; - } - - /** - * Verifies that $name is the name of a valid uploaded file. - */ - public function uploaded(?string $blank, string $name): bool - { - if (! ($files = $this->request->getFileMultiple($name))) { - $files = [$this->request->getFile($name)]; - } - - foreach ($files as $file) { - if ($file === null) { - return false; - } - - if (ENVIRONMENT === 'testing') { - if ($file->getError() !== 0) { - return false; - } - } else { - // Note: cannot unit test this; no way to over-ride ENVIRONMENT? - // @codeCoverageIgnoreStart - if (! $file->isValid()) { - return false; - } - // @codeCoverageIgnoreEnd - } - } - - return true; - } - - /** - * Verifies if the file's size in Kilobytes is no larger than the parameter. - */ - public function max_size(?string $blank, string $params): bool - { - // Grab the file name off the top of the $params - // after we split it. - $paramArray = explode(',', $params); - if (count($paramArray) !== 2) { - throw new InvalidArgumentException('Invalid max_size parameter: "' . $params . '"'); - } - $name = array_shift($paramArray); - - if (! ($files = $this->request->getFileMultiple($name))) { - $files = [$this->request->getFile($name)]; - } - - foreach ($files as $file) { - if ($file === null) { - return false; - } - - if ($file->getError() === UPLOAD_ERR_NO_FILE) { - return true; - } - - if ($file->getError() === UPLOAD_ERR_INI_SIZE) { - return false; - } - - if ($file->getSize() / 1024 > $paramArray[0]) { - return false; - } - } - - return true; - } - - /** - * Uses the mime config file to determine if a file is considered an "image", - * which for our purposes basically means that it's a raster image or svg. - */ - public function is_image(?string $blank, string $params): bool - { - // Grab the file name off the top of the $params - // after we split it. - $params = explode(',', $params); - $name = array_shift($params); - - if (! ($files = $this->request->getFileMultiple($name))) { - $files = [$this->request->getFile($name)]; - } - - foreach ($files as $file) { - if ($file === null) { - return false; - } - - if ($file->getError() === UPLOAD_ERR_NO_FILE) { - return true; - } - - // We know that our mimes list always has the first mime - // start with `image` even when then are multiple accepted types. - $type = Mimes::guessTypeFromExtension($file->getExtension()) ?? ''; - - if (mb_strpos($type, 'image') !== 0) { - return false; - } - } - - return true; - } - - /** - * Checks to see if an uploaded file's mime type matches one in the parameter. - */ - public function mime_in(?string $blank, string $params): bool - { - // Grab the file name off the top of the $params - // after we split it. - $params = explode(',', $params); - $name = array_shift($params); - - if (! ($files = $this->request->getFileMultiple($name))) { - $files = [$this->request->getFile($name)]; - } - - foreach ($files as $file) { - if ($file === null) { - return false; - } - - if ($file->getError() === UPLOAD_ERR_NO_FILE) { - return true; - } - - if (! in_array($file->getMimeType(), $params, true)) { - return false; - } - } - - return true; - } - - /** - * Checks to see if an uploaded file's extension matches one in the parameter. - */ - public function ext_in(?string $blank, string $params): bool - { - // Grab the file name off the top of the $params - // after we split it. - $params = explode(',', $params); - $name = array_shift($params); - - if (! ($files = $this->request->getFileMultiple($name))) { - $files = [$this->request->getFile($name)]; - } - - foreach ($files as $file) { - if ($file === null) { - return false; - } - - if ($file->getError() === UPLOAD_ERR_NO_FILE) { - return true; - } - - if (! in_array($file->guessExtension(), $params, true)) { - return false; - } - } - - return true; - } - - /** - * Checks an uploaded file to verify that the dimensions are within - * a specified allowable dimension. - */ - public function max_dims(?string $blank, string $params): bool - { - // Grab the file name off the top of the $params - // after we split it. - $params = explode(',', $params); - $name = array_shift($params); - - if (! ($files = $this->request->getFileMultiple($name))) { - $files = [$this->request->getFile($name)]; - } - - foreach ($files as $file) { - if ($file === null) { - return false; - } - - if ($file->getError() === UPLOAD_ERR_NO_FILE) { - return true; - } - - // Get Parameter sizes - $allowedWidth = $params[0] ?? 0; - $allowedHeight = $params[1] ?? 0; - - // Get uploaded image size - $info = getimagesize($file->getTempName()); - - if ($info === false) { - // Cannot get the image size. - return false; - } - - $fileWidth = $info[0]; - $fileHeight = $info[1]; - - if ($fileWidth > $allowedWidth || $fileHeight > $allowedHeight) { - return false; - } - } - - return true; - } - - /** - * Checks an uploaded file to verify that the dimensions are greater than - * a specified dimension. - */ - public function min_dims(?string $blank, string $params): bool - { - // Grab the file name off the top of the $params - // after we split it. - $params = explode(',', $params); - $name = array_shift($params); - - $files = $this->request->getFileMultiple($name); - if ($files === null) { - $files = [$this->request->getFile($name)]; - } - - foreach ($files as $file) { - if ($file === null) { - return false; - } - - if ($file->getError() === UPLOAD_ERR_NO_FILE) { - return true; - } - - // Get Parameter sizes - $minimumWidth = $params[0] ?? 0; - $minimumHeight = $params[1] ?? 0; - - // Get uploaded image size - $info = getimagesize($file->getTempName()); - - if ($info === false) { - // Cannot get the image size. - return false; - } - - $fileWidth = $info[0]; - $fileHeight = $info[1]; - - if ($fileWidth < $minimumWidth || $fileHeight < $minimumHeight) { - return false; - } - } - - return true; - } } diff --git a/system/Validation/StrictRules/FileRules.php b/system/Validation/StrictRules/FileRules.php index e86b84e99bfe..0b7bf3bd227b 100644 --- a/system/Validation/StrictRules/FileRules.php +++ b/system/Validation/StrictRules/FileRules.php @@ -13,13 +13,299 @@ namespace CodeIgniter\Validation\StrictRules; -use CodeIgniter\Validation\FileRules as NonStrictFileRules; +use CodeIgniter\Exceptions\InvalidArgumentException; +use CodeIgniter\HTTP\CLIRequest; +use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\RequestInterface; +use Config\Mimes; /** * File validation rules * * @see \CodeIgniter\Validation\StrictRules\FileRulesTest */ -class FileRules extends NonStrictFileRules +class FileRules { + /** + * Request instance. So we can get access to the files. + * + * @var IncomingRequest + */ + protected $request; + + /** + * Constructor. + */ + public function __construct(?RequestInterface $request = null) + { + if ($request === null) { + $request = service('request'); + } + + assert($request instanceof IncomingRequest || $request instanceof CLIRequest); + + $this->request = $request; + } + + /** + * Verifies that $name is the name of a valid uploaded file. + */ + public function uploaded(?string $blank, string $name): bool + { + if (! ($files = $this->request->getFileMultiple($name))) { + $files = [$this->request->getFile($name)]; + } + + foreach ($files as $file) { + if ($file === null) { + return false; + } + + if (ENVIRONMENT === 'testing') { + if ($file->getError() !== 0) { + return false; + } + } else { + // Note: cannot unit test this; no way to over-ride ENVIRONMENT? + // @codeCoverageIgnoreStart + if (! $file->isValid()) { + return false; + } + // @codeCoverageIgnoreEnd + } + } + + return true; + } + + /** + * Verifies if the file's size in Kilobytes is no larger than the parameter. + */ + public function max_size(?string $blank, string $params): bool + { + // Grab the file name off the top of the $params + // after we split it. + $paramArray = explode(',', $params); + if (count($paramArray) !== 2) { + throw new InvalidArgumentException('Invalid max_size parameter: "' . $params . '"'); + } + $name = array_shift($paramArray); + + if (! ($files = $this->request->getFileMultiple($name))) { + $files = [$this->request->getFile($name)]; + } + + foreach ($files as $file) { + if ($file === null) { + return false; + } + + if ($file->getError() === UPLOAD_ERR_NO_FILE) { + return true; + } + + if ($file->getError() === UPLOAD_ERR_INI_SIZE) { + return false; + } + + if ($file->getSize() / 1024 > $paramArray[0]) { + return false; + } + } + + return true; + } + + /** + * Uses the mime config file to determine if a file is considered an "image", + * which for our purposes basically means that it's a raster image or svg. + */ + public function is_image(?string $blank, string $params): bool + { + // Grab the file name off the top of the $params + // after we split it. + $params = explode(',', $params); + $name = array_shift($params); + + if (! ($files = $this->request->getFileMultiple($name))) { + $files = [$this->request->getFile($name)]; + } + + foreach ($files as $file) { + if ($file === null) { + return false; + } + + if ($file->getError() === UPLOAD_ERR_NO_FILE) { + return true; + } + + // We know that our mimes list always has the first mime + // start with `image` even when then are multiple accepted types. + $type = Mimes::guessTypeFromExtension($file->getExtension()) ?? ''; + + if (mb_strpos($type, 'image') !== 0) { + return false; + } + } + + return true; + } + + /** + * Checks to see if an uploaded file's mime type matches one in the parameter. + */ + public function mime_in(?string $blank, string $params): bool + { + // Grab the file name off the top of the $params + // after we split it. + $params = explode(',', $params); + $name = array_shift($params); + + if (! ($files = $this->request->getFileMultiple($name))) { + $files = [$this->request->getFile($name)]; + } + + foreach ($files as $file) { + if ($file === null) { + return false; + } + + if ($file->getError() === UPLOAD_ERR_NO_FILE) { + return true; + } + + if (! in_array($file->getMimeType(), $params, true)) { + return false; + } + } + + return true; + } + + /** + * Checks to see if an uploaded file's extension matches one in the parameter. + */ + public function ext_in(?string $blank, string $params): bool + { + // Grab the file name off the top of the $params + // after we split it. + $params = explode(',', $params); + $name = array_shift($params); + + if (! ($files = $this->request->getFileMultiple($name))) { + $files = [$this->request->getFile($name)]; + } + + foreach ($files as $file) { + if ($file === null) { + return false; + } + + if ($file->getError() === UPLOAD_ERR_NO_FILE) { + return true; + } + + if (! in_array($file->guessExtension(), $params, true)) { + return false; + } + } + + return true; + } + + /** + * Checks an uploaded file to verify that the dimensions are within + * a specified allowable dimension. + */ + public function max_dims(?string $blank, string $params): bool + { + // Grab the file name off the top of the $params + // after we split it. + $params = explode(',', $params); + $name = array_shift($params); + + if (! ($files = $this->request->getFileMultiple($name))) { + $files = [$this->request->getFile($name)]; + } + + foreach ($files as $file) { + if ($file === null) { + return false; + } + + if ($file->getError() === UPLOAD_ERR_NO_FILE) { + return true; + } + + // Get Parameter sizes + $allowedWidth = $params[0] ?? 0; + $allowedHeight = $params[1] ?? 0; + + // Get uploaded image size + $info = getimagesize($file->getTempName()); + + if ($info === false) { + // Cannot get the image size. + return false; + } + + $fileWidth = $info[0]; + $fileHeight = $info[1]; + + if ($fileWidth > $allowedWidth || $fileHeight > $allowedHeight) { + return false; + } + } + + return true; + } + + /** + * Checks an uploaded file to verify that the dimensions are greater than + * a specified dimension. + */ + public function min_dims(?string $blank, string $params): bool + { + // Grab the file name off the top of the $params + // after we split it. + $params = explode(',', $params); + $name = array_shift($params); + + $files = $this->request->getFileMultiple($name); + if ($files === null) { + $files = [$this->request->getFile($name)]; + } + + foreach ($files as $file) { + if ($file === null) { + return false; + } + + if ($file->getError() === UPLOAD_ERR_NO_FILE) { + return true; + } + + // Get Parameter sizes + $minimumWidth = $params[0] ?? 0; + $minimumHeight = $params[1] ?? 0; + + // Get uploaded image size + $info = getimagesize($file->getTempName()); + + if ($info === false) { + // Cannot get the image size. + return false; + } + + $fileWidth = $info[0]; + $fileHeight = $info[1]; + + if ($fileWidth < $minimumWidth || $fileHeight < $minimumHeight) { + return false; + } + } + + return true; + } } From 8f52edd6e1a4e1b8444f27359333e62e4324007a Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 20 Jun 2024 09:46:08 +0900 Subject: [PATCH 071/277] docs: add category to Enhancements > Libraries --- user_guide_src/source/changelogs/v4.6.0.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index 5efd08445411..a4644c91f4f0 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -127,8 +127,8 @@ Model Libraries ========= -- Added `retainMultiplePatterns()` to `FileCollection` class. See :ref:`FileCollection::retainMultiplePatterns() `. -- Added `min_dims` validation rule to `FileRules` class. See :ref`Validation `. +- **FileCollection:** Added `retainMultiplePatterns()` to `FileCollection` class. See :ref:`FileCollection::retainMultiplePatterns() `. +- **Validation:** Added `min_dims` validation rule to `FileRules` class. See :ref`Validation `. Helpers and Functions ===================== From bbeda77c9510b16d418f9c3b7a5098c1b92a5787 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 20 Jun 2024 09:55:45 +0900 Subject: [PATCH 072/277] refactor: if conditions We accept only booleans. --- system/Validation/StrictRules/FileRules.php | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/system/Validation/StrictRules/FileRules.php b/system/Validation/StrictRules/FileRules.php index 0b7bf3bd227b..e808d4b2a382 100644 --- a/system/Validation/StrictRules/FileRules.php +++ b/system/Validation/StrictRules/FileRules.php @@ -52,7 +52,8 @@ public function __construct(?RequestInterface $request = null) */ public function uploaded(?string $blank, string $name): bool { - if (! ($files = $this->request->getFileMultiple($name))) { + $files = $this->request->getFileMultiple($name); + if ($files === null) { $files = [$this->request->getFile($name)]; } @@ -91,7 +92,8 @@ public function max_size(?string $blank, string $params): bool } $name = array_shift($paramArray); - if (! ($files = $this->request->getFileMultiple($name))) { + $files = $this->request->getFileMultiple($name); + if ($files === null) { $files = [$this->request->getFile($name)]; } @@ -127,7 +129,8 @@ public function is_image(?string $blank, string $params): bool $params = explode(',', $params); $name = array_shift($params); - if (! ($files = $this->request->getFileMultiple($name))) { + $files = $this->request->getFileMultiple($name); + if ($files === null) { $files = [$this->request->getFile($name)]; } @@ -162,7 +165,8 @@ public function mime_in(?string $blank, string $params): bool $params = explode(',', $params); $name = array_shift($params); - if (! ($files = $this->request->getFileMultiple($name))) { + $files = $this->request->getFileMultiple($name); + if ($files === null) { $files = [$this->request->getFile($name)]; } @@ -193,7 +197,8 @@ public function ext_in(?string $blank, string $params): bool $params = explode(',', $params); $name = array_shift($params); - if (! ($files = $this->request->getFileMultiple($name))) { + $files = $this->request->getFileMultiple($name); + if ($files === null) { $files = [$this->request->getFile($name)]; } @@ -225,7 +230,8 @@ public function max_dims(?string $blank, string $params): bool $params = explode(',', $params); $name = array_shift($params); - if (! ($files = $this->request->getFileMultiple($name))) { + $files = $this->request->getFileMultiple($name); + if ($files === null) { $files = [$this->request->getFile($name)]; } From b6c0a66234c75dd9ec6ba1d3371162482678fae2 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 20 Jun 2024 09:56:31 +0900 Subject: [PATCH 073/277] chore: update phpstan-baseline.php --- phpstan-baseline.php | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/phpstan-baseline.php b/phpstan-baseline.php index 05825b1d76ed..fec8417d9d85 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -9973,12 +9973,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/Validation/DotArrayFilter.php', ]; -$ignoreErrors[] = [ - // identifier: booleanNot.exprNotBoolean - 'message' => '#^Only booleans are allowed in a negated boolean, array\\|null given\\.$#', - 'count' => 6, - 'path' => __DIR__ . '/system/Validation/FileRules.php', -]; $ignoreErrors[] = [ // identifier: missingType.iterableValue 'message' => '#^Method CodeIgniter\\\\Validation\\\\Rules\\:\\:differs\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', @@ -17953,24 +17947,6 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/Throttle/ThrottleTest.php', ]; -$ignoreErrors[] = [ - // identifier: missingType.iterableValue - 'message' => '#^Method CodeIgniter\\\\Validation\\\\CreditCardRulesTest\\:\\:calculateLuhnChecksum\\(\\) has parameter \\$digits with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/Validation/CreditCardRulesTest.php', -]; -$ignoreErrors[] = [ - // identifier: missingType.iterableValue - 'message' => '#^Method CodeIgniter\\\\Validation\\\\CreditCardRulesTest\\:\\:provideValidCCNumber\\(\\) return type has no value type specified in iterable type iterable\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/Validation/CreditCardRulesTest.php', -]; -$ignoreErrors[] = [ - // identifier: argument.type - 'message' => '#^Parameter \\#1 \\$config of class CodeIgniter\\\\Validation\\\\Validation constructor expects Config\\\\Validation, stdClass given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/Validation/CreditCardRulesTest.php', -]; $ignoreErrors[] = [ // identifier: missingType.iterableValue 'message' => '#^Property CodeIgniter\\\\Validation\\\\CreditCardRulesTest\\:\\:\\$config type has no value type specified in iterable type array\\.$#', @@ -17989,12 +17965,6 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/Validation/DatabaseRelatedRulesTest.php', ]; -$ignoreErrors[] = [ - // identifier: argument.type - 'message' => '#^Parameter \\#1 \\$config of class CodeIgniter\\\\Validation\\\\Validation constructor expects Config\\\\Validation, stdClass given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/Validation/FileRulesTest.php', -]; $ignoreErrors[] = [ // identifier: missingType.iterableValue 'message' => '#^Property CodeIgniter\\\\Validation\\\\FileRulesTest\\:\\:\\$config type has no value type specified in iterable type array\\.$#', From a83ad06a0d960807490b4dc3f59a75de65e317f4 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 20 Jun 2024 16:04:05 +0900 Subject: [PATCH 074/277] docs: fix RST format --- user_guide_src/source/changelogs/v4.6.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index a4644c91f4f0..880f9d444dca 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -128,7 +128,7 @@ Libraries ========= - **FileCollection:** Added `retainMultiplePatterns()` to `FileCollection` class. See :ref:`FileCollection::retainMultiplePatterns() `. -- **Validation:** Added `min_dims` validation rule to `FileRules` class. See :ref`Validation `. +- **Validation:** Added `min_dims` validation rule to `FileRules` class. See :ref:`Validation `. Helpers and Functions ===================== From db76c7e763ea4a2b95164cd361c2c4ad4af10014 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 20 Jun 2024 16:05:04 +0900 Subject: [PATCH 075/277] docs: fix text decoration --- user_guide_src/source/changelogs/v4.6.0.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index 880f9d444dca..e48060dd1b10 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -127,8 +127,8 @@ Model Libraries ========= -- **FileCollection:** Added `retainMultiplePatterns()` to `FileCollection` class. See :ref:`FileCollection::retainMultiplePatterns() `. -- **Validation:** Added `min_dims` validation rule to `FileRules` class. See :ref:`Validation `. +- **FileCollection:** Added ``retainMultiplePatterns()`` to ``FileCollection`` class. See :ref:`FileCollection::retainMultiplePatterns() `. +- **Validation:** Added ``min_dims`` validation rule to ``FileRules`` class. See :ref:`Validation `. Helpers and Functions ===================== From 4cb3ceeecce87320ddefc1f3241115a6b7260e1c Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 20 Jun 2024 16:05:35 +0900 Subject: [PATCH 076/277] docs: break long lines --- user_guide_src/source/changelogs/v4.6.0.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index e48060dd1b10..06e0f32a10e3 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -127,8 +127,10 @@ Model Libraries ========= -- **FileCollection:** Added ``retainMultiplePatterns()`` to ``FileCollection`` class. See :ref:`FileCollection::retainMultiplePatterns() `. -- **Validation:** Added ``min_dims`` validation rule to ``FileRules`` class. See :ref:`Validation `. +- **FileCollection:** Added ``retainMultiplePatterns()`` to ``FileCollection`` class. + See :ref:`FileCollection::retainMultiplePatterns() `. +- **Validation:** Added ``min_dims`` validation rule to ``FileRules`` class. See + :ref:`Validation `. Helpers and Functions ===================== From 48875f5ba1a93686208533248fbcba5737c8d288 Mon Sep 17 00:00:00 2001 From: ducng99 Date: Fri, 21 Jun 2024 00:05:21 +1200 Subject: [PATCH 077/277] feat: add `foundRows` config for MySQLi --- app/Config/Database.php | 1 + system/Database/BaseConnection.php | 1 + system/Database/MySQLi/Connection.php | 14 ++++++++++++++ 3 files changed, 16 insertions(+) diff --git a/app/Config/Database.php b/app/Config/Database.php index 7a1fd21e8d10..1b4c66e52e65 100644 --- a/app/Config/Database.php +++ b/app/Config/Database.php @@ -43,6 +43,7 @@ class Database extends Config 'failover' => [], 'port' => 3306, 'numberNative' => false, + 'foundRows' => false, 'dateFormat' => [ 'date' => 'Y-m-d', 'datetime' => 'Y-m-d H:i:s', diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index 57c37d8a0a6c..bf18abe12f42 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -35,6 +35,7 @@ * @property-read string $DSN * @property-read array|bool $encrypt * @property-read array $failover + * @property-read bool $foundRows * @property-read string $hostname * @property-read Query $lastQuery * @property-read string $password diff --git a/system/Database/MySQLi/Connection.php b/system/Database/MySQLi/Connection.php index e8db25c76640..c04d9e364432 100644 --- a/system/Database/MySQLi/Connection.php +++ b/system/Database/MySQLi/Connection.php @@ -81,6 +81,16 @@ class Connection extends BaseConnection */ public $numberNative = false; + /** + * Use MYSQLI_CLIENT_FOUND_ROWS + * + * Whether affected_rows should return number of rows found, + * or number of rows changed, after an UPDATE query. + * + * @var bool + */ + public $foundRows = false; + /** * Connect to the database. * @@ -182,6 +192,10 @@ public function connect(bool $persistent = false) $clientFlags += MYSQLI_CLIENT_SSL; } + if ($this->foundRows) { + $clientFlags += MYSQLI_CLIENT_FOUND_ROWS; + } + try { if ($this->mysqli->real_connect( $hostname, From 542df34d0d611ae32aa32b53867e3b3bb8c4e623 Mon Sep 17 00:00:00 2001 From: ducng99 Date: Fri, 21 Jun 2024 01:12:23 +1200 Subject: [PATCH 078/277] test: add test for foundRows db config --- .../Database/Live/MySQLi/FoundRowsTest.php | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 tests/system/Database/Live/MySQLi/FoundRowsTest.php diff --git a/tests/system/Database/Live/MySQLi/FoundRowsTest.php b/tests/system/Database/Live/MySQLi/FoundRowsTest.php new file mode 100644 index 000000000000..011830088d44 --- /dev/null +++ b/tests/system/Database/Live/MySQLi/FoundRowsTest.php @@ -0,0 +1,192 @@ + + * + * 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\Test\CIUnitTestCase; +use CodeIgniter\Test\DatabaseTestTrait; +use Config\Database; +use PHPUnit\Framework\Attributes\Group; +use Tests\Support\Database\Seeds\CITestSeeder; + +/** + * @internal + */ +#[Group('DatabaseLive')] +final class FoundRowsTest extends CIUnitTestCase +{ + use DatabaseTestTrait; + + /** + * @var array + */ + private $tests; + + protected $refresh = true; + protected $seed = CITestSeeder::class; + + protected function setUp(): void + { + parent::setUp(); + + $config = config('Database'); + + $this->tests = $config->tests; + } + + public function testEnableFoundRows(): void + { + $this->tests['foundRows'] = true; + + $db1 = Database::connect($this->tests); + + if ($db1->DBDriver !== 'MySQLi') { + $this->markTestSkipped('Only MySQLi can complete this test.'); + } + + $this->assertTrue($db1->foundRows); + } + + public function testDisableFoundRows(): void + { + $this->tests['foundRows'] = false; + + $db1 = Database::connect($this->tests); + + if ($db1->DBDriver !== 'MySQLi') { + $this->markTestSkipped('Only MySQLi can complete this test.'); + } + + $this->assertFalse($db1->foundRows); + } + + public function testAffectedRowsAfterEnableFoundRowsWithNoChange(): void + { + $this->tests['foundRows'] = true; + + $db1 = Database::connect($this->tests); + + if ($db1->DBDriver !== 'MySQLi') { + $this->markTestSkipped('Only MySQLi can complete this test.'); + } + + $db1->table('db_user') + ->set('country', 'US') + ->where('country', 'US') + ->update(); + + $affectedRows = $db1->affectedRows(); + + $this->assertSame($affectedRows, 2); + } + + public function testAffectedRowsAfterDisableFoundRowsWithNoChange(): void + { + $this->tests['foundRows'] = false; + + $db1 = Database::connect($this->tests); + + if ($db1->DBDriver !== 'MySQLi') { + $this->markTestSkipped('Only MySQLi can complete this test.'); + } + + $db1->table('db_user') + ->set('country', 'US') + ->where('country', 'US') + ->update(); + + $affectedRows = $db1->affectedRows(); + + $this->assertSame($affectedRows, 0); + } + + public function testAffectedRowsAfterEnableFoundRowsWithChange(): void + { + $this->tests['foundRows'] = true; + + $db1 = Database::connect($this->tests); + + if ($db1->DBDriver !== 'MySQLi') { + $this->markTestSkipped('Only MySQLi can complete this test.'); + } + + $db1->table('db_user') + ->set('country', 'NZ') + ->where('country', 'US') + ->update(); + + $affectedRows = $db1->affectedRows(); + + $this->assertSame($affectedRows, 2); + } + + public function testAffectedRowsAfterDisableFoundRowsWithChange(): void + { + $this->tests['foundRows'] = false; + + $db1 = Database::connect($this->tests); + + if ($db1->DBDriver !== 'MySQLi') { + $this->markTestSkipped('Only MySQLi can complete this test.'); + } + + $db1->table('db_user') + ->set('country', 'NZ') + ->where('country', 'US') + ->update(); + + $affectedRows = $db1->affectedRows(); + + $this->assertSame($affectedRows, 2); + } + + public function testAffectedRowsAfterEnableFoundRowsWithPartialChange(): void + { + $this->tests['foundRows'] = true; + + $db1 = Database::connect($this->tests); + + if ($db1->DBDriver !== 'MySQLi') { + $this->markTestSkipped('Only MySQLi can complete this test.'); + } + + $db1->table('db_user') + ->set('name', 'Derek Jones') + ->where('country', 'US') + ->update(); + + $affectedRows = $db1->affectedRows(); + + $this->assertSame($affectedRows, 2); + } + + public function testAffectedRowsAfterDisableFoundRowsWithPartialChange(): void + { + $this->tests['foundRows'] = false; + + $db1 = Database::connect($this->tests); + + if ($db1->DBDriver !== 'MySQLi') { + $this->markTestSkipped('Only MySQLi can complete this test.'); + } + + $db1->table('db_user') + ->set('name', 'Derek Jones') + ->where('country', 'US') + ->update(); + + $affectedRows = $db1->affectedRows(); + + $this->assertSame($affectedRows, 1); + } +} From 698f6432cc4ca851aed01935480e477c6aefcd1a Mon Sep 17 00:00:00 2001 From: ducng99 Date: Fri, 21 Jun 2024 01:12:34 +1200 Subject: [PATCH 079/277] docs: updated changelog and new config field for foundRows --- user_guide_src/source/changelogs/v4.6.0.rst | 2 ++ user_guide_src/source/database/configuration.rst | 1 + 2 files changed, 3 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index 5efd08445411..a181a4f95730 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -121,6 +121,8 @@ Forge Others ------ +- Added a new configuration `foundRows` for MySQLi to use `MYSQLI_CLIENT_FOUND_ROWS`. + Model ===== diff --git a/user_guide_src/source/database/configuration.rst b/user_guide_src/source/database/configuration.rst index 86155dea0803..b42317469038 100644 --- a/user_guide_src/source/database/configuration.rst +++ b/user_guide_src/source/database/configuration.rst @@ -179,6 +179,7 @@ Description of Values To enforce Foreign Key constraint, set this config item to true. **busyTimeout** (``SQLite3`` only) milliseconds (int) - Sleeps for a specified amount of time when a table is locked. **numberNative** (``MySQLi`` only) true/false (boolean) - Whether to enable MYSQLI_OPT_INT_AND_FLOAT_NATIVE. +**foundRows** (``MySQLi`` only) true/false (boolean) - Whether to enable MYSQLI_CLIENT_FOUND_ROWS. **dateFormat** The default date/time formats as PHP's `DateTime format`_. * ``date`` - date format * ``datetime`` - date and time format From 725d0d4ae3c6109067f60af0f0c75c49237cfd00 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 20 Jun 2024 15:06:27 +0900 Subject: [PATCH 080/277] docs: add @testTag to Filters::initialize() --- system/Filters/Filters.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index d8e22835e2ed..de3011a0e9f4 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -379,6 +379,9 @@ private function setToolbarToLast(array $filters, bool $remove = false): array * @TODO We don't need to accept null as $uri. * * @return Filters + * + * @testTag Only for test code. The run() calls this, so you don't need to + * call this in your app. */ public function initialize(?string $uri = null) { From 6bfdc8f654a01593a1870669b5fd06282e0716a3 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 20 Jun 2024 15:11:13 +0900 Subject: [PATCH 081/277] test: add tests for runRequired() --- phpstan-baseline.php | 2 +- tests/system/Filters/FiltersTest.php | 38 ++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/phpstan-baseline.php b/phpstan-baseline.php index 05825b1d76ed..98b10c320ce4 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -14014,7 +14014,7 @@ $ignoreErrors[] = [ // identifier: codeigniter.superglobalAccessAssign 'message' => '#^Assigning \'GET\' directly on offset \'REQUEST_METHOD\' of \\$_SERVER is discouraged\\.$#', - 'count' => 36, + 'count' => 38, 'path' => __DIR__ . '/tests/system/Filters/FiltersTest.php', ]; $ignoreErrors[] = [ diff --git a/tests/system/Filters/FiltersTest.php b/tests/system/Filters/FiltersTest.php index 3a35920989e5..98260ad65339 100644 --- a/tests/system/Filters/FiltersTest.php +++ b/tests/system/Filters/FiltersTest.php @@ -1352,4 +1352,42 @@ public function testReset(): void $this->assertSame(['foo'], $filters->initialize($uri)->getFilters()['before']); $this->assertSame([], $filters->reset()->getFilters()['before']); } + + public function testRunRequiredDoesBefore(): void + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + + $config = [ + 'aliases' => ['google' => GoogleMe::class], + 'required' => [ + 'before' => ['google'], + 'after' => [], + ], + ]; + $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config); + $filters = $this->createFilters($filtersConfig); + + $request = $filters->runRequired('before'); + + $this->assertSame('http://google.com', $request->getBody()); + } + + public function testRunRequiredDoesAfter(): void + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + + $config = [ + 'aliases' => ['google' => GoogleMe::class], + 'required' => [ + 'before' => [], + 'after' => ['google'], + ], + ]; + $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config); + $filters = $this->createFilters($filtersConfig); + + $response = $filters->runRequired('after'); + + $this->assertSame('http://google.com', $response->getBody()); + } } From b5b63f65e20bded8a1115badfe93ef7b3f8e26cc Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 20 Jun 2024 14:19:27 +0900 Subject: [PATCH 082/277] feat!: can execute a filter more than once with differnet arguments Changes array structures of $filters, $filtersClass properties. --- phpstan-baseline.php | 30 ----- system/Filters/Filters.php | 180 ++++++++++++++------------- tests/system/CodeIgniterTest.php | 8 +- tests/system/Filters/FiltersTest.php | 28 ++--- 4 files changed, 109 insertions(+), 137 deletions(-) diff --git a/phpstan-baseline.php b/phpstan-baseline.php index 98b10c320ce4..101313b73f3d 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -5323,18 +5323,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/Filters/Filters.php', ]; -$ignoreErrors[] = [ - // identifier: missingType.iterableValue - 'message' => '#^Method CodeIgniter\\\\Filters\\\\Filters\\:\\:getFilters\\(\\) return type has no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Filters/Filters.php', -]; -$ignoreErrors[] = [ - // identifier: missingType.iterableValue - 'message' => '#^Method CodeIgniter\\\\Filters\\\\Filters\\:\\:getFiltersClass\\(\\) return type has no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Filters/Filters.php', -]; $ignoreErrors[] = [ // identifier: missingType.iterableValue 'message' => '#^Method CodeIgniter\\\\Filters\\\\Filters\\:\\:getRequiredFilters\\(\\) return type has no value type specified in iterable type array\\.$#', @@ -5347,12 +5335,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/Filters/Filters.php', ]; -$ignoreErrors[] = [ - // identifier: missingType.iterableValue - 'message' => '#^Method CodeIgniter\\\\Filters\\\\Filters\\:\\:registerArguments\\(\\) has parameter \\$arguments with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Filters/Filters.php', -]; $ignoreErrors[] = [ // identifier: missingType.iterableValue 'message' => '#^Method CodeIgniter\\\\Filters\\\\Filters\\:\\:runAfter\\(\\) has parameter \\$filterClasses with no value type specified in iterable type array\\.$#', @@ -5377,18 +5359,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/Filters/Filters.php', ]; -$ignoreErrors[] = [ - // identifier: missingType.iterableValue - 'message' => '#^Property CodeIgniter\\\\Filters\\\\Filters\\:\\:\\$filters type has no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Filters/Filters.php', -]; -$ignoreErrors[] = [ - // identifier: missingType.iterableValue - 'message' => '#^Property CodeIgniter\\\\Filters\\\\Filters\\:\\:\\$filtersClass type has no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Filters/Filters.php', -]; $ignoreErrors[] = [ // identifier: missingType.iterableValue 'message' => '#^Method CodeIgniter\\\\Filters\\\\ForceHTTPS\\:\\:after\\(\\) has parameter \\$arguments with no value type specified in iterable type array\\.$#', diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index de3011a0e9f4..49ca13a99f3a 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -14,7 +14,6 @@ namespace CodeIgniter\Filters; use CodeIgniter\Config\Filters as BaseFiltersConfig; -use CodeIgniter\Exceptions\ConfigException; use CodeIgniter\Filters\Exceptions\FilterException; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; @@ -66,12 +65,28 @@ class Filters protected $initialized = false; /** - * The processed filters that will - * be used to check against. + * The filter list to execute for the current request (URI path). * + * This property is for display. Use $filtersClass to execute filters. * This does not include "Required Filters". * - * @var array + * [ + * 'before' => [ + * 'alias', + * 'alias:arg1', + * 'alias:arg1,arg2', + * ], + * 'after' => [ + * 'alias', + * 'alias:arg1', + * 'alias:arg1,arg2', + * ], + * ] + * + * @var array{ + * before: list, + * after: list + * } */ protected $filters = [ 'before' => [], @@ -79,12 +94,24 @@ class Filters ]; /** - * The collection of filters' class names that will - * be used to execute in each position. + * The collection of filters' class names to execute for the current request + * (URI path). * * This does not include "Required Filters". * - * @var array + * [ + * 'before' => [ + * [classname, arguments], + * ], + * 'after' => [ + * [classname, arguments], + * ], + * ] + * + * @var array{ + * before: list}>, + * after: list}> + * } */ protected $filtersClass = [ 'before' => [], @@ -95,6 +122,8 @@ class Filters * Any arguments to be passed to filters. * * @var array|null> [name => params] + * + * @deprecated 4.6.0 No longer used. */ protected $arguments = []; @@ -102,6 +131,8 @@ class Filters * Any arguments to be passed to filtersClass. * * @var array|null> [classname => arguments] + * + * @deprecated 4.6.0 No longer used. */ protected $argumentsClass = []; @@ -193,17 +224,17 @@ public function run(string $uri, string $position = 'before') */ private function runBefore(array $filterClasses) { - foreach ($filterClasses as $className) { + foreach ($filterClasses as $filterClassInfo) { + $className = $filterClassInfo[0]; + $arguments = ($filterClassInfo[1] === []) ? null : $filterClassInfo[1]; + $class = new $className(); if (! $class instanceof FilterInterface) { throw FilterException::forIncorrectInterface($class::class); } - $result = $class->before( - $this->request, - $this->argumentsClass[$className] ?? null - ); + $result = $class->before($this->request, $arguments); if ($result instanceof RequestInterface) { $this->request = $result; @@ -231,18 +262,17 @@ private function runBefore(array $filterClasses) private function runAfter(array $filterClasses): ResponseInterface { - foreach ($filterClasses as $className) { + foreach ($filterClasses as $filterClassInfo) { + $className = $filterClassInfo[0]; + $arguments = ($filterClassInfo[1] === []) ? null : $filterClassInfo[1]; + $class = new $className(); if (! $class instanceof FilterInterface) { throw FilterException::forIncorrectInterface($class::class); } - $result = $class->after( - $this->request, - $this->response, - $this->argumentsClass[$className] ?? null - ); + $result = $class->after($this->request, $this->response, $arguments); if ($result instanceof ResponseInterface) { $this->response = $result; @@ -277,9 +307,11 @@ public function runRequired(string $position = 'before') foreach ($filters as $alias) { if (is_array($aliases[$alias])) { - $filterClasses[$position] = array_merge($filterClasses[$position], $aliases[$alias]); + foreach ($this->config->aliases[$alias] as $class) { + $filterClasses[$position][] = [$class, []]; + } } else { - $filterClasses[$position][] = $aliases[$alias]; + $filterClasses[$position][] = [$aliases[$alias], []]; } } @@ -406,6 +438,11 @@ public function initialize(?string $uri = null) // Set the toolbar filter to the last position to be executed $this->filters['after'] = $this->setToolbarToLast($this->filters['after']); + // Since some filters like rate limiters rely on being executed once a request, + // we filter em here. + $this->filters['before'] = array_unique($this->filters['before']); + $this->filters['after'] = array_unique($this->filters['after']); + $this->processAliasesToClass('before'); $this->processAliasesToClass('after'); @@ -436,6 +473,11 @@ public function reset(): self /** * Returns the processed filters array. * This does not include "Required Filters". + * + * @return array{ + * before: list, + * after: list + * } */ public function getFilters(): array { @@ -445,6 +487,11 @@ public function getFilters(): array /** * Returns the filtersClass array. * This does not include "Required Filters". + * + * @return array{ + * before: list}>, + * after: list}> + * } */ public function getFiltersClass(): array { @@ -485,29 +532,31 @@ public function addFilter(string $class, ?string $alias = null, string $when = ' * are passed to the filter when executed. * * @param string $name filter_name or filter_name:arguments like 'role:admin,manager' + * or filter classname. */ private function enableFilter(string $name, string $when = 'before'): void { - // Get arguments and clean name - [$name, $arguments] = $this->getCleanName($name); - $this->arguments[$name] = ($arguments !== []) ? $arguments : null; - - if (class_exists($name)) { - $this->config->aliases[$name] = $name; - } elseif (! array_key_exists($name, $this->config->aliases)) { - throw FilterException::forNoAlias($name); + // Normalize the arguments. + [$alias, $arguments] = $this->getCleanName($name); + if ($arguments === []) { + $filter = $alias; + } else { + $filter = $alias . ':' . implode(',', $arguments); } - $classNames = (array) $this->config->aliases[$name]; - - foreach ($classNames as $className) { - $this->argumentsClass[$className] = $this->arguments[$name] ?? null; + if (class_exists($alias)) { + $this->config->aliases[$alias] = $alias; + } elseif (! array_key_exists($alias, $this->config->aliases)) { + throw FilterException::forNoAlias($alias); } - if (! isset($this->filters[$when][$name])) { - $this->filters[$when][] = $name; - $this->filtersClass[$when] = array_merge($this->filtersClass[$when], $classNames); + if (! isset($this->filters[$when][$filter])) { + $this->filters[$when][] = $filter; } + + // Since some filters like rate limiters rely on being executed once a request, + // we filter em here. + $this->filters[$when] = array_unique($this->filters[$when]); } /** @@ -557,6 +606,8 @@ public function enableFilters(array $names, string $when = 'before') * Returns the arguments for a specified key, or all. * * @return array|string + * + * @deprecated 4.6.0 Already does not work. */ public function getArguments(?string $key = null) { @@ -692,12 +743,7 @@ protected function processFilters(?string $uri = null) $path = $settings['before']; if ($this->pathApplies($uri, $path)) { - // Get arguments and clean name - [$name, $arguments] = $this->getCleanName($alias); - - $filters['before'][] = $name; - - $this->registerArguments($name, $arguments); + $filters['before'][] = $alias; } } @@ -705,14 +751,7 @@ protected function processFilters(?string $uri = null) $path = $settings['after']; if ($this->pathApplies($uri, $path)) { - // Get arguments and clean name - [$name, $arguments] = $this->getCleanName($alias); - - $filters['after'][] = $name; - - // The arguments may have already been registered in the before filter. - // So disable check. - $this->registerArguments($name, $arguments, false); + $filters['after'][] = $alias; } } } @@ -736,31 +775,6 @@ protected function processFilters(?string $uri = null) } } - /** - * @param string $name filter alias - * @param array $arguments filter arguments - * @param bool $check if true, check if already defined - */ - private function registerArguments(string $name, array $arguments, bool $check = true): void - { - if ($arguments !== []) { - if ($check && array_key_exists($name, $this->arguments)) { - throw new ConfigException( - '"' . $name . '" already has arguments: ' - . (($this->arguments[$name] === null) ? 'null' : implode(',', $this->arguments[$name])) - ); - } - - $this->arguments[$name] = $arguments; - } - - $classNames = (array) $this->config->aliases[$name]; - - foreach ($classNames as $className) { - $this->argumentsClass[$className] = $this->arguments[$name] ?? null; - } - } - /** * Maps filter aliases to the equivalent filter classes * @@ -772,32 +786,28 @@ protected function processAliasesToClass(string $position) { $filterClasses = []; - foreach ($this->filters[$position] as $alias => $rules) { - if (is_numeric($alias) && is_string($rules)) { - $alias = $rules; - } + foreach ($this->filters[$position] as $filter) { + // Get arguments and clean alias + [$alias, $arguments] = $this->getCleanName($filter); if (! array_key_exists($alias, $this->config->aliases)) { throw FilterException::forNoAlias($alias); } if (is_array($this->config->aliases[$alias])) { - $filterClasses = [...$filterClasses, ...$this->config->aliases[$alias]]; + foreach ($this->config->aliases[$alias] as $class) { + $filterClasses[] = [$class, $arguments]; + } } else { - $filterClasses[] = $this->config->aliases[$alias]; + $filterClasses[] = [$this->config->aliases[$alias], $arguments]; } } - // when using enableFilter() we already write the class name in $filterClasses as well as the - // alias in $filters. This leads to duplicates when using route filters. if ($position === 'before') { $this->filtersClass[$position] = array_merge($filterClasses, $this->filtersClass[$position]); } else { $this->filtersClass[$position] = array_merge($this->filtersClass[$position], $filterClasses); } - - // Since some filters like rate limiters rely on being executed once a request we filter em here. - $this->filtersClass[$position] = array_values(array_unique($this->filtersClass[$position])); } /** diff --git a/tests/system/CodeIgniterTest.php b/tests/system/CodeIgniterTest.php index a550e098e9b0..e65ba03c25c0 100644 --- a/tests/system/CodeIgniterTest.php +++ b/tests/system/CodeIgniterTest.php @@ -16,7 +16,6 @@ use App\Controllers\Home; use CodeIgniter\Config\Services; use CodeIgniter\Debug\Timer; -use CodeIgniter\Exceptions\ConfigException; use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\HTTP\Method; use CodeIgniter\HTTP\Response; @@ -313,9 +312,6 @@ public function testRunExecuteFilterByClassName(): void public function testRegisterSameFilterTwiceWithDifferentArgument(): void { - $this->expectException(ConfigException::class); - $this->expectExceptionMessage('"test-customfilter" already has arguments: null'); - $_SERVER['argv'] = ['index.php', 'pages/about']; $_SERVER['argc'] = 2; @@ -343,7 +339,11 @@ public function testRegisterSameFilterTwiceWithDifferentArgument(): void ]; Services::filters($filterConfig); + ob_start(); $this->codeigniter->run(); + $output = ob_get_clean(); + + $this->assertStringContainsString('http://hellowworld.comhttp://hellowworld.com', $output); $this->resetServices(); } diff --git a/tests/system/Filters/FiltersTest.php b/tests/system/Filters/FiltersTest.php index 98260ad65339..553d185ff156 100644 --- a/tests/system/Filters/FiltersTest.php +++ b/tests/system/Filters/FiltersTest.php @@ -14,7 +14,6 @@ namespace CodeIgniter\Filters; use CodeIgniter\Config\Services; -use CodeIgniter\Exceptions\ConfigException; use CodeIgniter\Filters\Exceptions\FilterException; use CodeIgniter\Filters\fixtures\GoogleCurious; use CodeIgniter\Filters\fixtures\GoogleEmpty; @@ -75,7 +74,7 @@ protected function setUp(): void private function createFilters(FiltersConfig $config, $request = null): Filters { - $request ??= Services::request(); + $request ??= Services::incomingrequest(); return new Filters($config, $request, $this->response); } @@ -862,9 +861,7 @@ public function testFiltersWithArguments(): void $filters = $filters->initialize('admin/foo/bar'); $found = $filters->getFilters(); - $this->assertContains('role', $found['before']); - $this->assertSame(['admin', 'super'], $filters->getArguments('role')); - $this->assertSame(['role' => ['admin', 'super']], $filters->getArguments()); + $this->assertContains('role:admin,super', $found['before']); $response = $filters->run('admin/foo/bar', 'before'); @@ -875,11 +872,8 @@ public function testFiltersWithArguments(): void $this->assertSame('admin;super', $response->getBody()); } - public function testFilterWithArgumentsIsDefined(): void + public function testFilterWithDiffernetArguments(): void { - $this->expectException(ConfigException::class); - $this->expectExceptionMessage('"role" already has arguments: admin,super'); - $_SERVER['REQUEST_METHOD'] = 'GET'; $config = [ @@ -898,6 +892,10 @@ public function testFilterWithArgumentsIsDefined(): void $filters = $this->createFilters($filtersConfig); $filters->initialize('admin/user/bar'); + $found = $filters->getFilters(); + + $this->assertContains('role:admin,super', $found['before']); + $this->assertContains('role:super', $found['before']); } public function testFilterWithoutArgumentsIsDefined(): void @@ -923,8 +921,6 @@ public function testFilterWithoutArgumentsIsDefined(): void $found = $filters->getFilters(); $this->assertContains('role', $found['before']); - $this->assertSame(['super'], $filters->getArguments('role')); - $this->assertSame(['role' => ['super']], $filters->getArguments()); } public function testEnableFilterWithArguments(): void @@ -941,14 +937,11 @@ public function testEnableFilterWithArguments(): void $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config); $filters = $this->createFilters($filtersConfig); - $filters = $filters->initialize('admin/foo/bar'); $filters->enableFilters(['role:admin , super'], 'before'); $filters->enableFilters(['role:admin , super'], 'after'); $found = $filters->getFilters(); - $this->assertContains('role', $found['before']); - $this->assertSame(['admin', 'super'], $filters->getArguments('role')); - $this->assertSame(['role' => ['admin', 'super']], $filters->getArguments()); + $this->assertContains('role:admin,super', $found['before']); $response = $filters->run('admin/foo/bar', 'before'); @@ -973,7 +966,6 @@ public function testEnableFilterWithNoArguments(): void $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config); $filters = $this->createFilters($filtersConfig); - $filters = $filters->initialize('admin/foo/bar'); $filters->enableFilters(['role'], 'before'); $filters->enableFilters(['role'], 'after'); $found = $filters->getFilters(); @@ -1323,8 +1315,8 @@ public function testFilterClass(): void $expected = [ 'before' => [], 'after' => [ - Multiple1::class, - Multiple2::class, + [Multiple1::class, []], + [Multiple2::class, []], ], ]; $this->assertSame($expected, $filters->getFiltersClass()); From a6129c6e75731211156ff14cabfe1b94a320cfa9 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 20 Jun 2024 14:24:48 +0900 Subject: [PATCH 083/277] test: move Filter::initialize() after enableFilters() We should call enableFilters() before initialize() or run(). --- tests/system/Filters/FiltersTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/system/Filters/FiltersTest.php b/tests/system/Filters/FiltersTest.php index 553d185ff156..6d98894e1172 100644 --- a/tests/system/Filters/FiltersTest.php +++ b/tests/system/Filters/FiltersTest.php @@ -833,8 +833,8 @@ public function testEnableFilter(): void $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config); $filters = $this->createFilters($filtersConfig); - $filters = $filters->initialize('admin/foo/bar'); $filters->enableFilters(['google'], 'before'); + $filters = $filters->initialize('admin/foo/bar'); $filters = $filters->getFilters(); $this->assertContains('google', $filters['before']); @@ -997,8 +997,8 @@ public function testEnableNonFilter(): void $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config); $filters = $this->createFilters($filtersConfig); - $filters = $filters->initialize('admin/foo/bar'); $filters->enableFilters(['goggle'], 'before'); + $filters = $filters->initialize('admin/foo/bar'); } /** From 52ac7ae25021b4f3915014ebccebb548420c6f0e Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 20 Jun 2024 14:54:13 +0900 Subject: [PATCH 084/277] refactor: rename parameter names --- phpstan-baseline.php | 12 ------------ system/Filters/Filters.php | 13 +++++++++---- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/phpstan-baseline.php b/phpstan-baseline.php index 101313b73f3d..d55b67243b52 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -5335,18 +5335,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/Filters/Filters.php', ]; -$ignoreErrors[] = [ - // identifier: missingType.iterableValue - 'message' => '#^Method CodeIgniter\\\\Filters\\\\Filters\\:\\:runAfter\\(\\) has parameter \\$filterClasses with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Filters/Filters.php', -]; -$ignoreErrors[] = [ - // identifier: missingType.iterableValue - 'message' => '#^Method CodeIgniter\\\\Filters\\\\Filters\\:\\:runBefore\\(\\) has parameter \\$filterClasses with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Filters/Filters.php', -]; $ignoreErrors[] = [ // identifier: missingType.iterableValue 'message' => '#^Method CodeIgniter\\\\Filters\\\\Filters\\:\\:setToolbarToLast\\(\\) return type has no value type specified in iterable type array\\.$#', diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index 49ca13a99f3a..d3cc0deb6b44 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -220,11 +220,13 @@ public function run(string $uri, string $position = 'before') } /** + * @param list}> $filterClassList [[classname, arguments], ...] + * * @return RequestInterface|ResponseInterface|string */ - private function runBefore(array $filterClasses) + private function runBefore(array $filterClassList) { - foreach ($filterClasses as $filterClassInfo) { + foreach ($filterClassList as $filterClassInfo) { $className = $filterClassInfo[0]; $arguments = ($filterClassInfo[1] === []) ? null : $filterClassInfo[1]; @@ -260,9 +262,12 @@ private function runBefore(array $filterClasses) return $this->request; } - private function runAfter(array $filterClasses): ResponseInterface + /** + * @param list}> $filterClassList [[classname, arguments], ...] + */ + private function runAfter(array $filterClassList): ResponseInterface { - foreach ($filterClasses as $filterClassInfo) { + foreach ($filterClassList as $filterClassInfo) { $className = $filterClassInfo[0]; $arguments = ($filterClassInfo[1] === []) ? null : $filterClassInfo[1]; From 63a3c6004f77ba18d8e6ab6e2db9c7f4c11580bb Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 20 Jun 2024 15:22:29 +0900 Subject: [PATCH 085/277] feat: do not instantiate the same filter class more than once --- system/Filters/Filters.php | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index d3cc0deb6b44..d72a28096479 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -118,6 +118,13 @@ class Filters 'after' => [], ]; + /** + * List of filter class instances. + * + * @var array [classname => instance] + */ + protected array $filterClassInstances = []; + /** * Any arguments to be passed to filters. * @@ -230,7 +237,17 @@ private function runBefore(array $filterClassList) $className = $filterClassInfo[0]; $arguments = ($filterClassInfo[1] === []) ? null : $filterClassInfo[1]; - $class = new $className(); + if (isset($this->filterClassInstances[$className])) { + $class = $this->filterClassInstances[$className]; + } else { + $class = new $className(); + + if (! $class instanceof FilterInterface) { + throw FilterException::forIncorrectInterface($class::class); + } + + $this->filterClassInstances[$className] = $class; + } if (! $class instanceof FilterInterface) { throw FilterException::forIncorrectInterface($class::class); @@ -271,10 +288,16 @@ private function runAfter(array $filterClassList): ResponseInterface $className = $filterClassInfo[0]; $arguments = ($filterClassInfo[1] === []) ? null : $filterClassInfo[1]; - $class = new $className(); + if (isset($this->filterClassInstances[$className])) { + $class = $this->filterClassInstances[$className]; + } else { + $class = new $className(); - if (! $class instanceof FilterInterface) { - throw FilterException::forIncorrectInterface($class::class); + if (! $class instanceof FilterInterface) { + throw FilterException::forIncorrectInterface($class::class); + } + + $this->filterClassInstances[$className] = $class; } $result = $class->after($this->request, $this->response, $arguments); From 7d1a10a73395165c6648b1899ed371b98d403703 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 20 Jun 2024 15:30:02 +0900 Subject: [PATCH 086/277] refactor: extract method --- system/Filters/Filters.php | 52 ++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index d72a28096479..300a23f6bfa9 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -237,23 +237,9 @@ private function runBefore(array $filterClassList) $className = $filterClassInfo[0]; $arguments = ($filterClassInfo[1] === []) ? null : $filterClassInfo[1]; - if (isset($this->filterClassInstances[$className])) { - $class = $this->filterClassInstances[$className]; - } else { - $class = new $className(); - - if (! $class instanceof FilterInterface) { - throw FilterException::forIncorrectInterface($class::class); - } - - $this->filterClassInstances[$className] = $class; - } - - if (! $class instanceof FilterInterface) { - throw FilterException::forIncorrectInterface($class::class); - } + $instance = $this->createFilter($className); - $result = $class->before($this->request, $arguments); + $result = $instance->before($this->request, $arguments); if ($result instanceof RequestInterface) { $this->request = $result; @@ -288,19 +274,9 @@ private function runAfter(array $filterClassList): ResponseInterface $className = $filterClassInfo[0]; $arguments = ($filterClassInfo[1] === []) ? null : $filterClassInfo[1]; - if (isset($this->filterClassInstances[$className])) { - $class = $this->filterClassInstances[$className]; - } else { - $class = new $className(); - - if (! $class instanceof FilterInterface) { - throw FilterException::forIncorrectInterface($class::class); - } - - $this->filterClassInstances[$className] = $class; - } + $instance = $this->createFilter($className); - $result = $class->after($this->request, $this->response, $arguments); + $result = $instance->after($this->request, $this->response, $arguments); if ($result instanceof ResponseInterface) { $this->response = $result; @@ -312,6 +288,26 @@ private function runAfter(array $filterClassList): ResponseInterface return $this->response; } + /** + * @param class-string $className + */ + private function createFilter(string $className): FilterInterface + { + if (isset($this->filterClassInstances[$className])) { + return $this->filterClassInstances[$className]; + } + + $instance = new $className(); + + if (! $instance instanceof FilterInterface) { + throw FilterException::forIncorrectInterface($instance::class); + } + + $this->filterClassInstances[$className] = $instance; + + return $instance; + } + /** * Runs "Required Filters" for the specified position. * From 237ed10f3b11e50989947d8e35faf2f13bef2bfa Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 20 Jun 2024 15:45:47 +0900 Subject: [PATCH 087/277] refactor: replace if with ternary operator --- system/Filters/Filters.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index 300a23f6bfa9..a89afcf147df 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -562,11 +562,7 @@ private function enableFilter(string $name, string $when = 'before'): void { // Normalize the arguments. [$alias, $arguments] = $this->getCleanName($name); - if ($arguments === []) { - $filter = $alias; - } else { - $filter = $alias . ':' . implode(',', $arguments); - } + $filter = ($arguments === []) ? $alias : $alias . ':' . implode(',', $arguments); if (class_exists($alias)) { $this->config->aliases[$alias] = $alias; From ab5dc662f8403b016dd7a1c4abcaba69f696e463 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 20 Jun 2024 15:46:28 +0900 Subject: [PATCH 088/277] test: remove unneeded variable --- tests/system/Filters/FiltersTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/system/Filters/FiltersTest.php b/tests/system/Filters/FiltersTest.php index 6d98894e1172..003e6f7c5cbf 100644 --- a/tests/system/Filters/FiltersTest.php +++ b/tests/system/Filters/FiltersTest.php @@ -998,7 +998,7 @@ public function testEnableNonFilter(): void $filters = $this->createFilters($filtersConfig); $filters->enableFilters(['goggle'], 'before'); - $filters = $filters->initialize('admin/foo/bar'); + $filters->initialize('admin/foo/bar'); } /** From 6bf2c097fc4ea95c84efc070185048fe7aae71de Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 21 Jun 2024 09:15:41 +0900 Subject: [PATCH 089/277] docs: update doc comment --- system/Filters/Filters.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index a89afcf147df..972257ac69ab 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -94,8 +94,8 @@ class Filters ]; /** - * The collection of filters' class names to execute for the current request - * (URI path). + * The collection of filter class names and its arguments to execute for the + * current request (URI path). * * This does not include "Required Filters". * From 44db01c88d8e94adeebd17d0a5dbd11bce8d2255 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 21 Jun 2024 10:08:00 +0900 Subject: [PATCH 090/277] docs: add user guide --- user_guide_src/source/changelogs/v4.6.0.rst | 18 ++++++++++++++++++ user_guide_src/source/incoming/filters.rst | 3 +++ user_guide_src/source/incoming/routing.rst | 3 ++- .../source/installation/upgrade_460.rst | 18 ++++++++++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index 06e0f32a10e3..b6b82a947b43 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -43,6 +43,13 @@ The following breaking changes have been made accordingly: - ``TestException`` now extends ``CodeIgniter\Exceptions\LogicException`` instead of ``CodeIgniter\Exceptions\CriticalError``. +Filters Changes +--------------- + +The ``Filters`` class has been changed to allow multiple runs of the same filter +with different arguments in before or after. See +:ref:`Upgrading Guide ` for details. + .. _v460-interface-changes: Interface Changes @@ -106,6 +113,9 @@ The following new Exception interfaces have been added: Commands ======== +- The ``spark routes`` and ``spark filter:check`` commands now display filter + arguments. + Testing ======= @@ -138,6 +148,9 @@ Helpers and Functions Others ====== +- **Filters:** Now you can execute a filter more than once with the different + arguments in before or after. + *************** Message Changes *************** @@ -176,6 +189,11 @@ The following changes have been made accordingly: Deprecations ************ +- **Filters:** + - The properties ``$arguments`` and ``$argumentsClass`` of ``Filters`` have + been deprecated. No longer used. + - The ``Filters::getArguments()`` method has been deprecated. No longer used. + ********** Bugs Fixed ********** diff --git a/user_guide_src/source/incoming/filters.rst b/user_guide_src/source/incoming/filters.rst index 4425ce03d3bf..0697e706e09d 100644 --- a/user_guide_src/source/incoming/filters.rst +++ b/user_guide_src/source/incoming/filters.rst @@ -226,6 +226,9 @@ will be passed in ``$arguments`` to the ``group`` filter's ``before()`` methods. When the URI matches ``admin/users/*'``, the array ``['users.manage']`` will be passed in ``$arguments`` to the ``permission`` filter's ``before()`` methods. +.. note:: Prior to v4.6.0, the same filter cannot be run multiple times with + different arguments. + .. _filter-execution-order: Filter Execution Order diff --git a/user_guide_src/source/incoming/routing.rst b/user_guide_src/source/incoming/routing.rst index 867983437acd..3ab7a5848bc9 100644 --- a/user_guide_src/source/incoming/routing.rst +++ b/user_guide_src/source/incoming/routing.rst @@ -589,7 +589,8 @@ The ``filter`` option passed to the outer ``group()`` are merged with the inner The above code runs ``myfilter1:config`` for the route ``admin``, and ``myfilter1:config`` and ``myfilter2:region`` for the route ``admin/users/list``. -.. note:: The same filter cannot be run multiple times with different arguments. +.. note:: Prior to v4.6.0, the same filter cannot be run multiple times with + different arguments. Any other overlapping options passed to the inner ``group()`` will overwrite their values. diff --git a/user_guide_src/source/installation/upgrade_460.rst b/user_guide_src/source/installation/upgrade_460.rst index 0aa5882e889d..04388acf7d08 100644 --- a/user_guide_src/source/installation/upgrade_460.rst +++ b/user_guide_src/source/installation/upgrade_460.rst @@ -54,6 +54,24 @@ See :ref:`ChangeLog ` for details. Breaking Enhancements ********************* +.. _upgrade-460-filters-changes: + +Filters Changes +=============== + +The ``Filters`` class has been changed to allow multiple runs of the same filter +with different arguments in before or after. + +If you are extending ``Filters``, you will need to modify it to conform to the +following changes: + +- The structure of the array properties ``$filters`` and ``$filtersClasses`` have + been changed. +- The properties ``$arguments`` and ``$argumentsClass`` are no longer used. +- ``Filters`` has been changed so that the same filter class is not instantiated + multiple times. If a filter class is used both before and after, the same instance + is used. + ************* Project Files ************* From 56775d06606f8450f06750d993134bde00fb5eef Mon Sep 17 00:00:00 2001 From: ducng99 Date: Fri, 21 Jun 2024 21:13:44 +1200 Subject: [PATCH 091/277] docs: updated to suggested changes Also add an ignore in phpstan to ignore missing foundRows in BaseConnection --- phpstan-baseline.php | 6 ++++++ system/Database/BaseConnection.php | 1 - system/Database/MySQLi/Connection.php | 2 +- user_guide_src/source/changelogs/v4.6.0.rst | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/phpstan-baseline.php b/phpstan-baseline.php index 05825b1d76ed..9e1f1433acfc 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -13459,6 +13459,12 @@ 'count' => 2, 'path' => __DIR__ . '/tests/system/Database/Live/MySQLi/NumberNativeTest.php', ]; +$ignoreErrors[] = [ + // identifier: property.notFound + 'message' => '#^Access to an undefined property CodeIgniter\\\\Database\\\\BaseConnection\\:\\:\\$foundRows\\.$#', + 'count' => 2, + 'path' => __DIR__ . '/tests/system/Database/Live/MySQLi/FoundRowsTest.php', +]; $ignoreErrors[] = [ // identifier: missingType.property 'message' => '#^Property CodeIgniter\\\\Database\\\\Live\\\\MySQLi\\\\NumberNativeTest\\:\\:\\$tests has no type specified\\.$#', diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index bf18abe12f42..57c37d8a0a6c 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -35,7 +35,6 @@ * @property-read string $DSN * @property-read array|bool $encrypt * @property-read array $failover - * @property-read bool $foundRows * @property-read string $hostname * @property-read Query $lastQuery * @property-read string $password diff --git a/system/Database/MySQLi/Connection.php b/system/Database/MySQLi/Connection.php index c04d9e364432..3868905dca27 100644 --- a/system/Database/MySQLi/Connection.php +++ b/system/Database/MySQLi/Connection.php @@ -84,7 +84,7 @@ class Connection extends BaseConnection /** * Use MYSQLI_CLIENT_FOUND_ROWS * - * Whether affected_rows should return number of rows found, + * Whether affectedRows() should return number of rows found, * or number of rows changed, after an UPDATE query. * * @var bool diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index a181a4f95730..09bc4d413fed 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -121,7 +121,7 @@ Forge Others ------ -- Added a new configuration `foundRows` for MySQLi to use `MYSQLI_CLIENT_FOUND_ROWS`. +- Added a new configuration ``foundRows`` for MySQLi to use ``MYSQLI_CLIENT_FOUND_ROWS``. Model ===== From 16fe250b7b893a7d0e24bca28665bc6ac26cc41c Mon Sep 17 00:00:00 2001 From: ducng99 Date: Fri, 21 Jun 2024 21:28:21 +1200 Subject: [PATCH 092/277] test: moved DBDriver check to setUp() instead of checking in each test --- .../Database/Live/MySQLi/FoundRowsTest.php | 36 +++---------------- 1 file changed, 4 insertions(+), 32 deletions(-) diff --git a/tests/system/Database/Live/MySQLi/FoundRowsTest.php b/tests/system/Database/Live/MySQLi/FoundRowsTest.php index 011830088d44..4e5299f8d7aa 100644 --- a/tests/system/Database/Live/MySQLi/FoundRowsTest.php +++ b/tests/system/Database/Live/MySQLi/FoundRowsTest.php @@ -42,6 +42,10 @@ protected function setUp(): void $config = config('Database'); $this->tests = $config->tests; + + if ($this->tests['DBDriver'] !== 'MySQLi') { + $this->markTestSkipped('Only MySQLi can complete this test.'); + } } public function testEnableFoundRows(): void @@ -50,10 +54,6 @@ public function testEnableFoundRows(): void $db1 = Database::connect($this->tests); - if ($db1->DBDriver !== 'MySQLi') { - $this->markTestSkipped('Only MySQLi can complete this test.'); - } - $this->assertTrue($db1->foundRows); } @@ -63,10 +63,6 @@ public function testDisableFoundRows(): void $db1 = Database::connect($this->tests); - if ($db1->DBDriver !== 'MySQLi') { - $this->markTestSkipped('Only MySQLi can complete this test.'); - } - $this->assertFalse($db1->foundRows); } @@ -76,10 +72,6 @@ public function testAffectedRowsAfterEnableFoundRowsWithNoChange(): void $db1 = Database::connect($this->tests); - if ($db1->DBDriver !== 'MySQLi') { - $this->markTestSkipped('Only MySQLi can complete this test.'); - } - $db1->table('db_user') ->set('country', 'US') ->where('country', 'US') @@ -96,10 +88,6 @@ public function testAffectedRowsAfterDisableFoundRowsWithNoChange(): void $db1 = Database::connect($this->tests); - if ($db1->DBDriver !== 'MySQLi') { - $this->markTestSkipped('Only MySQLi can complete this test.'); - } - $db1->table('db_user') ->set('country', 'US') ->where('country', 'US') @@ -116,10 +104,6 @@ public function testAffectedRowsAfterEnableFoundRowsWithChange(): void $db1 = Database::connect($this->tests); - if ($db1->DBDriver !== 'MySQLi') { - $this->markTestSkipped('Only MySQLi can complete this test.'); - } - $db1->table('db_user') ->set('country', 'NZ') ->where('country', 'US') @@ -136,10 +120,6 @@ public function testAffectedRowsAfterDisableFoundRowsWithChange(): void $db1 = Database::connect($this->tests); - if ($db1->DBDriver !== 'MySQLi') { - $this->markTestSkipped('Only MySQLi can complete this test.'); - } - $db1->table('db_user') ->set('country', 'NZ') ->where('country', 'US') @@ -156,10 +136,6 @@ public function testAffectedRowsAfterEnableFoundRowsWithPartialChange(): void $db1 = Database::connect($this->tests); - if ($db1->DBDriver !== 'MySQLi') { - $this->markTestSkipped('Only MySQLi can complete this test.'); - } - $db1->table('db_user') ->set('name', 'Derek Jones') ->where('country', 'US') @@ -176,10 +152,6 @@ public function testAffectedRowsAfterDisableFoundRowsWithPartialChange(): void $db1 = Database::connect($this->tests); - if ($db1->DBDriver !== 'MySQLi') { - $this->markTestSkipped('Only MySQLi can complete this test.'); - } - $db1->table('db_user') ->set('name', 'Derek Jones') ->where('country', 'US') From a0a2cef757f96de394ba042c60adbc662db85c2a Mon Sep 17 00:00:00 2001 From: christianberkman <39840601+christianberkman@users.noreply.github.com> Date: Sat, 22 Jun 2024 12:10:37 +0300 Subject: [PATCH 093/277] fix: add validation message for min_dims --- system/Language/en/Validation.php | 1 + 1 file changed, 1 insertion(+) diff --git a/system/Language/en/Validation.php b/system/Language/en/Validation.php index ba5b413e79f1..facf512d559a 100644 --- a/system/Language/en/Validation.php +++ b/system/Language/en/Validation.php @@ -74,4 +74,5 @@ 'mime_in' => '{field} does not have a valid mime type.', 'ext_in' => '{field} does not have a valid file extension.', 'max_dims' => '{field} is either not an image, or it is too wide or tall.', + 'min_dims' => '{field} is either not an image, or it is not wide or tall enough.', ]; From f9ae540068a5dc04553a4a744db038d37d0f9a03 Mon Sep 17 00:00:00 2001 From: christianberkman <39840601+christianberkman@users.noreply.github.com> Date: Sat, 22 Jun 2024 12:42:17 +0300 Subject: [PATCH 094/277] docs: update changelog --- user_guide_src/source/changelogs/v4.6.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index 06e0f32a10e3..6a355ef63ee2 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -141,6 +141,7 @@ Others *************** Message Changes *************** +- Added `Validation.min_dims` message ******* Changes From dba74435d6c283228c18ebdd57ed985605b6800a Mon Sep 17 00:00:00 2001 From: christianberkman <39840601+christianberkman@users.noreply.github.com> Date: Sat, 22 Jun 2024 19:29:25 +0300 Subject: [PATCH 095/277] Update user_guide_src/source/changelogs/v4.6.0.rst Co-authored-by: kenjis --- user_guide_src/source/changelogs/v4.6.0.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index 6a355ef63ee2..d27a48399464 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -141,7 +141,8 @@ Others *************** Message Changes *************** -- Added `Validation.min_dims` message + +- Added ``Validation.min_dims`` message ******* Changes From 2a4880839b700a6d2f31abb880440754b68f5e7f Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 22 Jun 2024 10:25:13 +0900 Subject: [PATCH 096/277] feat: add Filters::getRequiredClasses() method --- system/Filters/Filters.php | 42 +++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index 972257ac69ab..62684b46656a 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -309,22 +309,18 @@ private function createFilter(string $className): FilterInterface } /** - * Runs "Required Filters" for the specified position. - * - * @return RequestInterface|ResponseInterface|string|null + * Returns the "Required Filters" class list. * * @phpstan-param 'before'|'after' $position * - * @throws FilterException - * - * @internal + * @return list}> [[classname, arguments], ...] */ - public function runRequired(string $position = 'before') + public function getRequiredClasses(string $position): array { [$filters, $aliases] = $this->getRequiredFilters($position); if ($filters === []) { - return $position === 'before' ? $this->request : $this->response; + return []; } $filterClasses = []; @@ -332,19 +328,41 @@ public function runRequired(string $position = 'before') foreach ($filters as $alias) { if (is_array($aliases[$alias])) { foreach ($this->config->aliases[$alias] as $class) { - $filterClasses[$position][] = [$class, []]; + $filterClasses[] = [$class, []]; } } else { - $filterClasses[$position][] = [$aliases[$alias], []]; + $filterClasses[] = [$aliases[$alias], []]; } } + return $filterClasses; + } + + /** + * Runs "Required Filters" for the specified position. + * + * @phpstan-param 'before'|'after' $position + * + * @return RequestInterface|ResponseInterface|string|null + * + * @throws FilterException + * + * @internal + */ + public function runRequired(string $position = 'before') + { + $filterClasses = $this->getRequiredClasses($position); + + if ($filterClasses === []) { + return $position === 'before' ? $this->request : $this->response; + } + if ($position === 'before') { - return $this->runBefore($filterClasses[$position]); + return $this->runBefore($filterClasses); } // After - return $this->runAfter($filterClasses[$position]); + return $this->runAfter($filterClasses); } /** From 851e9382fd46556c3eae0ae7a661ab96f5960b44 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 22 Jun 2024 10:33:02 +0900 Subject: [PATCH 097/277] refactor: rename parameter names --- system/Filters/Filters.php | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index 62684b46656a..0ae058ff96eb 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -545,9 +545,11 @@ public function getFiltersClass(): array * MUST be called prior to initialize(); * Intended for use within routes files. * + * @phpstan-param 'before'|'after' $position + * * @return $this */ - public function addFilter(string $class, ?string $alias = null, string $when = 'before', string $section = 'globals') + public function addFilter(string $class, ?string $alias = null, string $position = 'before', string $section = 'globals') { $alias ??= md5($class); @@ -555,13 +557,13 @@ public function addFilter(string $class, ?string $alias = null, string $when = ' $this->config->{$section} = []; } - if (! isset($this->config->{$section}[$when])) { - $this->config->{$section}[$when] = []; + if (! isset($this->config->{$section}[$position])) { + $this->config->{$section}[$position] = []; } $this->config->aliases[$alias] = $class; - $this->config->{$section}[$when][] = $alias; + $this->config->{$section}[$position][] = $alias; return $this; } @@ -573,10 +575,11 @@ public function addFilter(string $class, ?string $alias = null, string $when = ' * after the filter name, followed by a comma-separated list of arguments that * are passed to the filter when executed. * - * @param string $name filter_name or filter_name:arguments like 'role:admin,manager' - * or filter classname. + * @param string $name filter_name or filter_name:arguments like 'role:admin,manager' + * or filter classname. + * @phpstan-param 'before'|'after' $position */ - private function enableFilter(string $name, string $when = 'before'): void + private function enableFilter(string $name, string $position = 'before'): void { // Normalize the arguments. [$alias, $arguments] = $this->getCleanName($name); @@ -588,13 +591,13 @@ private function enableFilter(string $name, string $when = 'before'): void throw FilterException::forNoAlias($alias); } - if (! isset($this->filters[$when][$filter])) { - $this->filters[$when][] = $filter; + if (! isset($this->filters[$position][$filter])) { + $this->filters[$position][] = $filter; } // Since some filters like rate limiters rely on being executed once a request, // we filter em here. - $this->filters[$when] = array_unique($this->filters[$when]); + $this->filters[$position] = array_unique($this->filters[$position]); } /** @@ -816,6 +819,8 @@ protected function processFilters(?string $uri = null) /** * Maps filter aliases to the equivalent filter classes * + * @phpstan-param 'before'|'after' $position + * * @return void * * @throws FilterException From ad68b6c68b1ad982552cb906729f3f7964be313f Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 22 Jun 2024 10:49:55 +0900 Subject: [PATCH 098/277] docs: update doc comments --- system/Filters/Filters.php | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index 0ae058ff96eb..91acea9d180f 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -29,7 +29,7 @@ class Filters { /** - * The original config file + * The Config\Filters instance * * @var FiltersConfig */ @@ -50,15 +50,14 @@ class Filters protected $response; /** - * Handle to the modules config. + * The Config\Modules instance * * @var Modules */ protected $modules; /** - * Whether we've done initial processing - * on the filter lists. + * Whether we've done initial processing on the filter lists. * * @var bool */ @@ -94,8 +93,8 @@ class Filters ]; /** - * The collection of filter class names and its arguments to execute for the - * current request (URI path). + * The collection of filter classnames and their arguments to execute for + * the current request (URI path). * * This does not include "Required Filters". * @@ -163,10 +162,10 @@ public function __construct($config, RequestInterface $request, ResponseInterfac /** * If discoverFilters is enabled in Config then system will try to - * auto-discover custom filters files in Namespaces and allow access to - * the config object via the variable $filters as with the routes file + * auto-discover custom filters files in namespaces and allow access to + * the config object via the variable $filters as with the routes file. * - * Sample : + * Sample: * $filters->aliases['custom-auth'] = \Acme\Blob\Filters\BlobAuth::class; * * @deprecated 4.4.2 Use Registrar instead. @@ -204,8 +203,8 @@ public function setResponse(ResponseInterface $response) } /** - * Runs through all of the filters for the specified - * uri and position. + * Runs through all the filters (except "Required Filters") for the specified + * URI and position. * * @param string $uri URI path relative to baseURL * @phpstan-param 'before'|'after' $position From e673a0e8c01bc0ea3541f17b538a3bd7ace043dc Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 22 Jun 2024 10:50:20 +0900 Subject: [PATCH 099/277] refactor: renamve variable names --- system/Filters/Filters.php | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index 91acea9d180f..9cfd6bca793b 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -322,19 +322,19 @@ public function getRequiredClasses(string $position): array return []; } - $filterClasses = []; + $filterClassList = []; foreach ($filters as $alias) { if (is_array($aliases[$alias])) { foreach ($this->config->aliases[$alias] as $class) { - $filterClasses[] = [$class, []]; + $filterClassList[] = [$class, []]; } } else { - $filterClasses[] = [$aliases[$alias], []]; + $filterClassList[] = [$aliases[$alias], []]; } } - return $filterClasses; + return $filterClassList; } /** @@ -350,18 +350,18 @@ public function getRequiredClasses(string $position): array */ public function runRequired(string $position = 'before') { - $filterClasses = $this->getRequiredClasses($position); + $filterClassList = $this->getRequiredClasses($position); - if ($filterClasses === []) { + if ($filterClassList === []) { return $position === 'before' ? $this->request : $this->response; } if ($position === 'before') { - return $this->runBefore($filterClasses); + return $this->runBefore($filterClassList); } // After - return $this->runAfter($filterClasses); + return $this->runAfter($filterClassList); } /** @@ -826,7 +826,7 @@ protected function processFilters(?string $uri = null) */ protected function processAliasesToClass(string $position) { - $filterClasses = []; + $filterClassList = []; foreach ($this->filters[$position] as $filter) { // Get arguments and clean alias @@ -838,17 +838,17 @@ protected function processAliasesToClass(string $position) if (is_array($this->config->aliases[$alias])) { foreach ($this->config->aliases[$alias] as $class) { - $filterClasses[] = [$class, $arguments]; + $filterClassList[] = [$class, $arguments]; } } else { - $filterClasses[] = [$this->config->aliases[$alias], $arguments]; + $filterClassList[] = [$this->config->aliases[$alias], $arguments]; } } if ($position === 'before') { - $this->filtersClass[$position] = array_merge($filterClasses, $this->filtersClass[$position]); + $this->filtersClass[$position] = array_merge($filterClassList, $this->filtersClass[$position]); } else { - $this->filtersClass[$position] = array_merge($this->filtersClass[$position], $filterClasses); + $this->filtersClass[$position] = array_merge($this->filtersClass[$position], $filterClassList); } } From eb95a96ea734a58a3c1d63f27ca4eb3140416ae4 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 22 Jun 2024 11:12:19 +0900 Subject: [PATCH 100/277] feat: `spark filter:check` shows filter classnames --- system/Commands/Utilities/FilterCheck.php | 22 +++++ .../Utilities/Routes/FilterCollector.php | 64 ++++++++++++- .../Utilities/Routes/FilterFinder.php | 89 +++++++++++++++++-- .../Utilities/Routes/FilterFinderTest.php | 36 ++++++++ 4 files changed, 203 insertions(+), 8 deletions(-) diff --git a/system/Commands/Utilities/FilterCheck.php b/system/Commands/Utilities/FilterCheck.php index 56fcdb00b971..c489f20d9e4f 100644 --- a/system/Commands/Utilities/FilterCheck.php +++ b/system/Commands/Utilities/FilterCheck.php @@ -125,6 +125,28 @@ public function run(array $params) CLI::table($tbody, $thead); + // Show filter classes + $requiredFilterClasses = $filterCollector->getRequiredFilterClasses(); + $filterClasses = $filterCollector->getClasses($method, $route); + + foreach ($requiredFilterClasses as $position => $classes) { + foreach ($classes as $class) { + $class = CLI::color($class, 'yellow'); + + $coloredRequiredFilterClasses[$position][] = $class; + } + } + + $classList = [ + 'before' => array_merge($coloredRequiredFilterClasses['before'], $filterClasses['before']), + 'after' => array_merge($filterClasses['after'], $coloredRequiredFilterClasses['after']), + ]; + + foreach ($classList as $position => $classes) { + CLI::write(ucfirst($position) . ' Filter Classes:', 'cyan'); + CLI::write(implode(' → ', $classes)); + } + return EXIT_SUCCESS; } diff --git a/system/Commands/Utilities/Routes/FilterCollector.php b/system/Commands/Utilities/Routes/FilterCollector.php index 002a529b953e..dd1015838851 100644 --- a/system/Commands/Utilities/Routes/FilterCollector.php +++ b/system/Commands/Utilities/Routes/FilterCollector.php @@ -43,7 +43,7 @@ public function __construct( * @param string $method HTTP verb like `GET`,`POST` or `CLI`. * @param string $uri URI path to find filters for * - * @return array{before: list, after: list} array of filter alias or classname + * @return array{before: list, after: list} array of alias/classname:args */ public function get(string $method, string $uri): array { @@ -79,10 +79,52 @@ public function get(string $method, string $uri): array return $finder->find($uri); } + /** + * Returns filter classes for the URI + * + * @param string $method HTTP verb like `GET`,`POST` or `CLI`. + * @param string $uri URI path to find filters for + * + * @return array{before: list, after: list} array of classname:args + */ + public function getClasses(string $method, string $uri): array + { + if ($method === strtolower($method)) { + @trigger_error( + 'Passing lowercase HTTP method "' . $method . '" is deprecated.' + . ' Use uppercase HTTP method like "' . strtoupper($method) . '".', + E_USER_DEPRECATED + ); + } + + /** + * @deprecated 4.5.0 + * @TODO Remove this in the future. + */ + $method = strtoupper($method); + + if ($method === 'CLI') { + return [ + 'before' => [], + 'after' => [], + ]; + } + + $request = Services::incomingrequest(null, false); + $request->setMethod($method); + + $router = $this->createRouter($request); + $filters = $this->createFilters($request); + + $finder = new FilterFinder($router, $filters); + + return $finder->findClasses($uri); + } + /** * Returns Required Filters * - * @return array{before: list, after: list} array of filter alias or classname + * @return array{before: list, after: list} array of aliases */ public function getRequiredFilters(): array { @@ -97,6 +139,24 @@ public function getRequiredFilters(): array return $finder->getRequiredFilters(); } + /** + * Returns Required Filter class list + * + * @return array{before: list, after: list} array of classnames + */ + public function getRequiredFilterClasses(): array + { + $request = Services::incomingrequest(null, false); + $request->setMethod(Method::GET); + + $router = $this->createRouter($request); + $filters = $this->createFilters($request); + + $finder = new FilterFinder($router, $filters); + + return $finder->getRequiredFilterClasses(); + } + private function createRouter(Request $request): Router { $routes = service('routes'); diff --git a/system/Commands/Utilities/Routes/FilterFinder.php b/system/Commands/Utilities/Routes/FilterFinder.php index 7971e5c1be84..f81995240291 100644 --- a/system/Commands/Utilities/Routes/FilterFinder.php +++ b/system/Commands/Utilities/Routes/FilterFinder.php @@ -46,23 +46,20 @@ private function getRouteFilters(string $uri): array /** * @param string $uri URI path to find filters for * - * @return array{before: list, after: list} array of filter alias or classname + * @return array{before: list, after: list} array of alias/classname:args */ public function find(string $uri): array { $this->filters->reset(); - // Add route filters try { + // Add route filters $routeFilters = $this->getRouteFilters($uri); - $this->filters->enableFilters($routeFilters, 'before'); - $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; if (! $oldFilterOrder) { $routeFilters = array_reverse($routeFilters); } - $this->filters->enableFilters($routeFilters, 'after'); $this->filters->initialize($uri); @@ -81,10 +78,66 @@ public function find(string $uri): array } } + /** + * @param string $uri URI path to find filters for + * + * @return array{before: list, after: list} array of classname:args + */ + public function findClasses(string $uri): array + { + $this->filters->reset(); + + try { + // Add route filters + $routeFilters = $this->getRouteFilters($uri); + $this->filters->enableFilters($routeFilters, 'before'); + $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; + if (! $oldFilterOrder) { + $routeFilters = array_reverse($routeFilters); + } + $this->filters->enableFilters($routeFilters, 'after'); + + $this->filters->initialize($uri); + + $filterClassList = $this->filters->getFiltersClass(); + + $filterClasses = [ + 'before' => [], + 'after' => [], + ]; + + foreach ($filterClassList['before'] as $classInfo) { + $classWithArguments = ($classInfo[1] === []) ? $classInfo[0] + : $classInfo[0] . ':' . implode(',', $classInfo[1]); + + $filterClasses['before'][] = $classWithArguments; + } + + foreach ($filterClassList['after'] as $classInfo) { + $classWithArguments = ($classInfo[1] === []) ? $classInfo[0] + : $classInfo[0] . ':' . implode(',', $classInfo[1]); + + $filterClasses['after'][] = $classWithArguments; + } + + return $filterClasses; + } catch (RedirectException) { + return [ + 'before' => [], + 'after' => [], + ]; + } catch (BadRequestException|PageNotFoundException) { + return [ + 'before' => [''], + 'after' => [''], + ]; + } + } + /** * Returns Required Filters * - * @return array{before: list, after:list} + * @return array{before: list, after:list} array of aliases */ public function getRequiredFilters(): array { @@ -96,4 +149,28 @@ public function getRequiredFilters(): array 'after' => $requiredAfter, ]; } + + /** + * Returns Required Filter classes + * + * @return array{before: list, after:list} + */ + public function getRequiredFilterClasses(): array + { + $before = $this->filters->getRequiredClasses('before'); + $after = $this->filters->getRequiredClasses('after'); + + foreach ($before as $classInfo) { + $requiredBefore[] = $classInfo[0]; + } + + foreach ($after as $classInfo) { + $requiredAfter[] = $classInfo[0]; + } + + return [ + 'before' => $requiredBefore, + 'after' => $requiredAfter, + ]; + } } diff --git a/tests/system/Commands/Utilities/Routes/FilterFinderTest.php b/tests/system/Commands/Utilities/Routes/FilterFinderTest.php index d96571c6b2f2..a51c3db08634 100644 --- a/tests/system/Commands/Utilities/Routes/FilterFinderTest.php +++ b/tests/system/Commands/Utilities/Routes/FilterFinderTest.php @@ -154,6 +154,42 @@ public function testFindGlobalsAndRouteFilters(): void $this->assertSame($expected, $filters); } + public function testFindGlobalsAndRouteFiltersWithArguments(): void + { + $collection = $this->createRouteCollection(); + $collection->get('admin', ' AdminController::index', ['filter' => 'honeypot:arg1,arg2']); + $router = $this->createRouter($collection); + $filters = $this->createFilters(); + + $finder = new FilterFinder($router, $filters); + + $filters = $finder->find('admin'); + + $expected = [ + 'before' => ['csrf', 'honeypot:arg1,arg2'], + 'after' => ['honeypot:arg1,arg2', 'toolbar'], + ]; + $this->assertSame($expected, $filters); + } + + public function testFindClassesGlobalsAndRouteFiltersWithArguments(): void + { + $collection = $this->createRouteCollection(); + $collection->get('admin', ' AdminController::index', ['filter' => 'honeypot:arg1,arg2']); + $router = $this->createRouter($collection); + $filters = $this->createFilters(); + + $finder = new FilterFinder($router, $filters); + + $filters = $finder->findClasses('admin'); + + $expected = [ + 'before' => [CSRF::class, Honeypot::class . ':arg1,arg2'], + 'after' => [Honeypot::class . ':arg1,arg2', DebugToolbar::class], + ]; + $this->assertSame($expected, $filters); + } + public function testFindGlobalsAndRouteClassnameFilters(): void { $collection = $this->createRouteCollection(); From 5e9d3b89f49dac6815a4d574f7cd356ad86a2b23 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 22 Jun 2024 11:25:37 +0900 Subject: [PATCH 101/277] refactor: extract methods --- system/Commands/Utilities/FilterCheck.php | 51 ++++++++++++++++++----- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/system/Commands/Utilities/FilterCheck.php b/system/Commands/Utilities/FilterCheck.php index c489f20d9e4f..2a58fa431529 100644 --- a/system/Commands/Utilities/FilterCheck.php +++ b/system/Commands/Utilities/FilterCheck.php @@ -73,13 +73,7 @@ class FilterCheck extends BaseCommand */ public function run(array $params) { - $tbody = []; - if (! isset($params[0], $params[1])) { - CLI::error('You must specify a HTTP verb and a route.'); - CLI::write(' Usage: ' . $this->usage); - CLI::write('Example: filter:check GET /'); - CLI::write(' filter:check PUT products/1'); - + if (! $this->checkParams($params)) { return EXIT_ERROR; } @@ -107,6 +101,40 @@ public function run(array $params) return EXIT_ERROR; } + $this->showTable($filterCollector, $filters, $method, $route); + $this->showFilterClasses($filterCollector, $method, $route); + + return EXIT_SUCCESS; + } + + /** + * @param array $params + */ + private function checkParams(array $params): bool + { + if (! isset($params[0], $params[1])) { + CLI::error('You must specify a HTTP verb and a route.'); + CLI::write(' Usage: ' . $this->usage); + CLI::write('Example: filter:check GET /'); + CLI::write(' filter:check PUT products/1'); + + return false; + } + + return true; + } + + /** + * @param array{before: list, after: list} $filters + */ + private function showTable( + FilterCollector $filterCollector, + array $filters, + string $method, + string $route + ): void { + $tbody = []; + $filters = $this->addRequiredFilters($filterCollector, $filters); $tbody[] = [ @@ -124,8 +152,13 @@ public function run(array $params) ]; CLI::table($tbody, $thead); + } - // Show filter classes + private function showFilterClasses( + FilterCollector $filterCollector, + string $method, + string $route + ): void { $requiredFilterClasses = $filterCollector->getRequiredFilterClasses(); $filterClasses = $filterCollector->getClasses($method, $route); @@ -146,8 +179,6 @@ public function run(array $params) CLI::write(ucfirst($position) . ' Filter Classes:', 'cyan'); CLI::write(implode(' → ', $classes)); } - - return EXIT_SUCCESS; } private function addRequiredFilters(FilterCollector $filterCollector, array $filters): array From a0219d7e9c6f26fc386de8323c91b49893906d70 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 22 Jun 2024 11:49:51 +0900 Subject: [PATCH 102/277] refactor: for readability --- phpstan-baseline.php | 12 --- system/Commands/Utilities/FilterCheck.php | 77 ++++++++----------- .../Utilities/Routes/FilterFinder.php | 3 + 3 files changed, 37 insertions(+), 55 deletions(-) diff --git a/phpstan-baseline.php b/phpstan-baseline.php index 64829de6cc3c..d2066f33094d 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -877,18 +877,6 @@ 'count' => 3, 'path' => __DIR__ . '/system/Commands/Utilities/Environment.php', ]; -$ignoreErrors[] = [ - // identifier: missingType.iterableValue - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Utilities\\\\FilterCheck\\:\\:addRequiredFilters\\(\\) has parameter \\$filters with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Utilities/FilterCheck.php', -]; -$ignoreErrors[] = [ - // identifier: missingType.iterableValue - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Utilities\\\\FilterCheck\\:\\:addRequiredFilters\\(\\) return type has no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Utilities/FilterCheck.php', -]; $ignoreErrors[] = [ // identifier: missingType.iterableValue 'message' => '#^Method CodeIgniter\\\\Commands\\\\Utilities\\\\Namespaces\\:\\:outputAllNamespaces\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', diff --git a/system/Commands/Utilities/FilterCheck.php b/system/Commands/Utilities/FilterCheck.php index 2a58fa431529..bb3811fc0ce8 100644 --- a/system/Commands/Utilities/FilterCheck.php +++ b/system/Commands/Utilities/FilterCheck.php @@ -133,17 +133,6 @@ private function showTable( string $method, string $route ): void { - $tbody = []; - - $filters = $this->addRequiredFilters($filterCollector, $filters); - - $tbody[] = [ - strtoupper($method), - $route, - implode(' ', $filters['before']), - implode(' ', $filters['after']), - ]; - $thead = [ 'Method', 'Route', @@ -151,9 +140,42 @@ private function showTable( 'After Filters', ]; + $required = $filterCollector->getRequiredFilters(); + + $coloredRequired = $this->colorItems($required); + + $before = array_merge($coloredRequired['before'], $filters['before']); + $after = array_merge($filters['after'], $coloredRequired['after']); + + $tbody = []; + $tbody[] = [ + strtoupper($method), + $route, + implode(' ', $before), + implode(' ', $after), + ]; + CLI::table($tbody, $thead); } + /** + * Color all elements of the array. + * + * @param array $array + * + * @return array + */ + private function colorItems(array $array): array + { + return array_map(function ($item) { + if (is_array($item)) { + return $this->colorItems($item); + } + + return CLI::color($item, 'yellow'); + }, $array); + } + private function showFilterClasses( FilterCollector $filterCollector, string $method, @@ -162,13 +184,7 @@ private function showFilterClasses( $requiredFilterClasses = $filterCollector->getRequiredFilterClasses(); $filterClasses = $filterCollector->getClasses($method, $route); - foreach ($requiredFilterClasses as $position => $classes) { - foreach ($classes as $class) { - $class = CLI::color($class, 'yellow'); - - $coloredRequiredFilterClasses[$position][] = $class; - } - } + $coloredRequiredFilterClasses = $this->colorItems($requiredFilterClasses); $classList = [ 'before' => array_merge($coloredRequiredFilterClasses['before'], $filterClasses['before']), @@ -180,29 +196,4 @@ private function showFilterClasses( CLI::write(implode(' → ', $classes)); } } - - private function addRequiredFilters(FilterCollector $filterCollector, array $filters): array - { - $output = []; - - $required = $filterCollector->getRequiredFilters(); - - $colored = []; - - foreach ($required['before'] as $filter) { - $filter = CLI::color($filter, 'yellow'); - $colored[] = $filter; - } - $output['before'] = array_merge($colored, $filters['before']); - - $colored = []; - - foreach ($required['after'] as $filter) { - $filter = CLI::color($filter, 'yellow'); - $colored[] = $filter; - } - $output['after'] = array_merge($filters['after'], $colored); - - return $output; - } } diff --git a/system/Commands/Utilities/Routes/FilterFinder.php b/system/Commands/Utilities/Routes/FilterFinder.php index f81995240291..f34ea26aa702 100644 --- a/system/Commands/Utilities/Routes/FilterFinder.php +++ b/system/Commands/Utilities/Routes/FilterFinder.php @@ -160,6 +160,9 @@ public function getRequiredFilterClasses(): array $before = $this->filters->getRequiredClasses('before'); $after = $this->filters->getRequiredClasses('after'); + $requiredBefore = []; + $requiredAfter = []; + foreach ($before as $classInfo) { $requiredBefore[] = $classInfo[0]; } From 5506bf72a394693f289961d33a9f0045ca87b7ce Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 24 Jun 2024 10:07:54 +0900 Subject: [PATCH 103/277] docs: add docs --- user_guide_src/source/changelogs/v4.6.0.rst | 1 + user_guide_src/source/incoming/filters.rst | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index 4de1f8640248..cf2b19052d7c 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -117,6 +117,7 @@ Commands - The ``spark routes`` and ``spark filter:check`` commands now display filter arguments. +- The ``spark filter:check`` command now displays filter classnames. Testing ======= diff --git a/user_guide_src/source/incoming/filters.rst b/user_guide_src/source/incoming/filters.rst index 0697e706e09d..8e420bdfef7a 100644 --- a/user_guide_src/source/incoming/filters.rst +++ b/user_guide_src/source/incoming/filters.rst @@ -274,11 +274,19 @@ The output is like the following: .. code-block:: none - +--------+-------+----------------+---------------+ - | Method | Route | Before Filters | After Filters | - +--------+-------+----------------+---------------+ - | GET | / | | toolbar | - +--------+-------+----------------+---------------+ + +--------+-------+----------------------+-------------------------------+ + | Method | Route | Before Filters | After Filters | + +--------+-------+----------------------+-------------------------------+ + | GET | / | forcehttps pagecache | pagecache performance toolbar | + +--------+-------+----------------------+-------------------------------+ + + Before Filter Classes: + CodeIgniter\Filters\ForceHTTPS → CodeIgniter\Filters\PageCache + After Filter Classes: + CodeIgniter\Filters\PageCache → CodeIgniter\Filters\PerformanceMetrics → CodeIgniter\Filters\DebugToolbar + +.. note:: Since v4.6.0, filter arguments have been displayed in the output table. + Also, the actual filter classnames have been displayed in the end. You can also see the routes and filters by the ``spark routes`` command, but it might not show accurate filters when you use regular expressions for routes. From 9c8f647e9e9571dc65a145fb3d9af300ac1ea8f2 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 24 Jun 2024 10:11:21 +0900 Subject: [PATCH 104/277] test: add test --- tests/system/Commands/FilterCheckTest.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/system/Commands/FilterCheckTest.php b/tests/system/Commands/FilterCheckTest.php index 0c26dd9bf349..3fffd9e3f72e 100644 --- a/tests/system/Commands/FilterCheckTest.php +++ b/tests/system/Commands/FilterCheckTest.php @@ -50,6 +50,13 @@ public function testFilterCheckDefinedRoute(): void '| GET | / | forcehttps pagecache | pagecache performance toolbar |', preg_replace('/\033\[.+?m/u', '', $this->getBuffer()) ); + $this->assertStringContainsString( + 'Before Filter Classes: +CodeIgniter\Filters\ForceHTTPS → CodeIgniter\Filters\PageCache +After Filter Classes: +CodeIgniter\Filters\PageCache → CodeIgniter\Filters\PerformanceMetrics → CodeIgniter\Filters\DebugToolbar', + preg_replace('/\033\[.+?m/u', '', $this->getBuffer()) + ); } public function testFilterCheckInvalidRoute(): void From ecfc323557d21a064729897ed254d2bf8a99e8c7 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 21 Jun 2024 13:30:08 +0900 Subject: [PATCH 105/277] test: define param types --- tests/system/Router/RouteCollectionTest.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/system/Router/RouteCollectionTest.php b/tests/system/Router/RouteCollectionTest.php index 89959cd5f33e..ef67c02a29fc 100644 --- a/tests/system/Router/RouteCollectionTest.php +++ b/tests/system/Router/RouteCollectionTest.php @@ -1768,11 +1768,8 @@ public static function provideRouteDefaultNamespace(): iterable ]; } - /** - * @param mixed $namespace - */ #[DataProvider('provideRouteDefaultNamespace')] - public function testAutoRoutesControllerNameReturnsFQCN($namespace): void + public function testAutoRoutesControllerNameReturnsFQCN(string $namespace): void { $routes = $this->getCollector(); $routes->setAutoRoute(true); @@ -1788,11 +1785,8 @@ public function testAutoRoutesControllerNameReturnsFQCN($namespace): void $this->assertSame('\\' . Product::class, $router->controllerName()); } - /** - * @param mixed $namespace - */ #[DataProvider('provideRouteDefaultNamespace')] - public function testRoutesControllerNameReturnsFQCN($namespace): void + public function testRoutesControllerNameReturnsFQCN(string $namespace): void { Services::request()->setMethod(Method::GET); $routes = $this->getCollector(); From ab49378eb7feed6ca2c6657acffb5703302c8864 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 21 Jun 2024 13:34:48 +0900 Subject: [PATCH 106/277] config: change default config for Auto Routing Improved Feature::$autoRoutesImproved -> true Routing::$translateUriToCamelCase -> true --- app/Config/Feature.php | 4 ++-- app/Config/Routing.php | 2 +- tests/system/Commands/RoutesTest.php | 3 +++ .../ControllerMethodReaderTest.php | 5 +++-- tests/system/Router/AutoRouterImprovedTest.php | 18 ++++++++++++++++++ tests/system/Router/RouteCollectionTest.php | 4 ++++ tests/system/Router/RouterTest.php | 8 ++++++++ 7 files changed, 39 insertions(+), 5 deletions(-) diff --git a/app/Config/Feature.php b/app/Config/Feature.php index efd4a0b20ae8..35024d357919 100644 --- a/app/Config/Feature.php +++ b/app/Config/Feature.php @@ -10,9 +10,9 @@ class Feature extends BaseConfig { /** - * Use improved new auto routing instead of the default legacy version. + * Use improved new auto routing instead of the legacy version. */ - public bool $autoRoutesImproved = false; + public bool $autoRoutesImproved = true; /** * Use filter execution order in 4.4 or before. diff --git a/app/Config/Routing.php b/app/Config/Routing.php index 7abadc7b76f0..3005543a9e79 100644 --- a/app/Config/Routing.php +++ b/app/Config/Routing.php @@ -136,5 +136,5 @@ class Routing extends BaseRouting * * Default: false */ - public bool $translateUriToCamelCase = false; + public bool $translateUriToCamelCase = true; } diff --git a/tests/system/Commands/RoutesTest.php b/tests/system/Commands/RoutesTest.php index 2ee9f44f1bac..60b326dee35e 100644 --- a/tests/system/Commands/RoutesTest.php +++ b/tests/system/Commands/RoutesTest.php @@ -16,6 +16,7 @@ use CodeIgniter\Router\RouteCollection; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; +use Config\Feature; use Config\Services; use PHPUnit\Framework\Attributes\Group; @@ -213,6 +214,8 @@ public function testRoutesCommandRouteLegacy(): void $routes = $this->getCleanRoutes(); $routes->loadRoutes(); + $featureConfig = config(Feature::class); + $featureConfig->autoRoutesImproved = false; $routes->setAutoRoute(true); command('routes'); diff --git a/tests/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReaderTest.php b/tests/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReaderTest.php index 936b2b8fe184..2eaabe3cd833 100644 --- a/tests/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReaderTest.php +++ b/tests/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReaderTest.php @@ -74,8 +74,9 @@ public function testRead(): void public function testReadTranslateURIDashes(): void { - $config = config(Routing::class); - $config->translateURIDashes = true; + $config = config(Routing::class); + $config->translateURIDashes = true; + $config->translateUriToCamelCase = false; Factories::injectMock('config', Routing::class, $config); $reader = $this->createControllerMethodReader( diff --git a/tests/system/Router/AutoRouterImprovedTest.php b/tests/system/Router/AutoRouterImprovedTest.php index 5975280bcdfd..7e6f639f7ddc 100644 --- a/tests/system/Router/AutoRouterImprovedTest.php +++ b/tests/system/Router/AutoRouterImprovedTest.php @@ -204,8 +204,16 @@ public function testAutoRouteFindsControllerWithSubSubfolder(): void $this->assertSame([], $params); } + private function disableTranslateUriToCamelCase(): void + { + $routingConfig = config(Routing::class); + $routingConfig->translateUriToCamelCase = false; + } + public function testAutoRouteFindsDashedSubfolder(): void { + $this->disableTranslateUriToCamelCase(); + $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] @@ -222,6 +230,8 @@ public function testAutoRouteFindsDashedSubfolder(): void public function testAutoRouteFindsDashedController(): void { + $this->disableTranslateUriToCamelCase(); + $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] @@ -235,6 +245,8 @@ public function testAutoRouteFindsDashedController(): void public function testAutoRouteFindsDashedMethod(): void { + $this->disableTranslateUriToCamelCase(); + $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] @@ -248,6 +260,8 @@ public function testAutoRouteFindsDashedMethod(): void public function testAutoRouteFindsDefaultDashFolder(): void { + $this->disableTranslateUriToCamelCase(); + $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] @@ -438,6 +452,8 @@ public function testRejectsURIWithUnderscoreController(): void 'AutoRouterImproved prohibits access to the URI containing underscores ("dash_controller")' ); + $this->disableTranslateUriToCamelCase(); + $router = $this->createNewAutoRouter(); $router->getRoute('dash-folder/dash_controller/dash-method', Method::GET); @@ -450,6 +466,8 @@ public function testRejectsURIWithUnderscoreMethod(): void 'AutoRouterImproved prohibits access to the URI containing underscores ("dash_method")' ); + $this->disableTranslateUriToCamelCase(); + $router = $this->createNewAutoRouter(); $router->getRoute('dash-folder/dash-controller/dash_method', Method::GET); diff --git a/tests/system/Router/RouteCollectionTest.php b/tests/system/Router/RouteCollectionTest.php index ef67c02a29fc..6f623196db69 100644 --- a/tests/system/Router/RouteCollectionTest.php +++ b/tests/system/Router/RouteCollectionTest.php @@ -19,6 +19,7 @@ use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\HTTP\Method; use CodeIgniter\Test\CIUnitTestCase; +use Config\Feature; use Config\Modules; use Config\Routing; use PHPUnit\Framework\Attributes\DataProvider; @@ -1771,6 +1772,9 @@ public static function provideRouteDefaultNamespace(): iterable #[DataProvider('provideRouteDefaultNamespace')] public function testAutoRoutesControllerNameReturnsFQCN(string $namespace): void { + $featureConfig = config(Feature::class); + $featureConfig->autoRoutesImproved = false; + $routes = $this->getCollector(); $routes->setAutoRoute(true); $routes->setDefaultNamespace($namespace); diff --git a/tests/system/Router/RouterTest.php b/tests/system/Router/RouterTest.php index bb5302e721dc..e21763b9dc8a 100644 --- a/tests/system/Router/RouterTest.php +++ b/tests/system/Router/RouterTest.php @@ -23,6 +23,7 @@ use CodeIgniter\Router\Exceptions\RouterException; use CodeIgniter\Test\CIUnitTestCase; use Config\App; +use Config\Feature; use Config\Modules; use Config\Routing; use PHPUnit\Framework\Attributes\DataProvider; @@ -42,12 +43,19 @@ protected function setUp(): void { parent::setUp(); + $this->disableAutoRoutesImproved(); $this->createRouteCollection(); $this->request = Services::request(); $this->request->setMethod(Method::GET); } + private function disableAutoRoutesImproved(): void + { + $featureConfig = config(Feature::class); + $featureConfig->autoRoutesImproved = false; + } + private function createRouteCollection(?Routing $routingConfig = null): void { $moduleConfig = new Modules(); From 5d139c32eb465fa9d8d94bed563b2d26fb92c521 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 21 Jun 2024 13:52:20 +0900 Subject: [PATCH 107/277] docs: create new page auto_routing_improved.rst --- .../source/incoming/auto_routing_improved.rst | 140 ++++++++++++++++++ user_guide_src/source/incoming/index.rst | 1 + user_guide_src/source/incoming/routing.rst | 130 +--------------- 3 files changed, 142 insertions(+), 129 deletions(-) create mode 100644 user_guide_src/source/incoming/auto_routing_improved.rst diff --git a/user_guide_src/source/incoming/auto_routing_improved.rst b/user_guide_src/source/incoming/auto_routing_improved.rst new file mode 100644 index 000000000000..db168a0d17b7 --- /dev/null +++ b/user_guide_src/source/incoming/auto_routing_improved.rst @@ -0,0 +1,140 @@ +####################### +Auto Routing (Improved) +####################### + +.. versionadded:: 4.2.0 + +.. contents:: + :local: + :depth: 3 + +.. _auto-routing-improved: + +Since v4.2.0, the new more secure Auto Routing has been introduced. + +.. note:: If you are familiar with Auto Routing, which was enabled by default + from CodeIgniter 3.x through 4.1.x, you can see the differences in + :ref:`ChangeLog v4.2.0 `. + +When no defined route is found that matches the URI, the system will attempt to match that URI against the controllers and methods when Auto Routing is enabled. + +.. important:: For security reasons, if a controller is used in the defined routes, Auto Routing (Improved) does not route to the controller. + +Auto Routing can automatically route HTTP requests based on conventions +and execute the corresponding controller methods. + +.. note:: Auto Routing (Improved) is disabled by default. To use it, see below. + +.. _enabled-auto-routing-improved: + +Enable Auto Routing +=================== + +To use it, you need to change the setting ``$autoRoute`` option to ``true`` in **app/Config/Routing.php**:: + + public bool $autoRoute = true; + +And you need to change the property ``$autoRoutesImproved`` to ``true`` in **app/Config/Feature.php**:: + + public bool $autoRoutesImproved = true; + +URI Segments +============ + +The segments in the URL, in following with the Model-View-Controller approach, usually represent:: + + example.com/class/method/ID + +1. The first segment represents the controller **class** that should be invoked. +2. The second segment represents the class **method** that should be called. +3. The third, and any additional segments, represent the ID and any variables that will be passed to the controller. + +Consider this URI:: + + example.com/index.php/helloworld/hello/1 + +In the above example, when you send an HTTP request with **GET** method, +Auto Routing would attempt to find a controller named ``App\Controllers\Helloworld`` +and executes ``getHello()`` method with passing ``'1'`` as the first argument. + +.. note:: A controller method that will be executed by Auto Routing (Improved) needs HTTP verb (``get``, ``post``, ``put``, etc.) prefix like ``getIndex()``, ``postCreate()``. + +See :ref:`Auto Routing in Controllers ` for more info. + +.. _routing-auto-routing-improved-configuration-options: + +Configuration Options +===================== + +These options are available in the **app/Config/Routing.php** file. + +Default Controller +------------------ + +For Site Root URI +^^^^^^^^^^^^^^^^^ + +When a user visits the root of your site (i.e., **example.com**) the controller +to use is determined by the value set to the ``$defaultController`` property, +unless a route exists for it explicitly. + +The default value for this is ``Home`` which matches the controller at +**app/Controllers/Home.php**:: + + public string $defaultController = 'Home'; + +For Directory URI +^^^^^^^^^^^^^^^^^ + +The default controller is also used when no matching route has been found, and the URI would point to a directory +in the controllers directory. For example, if the user visits **example.com/admin**, if a controller was found at +**app/Controllers/Admin/Home.php**, it would be used. + +.. important:: You cannot access the default controller with the URI of the controller name. + When the default controller is ``Home``, you can access **example.com/**, but if you access **example.com/home**, it will be not found. + +See :ref:`Auto Routing in Controllers ` for more info. + +.. _routing-auto-routing-improved-default-method: + +Default Method +-------------- + +This works similar to the default controller setting, but is used to determine the default method that is used +when a controller is found that matches the URI, but no segment exists for the method. The default value is +``index``. + +In this example, if the user were to visit **example.com/products**, and a ``Products`` +controller existed, the ``Products::getListAll()`` method would be executed:: + + public string $defaultMethod = 'listAll'; + +.. important:: You cannot access the controller with the URI of the default method name. + In the example above, you can access **example.com/products**, but if you access **example.com/products/listall**, it will be not found. + +.. _auto-routing-improved-module-routing: + + +Module Routing +============== + +.. versionadded:: 4.4.0 + +You can use auto routing even if you use :doc:`../general/modules` and place +the controllers in a different namespace. + +To route to a module, the ``$moduleRoutes`` property in **app/Config/Routing.php** +must be set:: + + public array $moduleRoutes = [ + 'blog' => 'Acme\Blog\Controllers', + ]; + +The key is the first URI segment for the module, and the value is the controller +namespace. In the above configuration, **http://localhost:8080/blog/foo/bar** +will be routed to ``Acme\Blog\Controllers\Foo::getBar()``. + +.. note:: If you define ``$moduleRoutes``, the routing for the module takes + precedence. In the above example, even if you have the ``App\Controllers\Blog`` + controller, **http://localhost:8080/blog** will be routed to the default + controller ``Acme\Blog\Controllers\Home``. diff --git a/user_guide_src/source/incoming/index.rst b/user_guide_src/source/incoming/index.rst index b4399e18d1ba..d8faf0a869b5 100644 --- a/user_guide_src/source/incoming/index.rst +++ b/user_guide_src/source/incoming/index.rst @@ -10,6 +10,7 @@ Controllers handle incoming requests. routing controllers filters + auto_routing_improved message request incomingrequest diff --git a/user_guide_src/source/incoming/routing.rst b/user_guide_src/source/incoming/routing.rst index 3ab7a5848bc9..da55dfa7fd20 100644 --- a/user_guide_src/source/incoming/routing.rst +++ b/user_guide_src/source/incoming/routing.rst @@ -767,140 +767,12 @@ will match **product/123**, **product/123/456**, **product/123/456/789** and so And if the URI is **product/123/456**, ``123/456`` will be passed to the first parameter of the ``Catalog::productLookup()`` method. -.. _auto-routing-improved: - Auto Routing (Improved) *********************** .. versionadded:: 4.2.0 -Since v4.2.0, the new more secure Auto Routing has been introduced. - -.. note:: If you are familiar with Auto Routing, which was enabled by default - from CodeIgniter 3.x through 4.1.x, you can see the differences in - :ref:`ChangeLog v4.2.0 `. - -When no defined route is found that matches the URI, the system will attempt to match that URI against the controllers and methods when Auto Routing is enabled. - -.. important:: For security reasons, if a controller is used in the defined routes, Auto Routing (Improved) does not route to the controller. - -Auto Routing can automatically route HTTP requests based on conventions -and execute the corresponding controller methods. - -.. note:: Auto Routing (Improved) is disabled by default. To use it, see below. - -.. _enabled-auto-routing-improved: - -Enable Auto Routing -=================== - -To use it, you need to change the setting ``$autoRoute`` option to ``true`` in **app/Config/Routing.php**:: - - public bool $autoRoute = true; - -And you need to change the property ``$autoRoutesImproved`` to ``true`` in **app/Config/Feature.php**:: - - public bool $autoRoutesImproved = true; - -URI Segments -============ - -The segments in the URL, in following with the Model-View-Controller approach, usually represent:: - - example.com/class/method/ID - -1. The first segment represents the controller **class** that should be invoked. -2. The second segment represents the class **method** that should be called. -3. The third, and any additional segments, represent the ID and any variables that will be passed to the controller. - -Consider this URI:: - - example.com/index.php/helloworld/hello/1 - -In the above example, when you send an HTTP request with **GET** method, -Auto Routing would attempt to find a controller named ``App\Controllers\Helloworld`` -and executes ``getHello()`` method with passing ``'1'`` as the first argument. - -.. note:: A controller method that will be executed by Auto Routing (Improved) needs HTTP verb (``get``, ``post``, ``put``, etc.) prefix like ``getIndex()``, ``postCreate()``. - -See :ref:`Auto Routing in Controllers ` for more info. - -.. _routing-auto-routing-improved-configuration-options: - -Configuration Options -===================== - -These options are available in the **app/Config/Routing.php** file. - -Default Controller ------------------- - -For Site Root URI -^^^^^^^^^^^^^^^^^ - -When a user visits the root of your site (i.e., **example.com**) the controller -to use is determined by the value set to the ``$defaultController`` property, -unless a route exists for it explicitly. - -The default value for this is ``Home`` which matches the controller at -**app/Controllers/Home.php**:: - - public string $defaultController = 'Home'; - -For Directory URI -^^^^^^^^^^^^^^^^^ - -The default controller is also used when no matching route has been found, and the URI would point to a directory -in the controllers directory. For example, if the user visits **example.com/admin**, if a controller was found at -**app/Controllers/Admin/Home.php**, it would be used. - -.. important:: You cannot access the default controller with the URI of the controller name. - When the default controller is ``Home``, you can access **example.com/**, but if you access **example.com/home**, it will be not found. - -See :ref:`Auto Routing in Controllers ` for more info. - -.. _routing-auto-routing-improved-default-method: - -Default Method --------------- - -This works similar to the default controller setting, but is used to determine the default method that is used -when a controller is found that matches the URI, but no segment exists for the method. The default value is -``index``. - -In this example, if the user were to visit **example.com/products**, and a ``Products`` -controller existed, the ``Products::getListAll()`` method would be executed:: - - public string $defaultMethod = 'listAll'; - -.. important:: You cannot access the controller with the URI of the default method name. - In the example above, you can access **example.com/products**, but if you access **example.com/products/listall**, it will be not found. - -.. _auto-routing-improved-module-routing: - -Module Routing -============== - -.. versionadded:: 4.4.0 - -You can use auto routing even if you use :doc:`../general/modules` and place -the controllers in a different namespace. - -To route to a module, the ``$moduleRoutes`` property in **app/Config/Routing.php** -must be set:: - - public array $moduleRoutes = [ - 'blog' => 'Acme\Blog\Controllers', - ]; - -The key is the first URI segment for the module, and the value is the controller -namespace. In the above configuration, **http://localhost:8080/blog/foo/bar** -will be routed to ``Acme\Blog\Controllers\Foo::getBar()``. - -.. note:: If you define ``$moduleRoutes``, the routing for the module takes - precedence. In the above example, even if you have the ``App\Controllers\Blog`` - controller, **http://localhost:8080/blog** will be routed to the default - controller ``Acme\Blog\Controllers\Home``. +See :doc:`auto_routing_improved`. .. _auto-routing-legacy: From e51627a4f5f491d808296b32eaf94def77f3ad26 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 21 Jun 2024 14:11:15 +0900 Subject: [PATCH 108/277] docs: move contents in controllers.rst to auto_routing_improved.rst --- .../source/incoming/auto_routing_improved.rst | 269 ++++++++++++++++- .../source/incoming/controllers.rst | 278 +----------------- 2 files changed, 269 insertions(+), 278 deletions(-) diff --git a/user_guide_src/source/incoming/auto_routing_improved.rst b/user_guide_src/source/incoming/auto_routing_improved.rst index db168a0d17b7..c95aff1b308d 100644 --- a/user_guide_src/source/incoming/auto_routing_improved.rst +++ b/user_guide_src/source/incoming/auto_routing_improved.rst @@ -59,7 +59,274 @@ and executes ``getHello()`` method with passing ``'1'`` as the first argument. .. note:: A controller method that will be executed by Auto Routing (Improved) needs HTTP verb (``get``, ``post``, ``put``, etc.) prefix like ``getIndex()``, ``postCreate()``. -See :ref:`Auto Routing in Controllers ` for more info. +This section describes the functionality of the new auto-routing. +It automatically routes an HTTP request, and executes the corresponding controller method +without route definitions. + +Consider this URI:: + + example.com/index.php/helloworld/ + +In the above example, CodeIgniter would attempt to find a controller named ``App\Controllers\Helloworld`` and load it, when auto-routing is enabled. + +.. note:: When a controller's short name matches the first segment of a URI, it will be loaded. + +Let's try it: Hello World! +========================== + +Let's create a simple controller so you can see it in action. Using your text editor, create a file called **Helloworld.php**, +and put the following code in it. You will notice that the ``Helloworld`` Controller is extending the ``BaseController``. you can +also extend the ``CodeIgniter\Controller`` if you do not need the functionality of the BaseController. + +The BaseController provides a convenient place for loading components and performing functions that are needed by all your +controllers. You can extend this class in any new controller. + +.. literalinclude:: controllers/020.php + +Then save the file to your **app/Controllers** directory. + +.. important:: The file must be called **Helloworld.php**, with a capital ``H``. When you use Auto Routing, Controller class names MUST start with an uppercase letter and ONLY the first character can be uppercase. + + Since v4.5.0, if you enable the ``$translateUriToCamelCase`` option, you can + use CamelCase classnames. See :ref:`controller-translate-uri-to-camelcase` + for details. + +.. important:: A controller method that will be executed by Auto Routing (Improved) needs HTTP verb (``get``, ``post``, ``put``, etc.) prefix like ``getIndex()``, ``postCreate()``. + +Now visit your site using a URL similar to this:: + + example.com/index.php/helloworld + +If you did it right you should see:: + + Hello World! + +This is valid: + +.. literalinclude:: controllers/009.php + +This is **not** valid: + +.. literalinclude:: controllers/010.php + +This is **not** valid: + +.. literalinclude:: controllers/011.php + +.. note:: Since v4.5.0, if you enable the ``$translateUriToCamelCase`` option, + you can use CamelCase classnames like above. See + :ref:`controller-translate-uri-to-camelcase` for details. + +Also, always make sure your controller extends the parent controller +class so that it can inherit all its methods. + +.. note:: + The system will attempt to match the URI against Controllers by matching each segment against + directories/files in **app/Controllers**, when a match wasn't found against defined routes. + That's why your directories/files MUST start with a capital letter and the rest MUST be lowercase. + + If you want another naming convention you need to manually define it using the + :ref:`Defined Route Routing `. + Here is an example based on PSR-4 Autoloader: + + .. literalinclude:: controllers/012.php + +Methods +======= + +Method Visibility +----------------- + +When you define a method that is executable via HTTP request, the method must be +declared as ``public``. + +.. warning:: For security reasons be sure to declare any new utility methods as ``protected`` or ``private``. + +Default Method +-------------- + +In the above example, the method name is ``getIndex()``. +The method (HTTP verb + ``Index()``) is called the **default method**, and is loaded if the **second segment** of the URI is empty. + +Normal Methods +-------------- + +The second segment of the URI determines which method in the +controller gets called. + +Let's try it. Add a new method to your controller: + +.. literalinclude:: controllers/021.php + +Now load the following URL to see the ``getComment()`` method:: + + example.com/index.php/helloworld/comment/ + +You should see your new message. + +Passing URI Segments to Your Methods +==================================== + +If your URI contains more than two segments they will be passed to your +method as parameters. + +For example, let's say you have a URI like this:: + + example.com/index.php/products/shoes/sandals/123 + +Your method will be passed URI segments 3 and 4 (``'sandals'`` and ``'123'``): + +.. literalinclude:: controllers/022.php + +Default Controller +================== + +The Default Controller is a special controller that is used when a URI ends with +a directory name or when a URI is not present, as will be the case when only your +site root URL is requested. + +Defining a Default Controller +----------------------------- + +Let's try it with the ``Helloworld`` controller. + +To specify a default controller open your **app/Config/Routing.php** +file and set this property:: + + public string $defaultController = 'Helloworld'; + +Where ``Helloworld`` is the name of the controller class you want to be used. + +And comment out the line in **app/Config/Routes.php**: + +.. literalinclude:: controllers/016.php + :lines: 2- + +If you now browse to your site without specifying any URI segments you'll +see the "Hello World" message. + +.. important:: When you use Auto Routing (Improved), you must remove the line + ``$routes->get('/', 'Home::index');``. Because defined routes take + precedence over Auto Routing, and controllers defined in the defined routes + are denied access by Auto Routing (Improved) for security reasons. + +For more information, please refer to the +:ref:`routing-auto-routing-improved-configuration-options` documentation. + +.. _controller-default-method-fallback: + +Default Method Fallback +======================= + +.. versionadded:: 4.4.0 + +If the controller method corresponding to the URI segment of the method name +does not exist, and if the default method is defined, the remaining URI segments +are passed to the default method for execution. + +.. literalinclude:: controllers/024.php + +Load the following URL:: + + example.com/index.php/product/15/edit + +The method will be passed URI segments 2 and 3 (``'15'`` and ``'edit'``): + +.. important:: If there are more parameters in the URI than the method parameters, + Auto Routing (Improved) does not execute the method, and it results in 404 + Not Found. + +Fallback to Default Controller +------------------------------ + +If the controller corresponding to the URI segment of the controller name +does not exist, and if the default controller (``Home`` by default) exists in +the directory, the remaining URI segments are passed to the default controller's +default method. + +For example, when you have the following default controller ``Home`` in the +**app/Controllers/News** directory: + +.. literalinclude:: controllers/025.php + +Load the following URL:: + + example.com/index.php/news/101 + +The ``News\Home`` controller and the default ``getIndex()`` method will be found. +So the default method will be passed URI segments 2 (``'101'``): + +.. note:: If there is ``App\Controllers\News`` controller, it takes precedence. + The URI segments are searched sequentially and the first controller found + is used. + +.. note:: If there are more parameters in the URI than the method parameters, + Auto Routing (Improved) does not execute the method, and it results in 404 + Not Found. + +Organizing Your Controllers into Sub-directories +================================================ + +If you are building a large application you might want to hierarchically +organize or structure your controllers into sub-directories. CodeIgniter +permits you to do this. + +Simply create sub-directories under the main **app/Controllers**, +and place your controller classes within them. + +.. important:: Directory names MUST start with an uppercase letter and ONLY the first character can be uppercase. + + Since v4.5.0, if you enable the ``$translateUriToCamelCase`` option, you can + use CamelCase directory names. See :ref:`controller-translate-uri-to-camelcase` + for details. + +When using this feature the first segment of your URI must +specify the directory. For example, let's say you have a controller located here:: + + app/Controllers/Products/Shoes.php + +To call the above controller your URI will look something like this:: + + example.com/index.php/products/shoes/show/123 + +.. note:: You cannot have directories with the same name in **app/Controllers** + and **public**. + This is because if there is a directory, the web server will search for it and + it will not be routed to CodeIgniter. + +Each of your sub-directories may contain a default controller which will be +called if the URL contains *only* the sub-directory. Simply put a controller +in there that matches the name of your default controller as specified in +your **app/Config/Routing.php** file. + +CodeIgniter also permits you to map your URIs using its :ref:`Defined Route Routing `.. + +.. _controller-translate-uri-to-camelcase: + +Translate URI To CamelCase +========================== + +.. versionadded:: 4.5.0 + +Since v4.5.0, the ``$translateUriToCamelCase`` option has been implemented, +which works well with the current CodeIgniter's coding standards. + +This option enables you to automatically translate URI with dashes (``-``) to +CamelCase in the controller and method URI segments. + +For example, the URI ``sub-dir/hello-controller/some-method`` will execute the +``SubDir\HelloController::getSomeMethod()`` method. + +.. note:: When this option is enabled, the ``$translateURIDashes`` option is + ignored. + +Enable Translate URI To CamelCase +--------------------------------- + +To enable it, you need to change the setting ``$translateUriToCamelCase`` option +to ``true`` in **app/Config/Routing.php**:: + + public bool $translateUriToCamelCase = true; .. _routing-auto-routing-improved-configuration-options: diff --git a/user_guide_src/source/incoming/controllers.rst b/user_guide_src/source/incoming/controllers.rst index bd1f011df78b..de5798e8477e 100644 --- a/user_guide_src/source/incoming/controllers.rst +++ b/user_guide_src/source/incoming/controllers.rst @@ -182,283 +182,7 @@ Auto Routing (Improved) .. versionadded:: 4.2.0 -Since v4.2.0, the new more secure Auto Routing has been introduced. - -.. note:: If you are familiar with Auto Routing, which was enabled by default - from CodeIgniter 3.x through 4.1.x, you can see the differences in - :ref:`ChangeLog v4.2.0 `. - -This section describes the functionality of the new auto-routing. -It automatically routes an HTTP request, and executes the corresponding controller method -without route definitions. - -Since v4.2.0, the auto-routing is disabled by default. To use it, see :ref:`enabled-auto-routing-improved`. - -Consider this URI:: - - example.com/index.php/helloworld/ - -In the above example, CodeIgniter would attempt to find a controller named ``App\Controllers\Helloworld`` and load it, when auto-routing is enabled. - -.. note:: When a controller's short name matches the first segment of a URI, it will be loaded. - -Let's try it: Hello World! -========================== - -Let's create a simple controller so you can see it in action. Using your text editor, create a file called **Helloworld.php**, -and put the following code in it. You will notice that the ``Helloworld`` Controller is extending the ``BaseController``. you can -also extend the ``CodeIgniter\Controller`` if you do not need the functionality of the BaseController. - -The BaseController provides a convenient place for loading components and performing functions that are needed by all your -controllers. You can extend this class in any new controller. - -.. literalinclude:: controllers/020.php - -Then save the file to your **app/Controllers** directory. - -.. important:: The file must be called **Helloworld.php**, with a capital ``H``. When you use Auto Routing, Controller class names MUST start with an uppercase letter and ONLY the first character can be uppercase. - - Since v4.5.0, if you enable the ``$translateUriToCamelCase`` option, you can - use CamelCase classnames. See :ref:`controller-translate-uri-to-camelcase` - for details. - -.. important:: A controller method that will be executed by Auto Routing (Improved) needs HTTP verb (``get``, ``post``, ``put``, etc.) prefix like ``getIndex()``, ``postCreate()``. - -Now visit your site using a URL similar to this:: - - example.com/index.php/helloworld - -If you did it right you should see:: - - Hello World! - -This is valid: - -.. literalinclude:: controllers/009.php - -This is **not** valid: - -.. literalinclude:: controllers/010.php - -This is **not** valid: - -.. literalinclude:: controllers/011.php - -.. note:: Since v4.5.0, if you enable the ``$translateUriToCamelCase`` option, - you can use CamelCase classnames like above. See - :ref:`controller-translate-uri-to-camelcase` for details. - -Also, always make sure your controller extends the parent controller -class so that it can inherit all its methods. - -.. note:: - The system will attempt to match the URI against Controllers by matching each segment against - directories/files in **app/Controllers**, when a match wasn't found against defined routes. - That's why your directories/files MUST start with a capital letter and the rest MUST be lowercase. - - If you want another naming convention you need to manually define it using the - :ref:`Defined Route Routing `. - Here is an example based on PSR-4 Autoloader: - - .. literalinclude:: controllers/012.php - -Methods -======= - -Method Visibility ------------------ - -When you define a method that is executable via HTTP request, the method must be -declared as ``public``. - -.. warning:: For security reasons be sure to declare any new utility methods as ``protected`` or ``private``. - -Default Method --------------- - -In the above example, the method name is ``getIndex()``. -The method (HTTP verb + ``Index()``) is called the **default method**, and is loaded if the **second segment** of the URI is empty. - -Normal Methods --------------- - -The second segment of the URI determines which method in the -controller gets called. - -Let's try it. Add a new method to your controller: - -.. literalinclude:: controllers/021.php - -Now load the following URL to see the ``getComment()`` method:: - - example.com/index.php/helloworld/comment/ - -You should see your new message. - -Passing URI Segments to Your Methods -==================================== - -If your URI contains more than two segments they will be passed to your -method as parameters. - -For example, let's say you have a URI like this:: - - example.com/index.php/products/shoes/sandals/123 - -Your method will be passed URI segments 3 and 4 (``'sandals'`` and ``'123'``): - -.. literalinclude:: controllers/022.php - -Default Controller -================== - -The Default Controller is a special controller that is used when a URI ends with -a directory name or when a URI is not present, as will be the case when only your -site root URL is requested. - -Defining a Default Controller ------------------------------ - -Let's try it with the ``Helloworld`` controller. - -To specify a default controller open your **app/Config/Routing.php** -file and set this property:: - - public string $defaultController = 'Helloworld'; - -Where ``Helloworld`` is the name of the controller class you want to be used. - -And comment out the line in **app/Config/Routes.php**: - -.. literalinclude:: controllers/016.php - :lines: 2- - -If you now browse to your site without specifying any URI segments you'll -see the "Hello World" message. - -.. important:: When you use Auto Routing (Improved), you must remove the line - ``$routes->get('/', 'Home::index');``. Because defined routes take - precedence over Auto Routing, and controllers defined in the defined routes - are denied access by Auto Routing (Improved) for security reasons. - -For more information, please refer to the -:ref:`routing-auto-routing-improved-configuration-options` documentation. - -.. _controller-default-method-fallback: - -Default Method Fallback -======================= - -.. versionadded:: 4.4.0 - -If the controller method corresponding to the URI segment of the method name -does not exist, and if the default method is defined, the remaining URI segments -are passed to the default method for execution. - -.. literalinclude:: controllers/024.php - -Load the following URL:: - - example.com/index.php/product/15/edit - -The method will be passed URI segments 2 and 3 (``'15'`` and ``'edit'``): - -.. important:: If there are more parameters in the URI than the method parameters, - Auto Routing (Improved) does not execute the method, and it results in 404 - Not Found. - -Fallback to Default Controller ------------------------------- - -If the controller corresponding to the URI segment of the controller name -does not exist, and if the default controller (``Home`` by default) exists in -the directory, the remaining URI segments are passed to the default controller's -default method. - -For example, when you have the following default controller ``Home`` in the -**app/Controllers/News** directory: - -.. literalinclude:: controllers/025.php - -Load the following URL:: - - example.com/index.php/news/101 - -The ``News\Home`` controller and the default ``getIndex()`` method will be found. -So the default method will be passed URI segments 2 (``'101'``): - -.. note:: If there is ``App\Controllers\News`` controller, it takes precedence. - The URI segments are searched sequentially and the first controller found - is used. - -.. note:: If there are more parameters in the URI than the method parameters, - Auto Routing (Improved) does not execute the method, and it results in 404 - Not Found. - -Organizing Your Controllers into Sub-directories -================================================ - -If you are building a large application you might want to hierarchically -organize or structure your controllers into sub-directories. CodeIgniter -permits you to do this. - -Simply create sub-directories under the main **app/Controllers**, -and place your controller classes within them. - -.. important:: Directory names MUST start with an uppercase letter and ONLY the first character can be uppercase. - - Since v4.5.0, if you enable the ``$translateUriToCamelCase`` option, you can - use CamelCase directory names. See :ref:`controller-translate-uri-to-camelcase` - for details. - -When using this feature the first segment of your URI must -specify the directory. For example, let's say you have a controller located here:: - - app/Controllers/Products/Shoes.php - -To call the above controller your URI will look something like this:: - - example.com/index.php/products/shoes/show/123 - -.. note:: You cannot have directories with the same name in **app/Controllers** - and **public**. - This is because if there is a directory, the web server will search for it and - it will not be routed to CodeIgniter. - -Each of your sub-directories may contain a default controller which will be -called if the URL contains *only* the sub-directory. Simply put a controller -in there that matches the name of your default controller as specified in -your **app/Config/Routing.php** file. - -CodeIgniter also permits you to map your URIs using its :ref:`Defined Route Routing `.. - -.. _controller-translate-uri-to-camelcase: - -Translate URI To CamelCase -========================== - -.. versionadded:: 4.5.0 - -Since v4.5.0, the ``$translateUriToCamelCase`` option has been implemented, -which works well with the current CodeIgniter's coding standards. - -This option enables you to automatically translate URI with dashes (``-``) to -CamelCase in the controller and method URI segments. - -For example, the URI ``sub-dir/hello-controller/some-method`` will execute the -``SubDir\HelloController::getSomeMethod()`` method. - -.. note:: When this option is enabled, the ``$translateURIDashes`` option is - ignored. - -Enable Translate URI To CamelCase ---------------------------------- - -To enable it, you need to change the setting ``$translateUriToCamelCase`` option -to ``true`` in **app/Config/Routing.php**:: - - public bool $translateUriToCamelCase = true; - +See :doc:`auto_routing_improved`. .. _controller-auto-routing-legacy: From 7ce54844646beef6d6bf7c7a0e95e4e3f8c08aa8 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 21 Jun 2024 14:15:49 +0900 Subject: [PATCH 109/277] docs: fix section title marks --- .../source/incoming/auto_routing_improved.rst | 54 +++++++++++-------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/user_guide_src/source/incoming/auto_routing_improved.rst b/user_guide_src/source/incoming/auto_routing_improved.rst index c95aff1b308d..cf82ab5f87bc 100644 --- a/user_guide_src/source/incoming/auto_routing_improved.rst +++ b/user_guide_src/source/incoming/auto_routing_improved.rst @@ -27,8 +27,9 @@ and execute the corresponding controller methods. .. _enabled-auto-routing-improved: +******************* Enable Auto Routing -=================== +******************* To use it, you need to change the setting ``$autoRoute`` option to ``true`` in **app/Config/Routing.php**:: @@ -38,8 +39,9 @@ And you need to change the property ``$autoRoutesImproved`` to ``true`` in **app public bool $autoRoutesImproved = true; +************ URI Segments -============ +************ The segments in the URL, in following with the Model-View-Controller approach, usually represent:: @@ -71,8 +73,9 @@ In the above example, CodeIgniter would attempt to find a controller named ``App .. note:: When a controller's short name matches the first segment of a URI, it will be loaded. +************************** Let's try it: Hello World! -========================== +************************** Let's create a simple controller so you can see it in action. Using your text editor, create a file called **Helloworld.php**, and put the following code in it. You will notice that the ``Helloworld`` Controller is extending the ``BaseController``. you can @@ -131,11 +134,12 @@ class so that it can inherit all its methods. .. literalinclude:: controllers/012.php +******* Methods -======= +******* Method Visibility ------------------ +================= When you define a method that is executable via HTTP request, the method must be declared as ``public``. @@ -143,13 +147,13 @@ declared as ``public``. .. warning:: For security reasons be sure to declare any new utility methods as ``protected`` or ``private``. Default Method --------------- +============== In the above example, the method name is ``getIndex()``. The method (HTTP verb + ``Index()``) is called the **default method**, and is loaded if the **second segment** of the URI is empty. Normal Methods --------------- +============== The second segment of the URI determines which method in the controller gets called. @@ -164,8 +168,9 @@ Now load the following URL to see the ``getComment()`` method:: You should see your new message. +************************************ Passing URI Segments to Your Methods -==================================== +************************************ If your URI contains more than two segments they will be passed to your method as parameters. @@ -178,15 +183,16 @@ Your method will be passed URI segments 3 and 4 (``'sandals'`` and ``'123'``): .. literalinclude:: controllers/022.php +****************** Default Controller -================== +****************** The Default Controller is a special controller that is used when a URI ends with a directory name or when a URI is not present, as will be the case when only your site root URL is requested. Defining a Default Controller ------------------------------ +============================= Let's try it with the ``Helloworld`` controller. @@ -215,8 +221,9 @@ For more information, please refer to the .. _controller-default-method-fallback: +*********************** Default Method Fallback -======================= +*********************** .. versionadded:: 4.4.0 @@ -237,7 +244,7 @@ The method will be passed URI segments 2 and 3 (``'15'`` and ``'edit'``): Not Found. Fallback to Default Controller ------------------------------- +============================== If the controller corresponding to the URI segment of the controller name does not exist, and if the default controller (``Home`` by default) exists in @@ -264,8 +271,9 @@ So the default method will be passed URI segments 2 (``'101'``): Auto Routing (Improved) does not execute the method, and it results in 404 Not Found. +************************************************ Organizing Your Controllers into Sub-directories -================================================ +************************************************ If you are building a large application you might want to hierarchically organize or structure your controllers into sub-directories. CodeIgniter @@ -303,8 +311,9 @@ CodeIgniter also permits you to map your URIs using its :ref:`Defined Route Rout .. _controller-translate-uri-to-camelcase: +************************** Translate URI To CamelCase -========================== +************************** .. versionadded:: 4.5.0 @@ -321,7 +330,7 @@ For example, the URI ``sub-dir/hello-controller/some-method`` will execute the ignored. Enable Translate URI To CamelCase ---------------------------------- +================================= To enable it, you need to change the setting ``$translateUriToCamelCase`` option to ``true`` in **app/Config/Routing.php**:: @@ -330,16 +339,17 @@ to ``true`` in **app/Config/Routing.php**:: .. _routing-auto-routing-improved-configuration-options: +********************* Configuration Options -===================== +********************* These options are available in the **app/Config/Routing.php** file. Default Controller ------------------- +================== For Site Root URI -^^^^^^^^^^^^^^^^^ +----------------- When a user visits the root of your site (i.e., **example.com**) the controller to use is determined by the value set to the ``$defaultController`` property, @@ -351,7 +361,7 @@ The default value for this is ``Home`` which matches the controller at public string $defaultController = 'Home'; For Directory URI -^^^^^^^^^^^^^^^^^ +----------------- The default controller is also used when no matching route has been found, and the URI would point to a directory in the controllers directory. For example, if the user visits **example.com/admin**, if a controller was found at @@ -365,7 +375,7 @@ See :ref:`Auto Routing in Controllers ` for mo .. _routing-auto-routing-improved-default-method: Default Method --------------- +============== This works similar to the default controller setting, but is used to determine the default method that is used when a controller is found that matches the URI, but no segment exists for the method. The default value is @@ -381,9 +391,9 @@ controller existed, the ``Products::getListAll()`` method would be executed:: .. _auto-routing-improved-module-routing: - +************** Module Routing -============== +************** .. versionadded:: 4.4.0 From 51607f89a8ca3ba19c3eb2a932cb219dd6ce61a0 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 21 Jun 2024 14:42:24 +0900 Subject: [PATCH 110/277] docs: add section title "What is Auto Routing (Improved) ?" and rewrite --- .../source/incoming/auto_routing_improved.rst | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/user_guide_src/source/incoming/auto_routing_improved.rst b/user_guide_src/source/incoming/auto_routing_improved.rst index cf82ab5f87bc..607752805df0 100644 --- a/user_guide_src/source/incoming/auto_routing_improved.rst +++ b/user_guide_src/source/incoming/auto_routing_improved.rst @@ -10,18 +10,23 @@ Auto Routing (Improved) .. _auto-routing-improved: -Since v4.2.0, the new more secure Auto Routing has been introduced. +********************************* +What is Auto Routing (Improved) ? +********************************* -.. note:: If you are familiar with Auto Routing, which was enabled by default - from CodeIgniter 3.x through 4.1.x, you can see the differences in - :ref:`ChangeLog v4.2.0 `. +By default, all routes must be :ref:`defined ` in the +configuration file. -When no defined route is found that matches the URI, the system will attempt to match that URI against the controllers and methods when Auto Routing is enabled. +However, with **Auto Routing (Improved)**, you can define the controller name and +its method name according to the convention and it will be automatically routed. +In other words, there is no need to define routes manually. -.. important:: For security reasons, if a controller is used in the defined routes, Auto Routing (Improved) does not route to the controller. +If you enable Auto Routing (Improved), when no defined route is found that matches +the URI, the system will attempt to match that URI against the controllers and +methods. -Auto Routing can automatically route HTTP requests based on conventions -and execute the corresponding controller methods. +.. important:: For security reasons, if a controller is used in the defined routes, + Auto Routing (Improved) does not route to the controller. .. note:: Auto Routing (Improved) is disabled by default. To use it, see below. From 38d8f7261843784ad7b0b52736648267bebdc66e Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 21 Jun 2024 14:43:03 +0900 Subject: [PATCH 111/277] docs: add sectioin "Differences from Auto Routing (Legacy)" --- .../source/incoming/auto_routing_improved.rst | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/user_guide_src/source/incoming/auto_routing_improved.rst b/user_guide_src/source/incoming/auto_routing_improved.rst index 607752805df0..8cf855c46f92 100644 --- a/user_guide_src/source/incoming/auto_routing_improved.rst +++ b/user_guide_src/source/incoming/auto_routing_improved.rst @@ -30,6 +30,31 @@ methods. .. note:: Auto Routing (Improved) is disabled by default. To use it, see below. +************************************** +Differences from Auto Routing (Legacy) +************************************** + +:ref:`auto-routing-legacy` is a routing system from CodeIgniter 3. If you are not +familiar with it, go to the next section. + +If you know it well, these are some changes in **Auto Routing (Improved)**: + +- A controller method needs HTTP verb prefix like ``getIndex()``, ``postCreate()``. + - Developers always know the HTTP method, so requests by an unexpected HTTP + method does not pass. +- The Default Controller (``Home`` by default) and the Default Method (``index`` by default) must be omitted in the URI. + - It restricts one-to-one correspondence between controller methods and URIs. + - E.g. by default, you can access ``/``, but ``/home`` and ``/home/index`` + will be 404. +- It checks method parameter count. + - If there are more parameters in the URI than the method parameters, it results + in 404. +- It does not support ``_remap()`` method. + - It restricts one-to-one correspondence between controller methods and URIs. +- Can't access controllers in Defined Routes. + - It completely separates controllers accessible via **Auto Routing** from + those accessible via **Defined Routes**. + .. _enabled-auto-routing-improved: ******************* From 27b420c77a36bf44b7f3388f41baa75409dea16c Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 21 Jun 2024 14:43:17 +0900 Subject: [PATCH 112/277] docs: change section title --- user_guide_src/source/incoming/auto_routing_improved.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/user_guide_src/source/incoming/auto_routing_improved.rst b/user_guide_src/source/incoming/auto_routing_improved.rst index 8cf855c46f92..bcc533c62301 100644 --- a/user_guide_src/source/incoming/auto_routing_improved.rst +++ b/user_guide_src/source/incoming/auto_routing_improved.rst @@ -57,9 +57,9 @@ If you know it well, these are some changes in **Auto Routing (Improved)**: .. _enabled-auto-routing-improved: -******************* -Enable Auto Routing -******************* +****************************** +Enable Auto Routing (Improved) +****************************** To use it, you need to change the setting ``$autoRoute`` option to ``true`` in **app/Config/Routing.php**:: From 2e3bf1d2d5e62215a9c84913ea5cbcceb605f547 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 21 Jun 2024 15:53:34 +0900 Subject: [PATCH 113/277] docs: rewrite "Auto Routing (Improved)" contents --- .../source/incoming/auto_routing_improved.rst | 254 ++++++++---------- .../incoming/auto_routing_improved/009.php | 8 + .../incoming/auto_routing_improved/010.php | 8 + .../incoming/auto_routing_improved/011.php | 8 + .../020.php | 2 +- .../021.php | 2 +- .../022.php | 4 +- .../source/incoming/controllers.rst | 4 +- user_guide_src/source/incoming/routing.rst | 4 +- 9 files changed, 148 insertions(+), 146 deletions(-) create mode 100644 user_guide_src/source/incoming/auto_routing_improved/009.php create mode 100644 user_guide_src/source/incoming/auto_routing_improved/010.php create mode 100644 user_guide_src/source/incoming/auto_routing_improved/011.php rename user_guide_src/source/incoming/{controllers => auto_routing_improved}/020.php (73%) rename user_guide_src/source/incoming/{controllers => auto_routing_improved}/021.php (82%) rename user_guide_src/source/incoming/{controllers => auto_routing_improved}/022.php (54%) diff --git a/user_guide_src/source/incoming/auto_routing_improved.rst b/user_guide_src/source/incoming/auto_routing_improved.rst index bcc533c62301..1fc15a8c5a73 100644 --- a/user_guide_src/source/incoming/auto_routing_improved.rst +++ b/user_guide_src/source/incoming/auto_routing_improved.rst @@ -1,3 +1,5 @@ +.. _auto-routing-improved: + ####################### Auto Routing (Improved) ####################### @@ -8,8 +10,6 @@ Auto Routing (Improved) :local: :depth: 3 -.. _auto-routing-improved: - ********************************* What is Auto Routing (Improved) ? ********************************* @@ -28,7 +28,8 @@ methods. .. important:: For security reasons, if a controller is used in the defined routes, Auto Routing (Improved) does not route to the controller. -.. note:: Auto Routing (Improved) is disabled by default. To use it, see below. +.. note:: Auto Routing (Improved) is disabled by default. To use it, see + :ref:`enabled-auto-routing-improved`. ************************************** Differences from Auto Routing (Legacy) @@ -51,6 +52,7 @@ If you know it well, these are some changes in **Auto Routing (Improved)**: in 404. - It does not support ``_remap()`` method. - It restricts one-to-one correspondence between controller methods and URIs. + But it has :ref:`controller-default-method-fallback`. - Can't access controllers in Defined Routes. - It completely separates controllers accessible via **Auto Routing** from those accessible via **Defined Routes**. @@ -61,11 +63,13 @@ If you know it well, these are some changes in **Auto Routing (Improved)**: Enable Auto Routing (Improved) ****************************** -To use it, you need to change the setting ``$autoRoute`` option to ``true`` in **app/Config/Routing.php**:: +To use it, you need to change the setting ``$autoRoute`` option to ``true`` in +**app/Config/Routing.php**:: public bool $autoRoute = true; -And you need to change the property ``$autoRoutesImproved`` to ``true`` in **app/Config/Feature.php**:: +And you need to change the property ``$autoRoutesImproved`` to ``true`` in +**app/Config/Feature.php**:: public bool $autoRoutesImproved = true; @@ -73,9 +77,10 @@ And you need to change the property ``$autoRoutesImproved`` to ``true`` in **app URI Segments ************ -The segments in the URL, in following with the Model-View-Controller approach, usually represent:: +The segments in the URL, in following with the Model-View-Controller approach, +usually represent:: - example.com/class/method/ID + http://example.com/class/method/ID 1. The first segment represents the controller **class** that should be invoked. 2. The second segment represents the class **method** that should be called. @@ -83,52 +88,55 @@ The segments in the URL, in following with the Model-View-Controller approach, u Consider this URI:: - example.com/index.php/helloworld/hello/1 + http://example.com/index.php/hello-world/hello/1 In the above example, when you send an HTTP request with **GET** method, -Auto Routing would attempt to find a controller named ``App\Controllers\Helloworld`` -and executes ``getHello()`` method with passing ``'1'`` as the first argument. - -.. note:: A controller method that will be executed by Auto Routing (Improved) needs HTTP verb (``get``, ``post``, ``put``, etc.) prefix like ``getIndex()``, ``postCreate()``. +Auto Routing (Improved) would attempt to find a controller named +``App\Controllers\HelloWorld`` and executes ``getHello()`` method with passing +``'1'`` as the first argument. -This section describes the functionality of the new auto-routing. -It automatically routes an HTTP request, and executes the corresponding controller method -without route definitions. +.. note:: A controller method that will be executed by Auto Routing (Improved) + needs HTTP verb (``get``, ``post``, ``put``, etc.) prefix like ``getIndex()``, + ``postCreate()``. -Consider this URI:: - - example.com/index.php/helloworld/ - -In the above example, CodeIgniter would attempt to find a controller named ``App\Controllers\Helloworld`` and load it, when auto-routing is enabled. - -.. note:: When a controller's short name matches the first segment of a URI, it will be loaded. +.. note:: When a controller's short name matches the first segment of a URI, it + will be loaded. ************************** Let's try it: Hello World! ************************** -Let's create a simple controller so you can see it in action. Using your text editor, create a file called **Helloworld.php**, -and put the following code in it. You will notice that the ``Helloworld`` Controller is extending the ``BaseController``. you can -also extend the ``CodeIgniter\Controller`` if you do not need the functionality of the BaseController. +Let's create a simple controller so you can see it in action. -The BaseController provides a convenient place for loading components and performing functions that are needed by all your -controllers. You can extend this class in any new controller. +Using your text editor, create a file called **HelloWorld.php** in your +**app/Controllers** directory, and put the following code in it. -.. literalinclude:: controllers/020.php +.. literalinclude:: auto_routing_improved/020.php -Then save the file to your **app/Controllers** directory. +You will notice that the ``HelloWorld`` Controller is extending the ``BaseController``. +you can also extend the ``CodeIgniter\Controller`` if you do not need the functionality +of the BaseController. -.. important:: The file must be called **Helloworld.php**, with a capital ``H``. When you use Auto Routing, Controller class names MUST start with an uppercase letter and ONLY the first character can be uppercase. +The BaseController provides a convenient place for loading components and performing +functions that are needed by all your controllers. You can extend this class in +any new controller. - Since v4.5.0, if you enable the ``$translateUriToCamelCase`` option, you can - use CamelCase classnames. See :ref:`controller-translate-uri-to-camelcase` - for details. +.. important:: The file must be called **HelloWorld.php**. When you use Auto + Routing (Improved), controller class names MUST be CamelCase. -.. important:: A controller method that will be executed by Auto Routing (Improved) needs HTTP verb (``get``, ``post``, ``put``, etc.) prefix like ``getIndex()``, ``postCreate()``. +.. important:: A controller method that will be executed by Auto Routing (Improved) + needs HTTP verb (``get``, ``post``, ``put``, etc.) prefix like ``getIndex()``, + ``postCreate()``. Now visit your site using a URL similar to this:: - example.com/index.php/helloworld + http://example.com/index.php/hello-world + +The system automatically translates URI with dashes (``-``) to CamelCase in the +controller and method URI segments. + +For example, the URI ``sub-dir/hello-controller/some-method`` will execute the +``SubDir\HelloController::getSomeMethod()`` method. If you did it right you should see:: @@ -136,34 +144,19 @@ If you did it right you should see:: This is valid: -.. literalinclude:: controllers/009.php +.. literalinclude:: auto_routing_improved/009.php This is **not** valid: -.. literalinclude:: controllers/010.php +.. literalinclude:: auto_routing_improved/010.php This is **not** valid: -.. literalinclude:: controllers/011.php - -.. note:: Since v4.5.0, if you enable the ``$translateUriToCamelCase`` option, - you can use CamelCase classnames like above. See - :ref:`controller-translate-uri-to-camelcase` for details. +.. literalinclude:: auto_routing_improved/011.php Also, always make sure your controller extends the parent controller class so that it can inherit all its methods. -.. note:: - The system will attempt to match the URI against Controllers by matching each segment against - directories/files in **app/Controllers**, when a match wasn't found against defined routes. - That's why your directories/files MUST start with a capital letter and the rest MUST be lowercase. - - If you want another naming convention you need to manually define it using the - :ref:`Defined Route Routing `. - Here is an example based on PSR-4 Autoloader: - - .. literalinclude:: controllers/012.php - ******* Methods ******* @@ -174,27 +167,28 @@ Method Visibility When you define a method that is executable via HTTP request, the method must be declared as ``public``. -.. warning:: For security reasons be sure to declare any new utility methods as ``protected`` or ``private``. +.. warning:: For security reasons be sure to declare any new utility methods as + ``protected`` or ``private``. Default Method ============== -In the above example, the method name is ``getIndex()``. -The method (HTTP verb + ``Index()``) is called the **default method**, and is loaded if the **second segment** of the URI is empty. +In the above example, the method name is ``getIndex()``. The method +(HTTP verb + ``Index()``) is called the **Default Method**, and is loaded if the +**second segment** of the URI is empty. Normal Methods ============== -The second segment of the URI determines which method in the -controller gets called. +The second segment of the URI determines which method in the controller gets called. Let's try it. Add a new method to your controller: -.. literalinclude:: controllers/021.php +.. literalinclude:: auto_routing_improved/021.php Now load the following URL to see the ``getComment()`` method:: - example.com/index.php/helloworld/comment/ + http://example.com/index.php/hello-world/comment/ You should see your new message. @@ -207,47 +201,24 @@ method as parameters. For example, let's say you have a URI like this:: - example.com/index.php/products/shoes/sandals/123 + http://example.com/index.php/products/shoes/sandals/123 Your method will be passed URI segments 3 and 4 (``'sandals'`` and ``'123'``): -.. literalinclude:: controllers/022.php +.. literalinclude:: auto_routing_improved/022.php ****************** Default Controller ****************** -The Default Controller is a special controller that is used when a URI ends with -a directory name or when a URI is not present, as will be the case when only your -site root URL is requested. - -Defining a Default Controller -============================= - -Let's try it with the ``Helloworld`` controller. +The **Default Controller** is a special controller that is used when a URI ends +with a directory name or when a URI is not present, as will be the case when only +your site root URL is requested. -To specify a default controller open your **app/Config/Routing.php** -file and set this property:: - - public string $defaultController = 'Helloworld'; - -Where ``Helloworld`` is the name of the controller class you want to be used. - -And comment out the line in **app/Config/Routes.php**: - -.. literalinclude:: controllers/016.php - :lines: 2- - -If you now browse to your site without specifying any URI segments you'll -see the "Hello World" message. - -.. important:: When you use Auto Routing (Improved), you must remove the line - ``$routes->get('/', 'Home::index');``. Because defined routes take - precedence over Auto Routing, and controllers defined in the defined routes - are denied access by Auto Routing (Improved) for security reasons. +By default, the Default Controller is ``Home``. For more information, please refer to the -:ref:`routing-auto-routing-improved-configuration-options` documentation. +:ref:`routing-auto-routing-improved-configuration-options`. .. _controller-default-method-fallback: @@ -265,7 +236,7 @@ are passed to the default method for execution. Load the following URL:: - example.com/index.php/product/15/edit + http://example.com/index.php/product/15/edit The method will be passed URI segments 2 and 3 (``'15'`` and ``'edit'``): @@ -288,10 +259,10 @@ For example, when you have the following default controller ``Home`` in the Load the following URL:: - example.com/index.php/news/101 + http://example.com/index.php/news/101 The ``News\Home`` controller and the default ``getIndex()`` method will be found. -So the default method will be passed URI segments 2 (``'101'``): +So the default method will get the second URI segment (``'101'``): .. note:: If there is ``App\Controllers\News`` controller, it takes precedence. The URI segments are searched sequentially and the first controller found @@ -312,11 +283,7 @@ permits you to do this. Simply create sub-directories under the main **app/Controllers**, and place your controller classes within them. -.. important:: Directory names MUST start with an uppercase letter and ONLY the first character can be uppercase. - - Since v4.5.0, if you enable the ``$translateUriToCamelCase`` option, you can - use CamelCase directory names. See :ref:`controller-translate-uri-to-camelcase` - for details. +.. important:: Directory names MUST start with an uppercase letter and be CamelCase. When using this feature the first segment of your URI must specify the directory. For example, let's say you have a controller located here:: @@ -325,7 +292,7 @@ specify the directory. For example, let's say you have a controller located here To call the above controller your URI will look something like this:: - example.com/index.php/products/shoes/show/123 + http://example.com/index.php/products/shoes/show/123 .. note:: You cannot have directories with the same name in **app/Controllers** and **public**. @@ -337,36 +304,6 @@ called if the URL contains *only* the sub-directory. Simply put a controller in there that matches the name of your default controller as specified in your **app/Config/Routing.php** file. -CodeIgniter also permits you to map your URIs using its :ref:`Defined Route Routing `.. - -.. _controller-translate-uri-to-camelcase: - -************************** -Translate URI To CamelCase -************************** - -.. versionadded:: 4.5.0 - -Since v4.5.0, the ``$translateUriToCamelCase`` option has been implemented, -which works well with the current CodeIgniter's coding standards. - -This option enables you to automatically translate URI with dashes (``-``) to -CamelCase in the controller and method URI segments. - -For example, the URI ``sub-dir/hello-controller/some-method`` will execute the -``SubDir\HelloController::getSomeMethod()`` method. - -.. note:: When this option is enabled, the ``$translateURIDashes`` option is - ignored. - -Enable Translate URI To CamelCase -================================= - -To enable it, you need to change the setting ``$translateUriToCamelCase`` option -to ``true`` in **app/Config/Routing.php**:: - - public bool $translateUriToCamelCase = true; - .. _routing-auto-routing-improved-configuration-options: ********************* @@ -390,34 +327,71 @@ The default value for this is ``Home`` which matches the controller at public string $defaultController = 'Home'; +.. important:: If you change the default controller name, you must remove the + line ``$routes->get('/', 'Home::index');`` in **app/Config/Routes.php**. + Because defined routes take precedence over Auto Routing, and controllers + defined in the defined routes are denied access by Auto Routing (Improved) + for security reasons. + For Directory URI ----------------- -The default controller is also used when no matching route has been found, and the URI would point to a directory -in the controllers directory. For example, if the user visits **example.com/admin**, if a controller was found at +The default controller is also used when no matching route has been found, and +the URI would point to a directory in the controllers directory. For example, if +the user visits **example.com/admin**, if a controller was found at **app/Controllers/Admin/Home.php**, it would be used. -.. important:: You cannot access the default controller with the URI of the controller name. - When the default controller is ``Home``, you can access **example.com/**, but if you access **example.com/home**, it will be not found. - -See :ref:`Auto Routing in Controllers ` for more info. +.. important:: You cannot access the default controller with the URI of the + controller name. When the default controller is ``Home``, you can access + **example.com/**, but if you access **example.com/home**, it will be not found. .. _routing-auto-routing-improved-default-method: Default Method ============== -This works similar to the default controller setting, but is used to determine the default method that is used -when a controller is found that matches the URI, but no segment exists for the method. The default value is -``index``. +This works similar to the default controller setting, but is used to determine +the default method that is used when a controller is found that matches the URI, +but no segment exists for the method. The default value is ``index``. In this example, if the user were to visit **example.com/products**, and a ``Products`` controller existed, the ``Products::getListAll()`` method would be executed:: public string $defaultMethod = 'listAll'; -.. important:: You cannot access the controller with the URI of the default method name. - In the example above, you can access **example.com/products**, but if you access **example.com/products/listall**, it will be not found. +.. important:: You cannot access the controller with the URI of the default method + name. In the example above, you can access **example.com/products**, but if + you access **example.com/products/listall**, it will be not found. + +.. _controller-translate-uri-to-camelcase: + +Translate URI To CamelCase +========================== + +.. versionadded:: 4.5.0 + +.. note:: Since v4.6.0, the ``$translateUriToCamelCase`` option is enabled by + default. + +Since v4.5.0, the ``$translateUriToCamelCase`` option has been implemented, +which works well with the current CodeIgniter's coding standards. + +This option enables you to automatically translate URI with dashes (``-``) to +CamelCase in the controller and method URI segments. + +For example, the URI ``sub-dir/hello-controller/some-method`` will execute the +``SubDir\HelloController::getSomeMethod()`` method. + +.. note:: When this option is enabled, the ``$translateURIDashes`` option is + ignored. + +Disable Translate URI To CamelCase +---------------------------------- + +To disable it, you need to change the setting ``$translateUriToCamelCase`` option +to ``false`` in **app/Config/Routing.php**:: + + public bool $translateUriToCamelCase = false; .. _auto-routing-improved-module-routing: diff --git a/user_guide_src/source/incoming/auto_routing_improved/009.php b/user_guide_src/source/incoming/auto_routing_improved/009.php new file mode 100644 index 000000000000..7615eecaeba0 --- /dev/null +++ b/user_guide_src/source/incoming/auto_routing_improved/009.php @@ -0,0 +1,8 @@ + Date: Fri, 21 Jun 2024 16:32:01 +0900 Subject: [PATCH 114/277] test: update test code for config changes --- tests/system/Test/FeatureTestTraitTest.php | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/system/Test/FeatureTestTraitTest.php b/tests/system/Test/FeatureTestTraitTest.php index 4fd921d6e9a6..b3ee3eadb4cc 100644 --- a/tests/system/Test/FeatureTestTraitTest.php +++ b/tests/system/Test/FeatureTestTraitTest.php @@ -20,6 +20,7 @@ use CodeIgniter\HTTP\Response; use CodeIgniter\Test\Mock\MockCodeIgniter; use Config\App; +use Config\Feature; use Config\Routing; use Config\Services; use PHPUnit\Framework\Attributes\DataProvider; @@ -359,17 +360,19 @@ public static function provideOpenCliRoutesFromHttpGot404(): iterable ]; } - /** - * @param mixed $from - * @param mixed $to - * @param mixed $httpGet - */ + private function disableAutoRoutesImproved(): void + { + $featureConfig = config(Feature::class); + $featureConfig->autoRoutesImproved = false; + } + #[DataProvider('provideOpenCliRoutesFromHttpGot404')] - public function testOpenCliRoutesFromHttpGot404($from, $to, $httpGet): void + public function testOpenCliRoutesFromHttpGot404(string $from, string $to, string $httpGet): void { $this->expectException(PageNotFoundException::class); $this->expectExceptionMessage('Cannot access CLI Route: '); + $this->disableAutoRoutesImproved(); $collection = Services::routes(); $collection->setAutoRoute(true); $collection->setDefaultNamespace('Tests\Support\Controllers'); @@ -641,6 +644,7 @@ public function testSetupRequestBodyWithBody(): void public function testAutoRoutingLegacy(): void { + $this->disableAutoRoutesImproved(); $config = config(Routing::class); $config->autoRoute = true; Factories::injectMock('config', Routing::class, $config); From dd33f98a32dfc9b31aa0d88c60016e5a21c687ef Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 23 Jun 2024 20:30:53 +0900 Subject: [PATCH 115/277] docs: add note --- user_guide_src/source/incoming/auto_routing_improved.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/user_guide_src/source/incoming/auto_routing_improved.rst b/user_guide_src/source/incoming/auto_routing_improved.rst index 1fc15a8c5a73..95e65e387def 100644 --- a/user_guide_src/source/incoming/auto_routing_improved.rst +++ b/user_guide_src/source/incoming/auto_routing_improved.rst @@ -388,6 +388,9 @@ For example, the URI ``sub-dir/hello-controller/some-method`` will execute the Disable Translate URI To CamelCase ---------------------------------- +.. note:: The option to disable "Translate URI To CamelCase" exists only for + backward compatibility. We don't recommend to disable it. + To disable it, you need to change the setting ``$translateUriToCamelCase`` option to ``false`` in **app/Config/Routing.php**:: From 580a59b7e0bd3165d200b445d24d2e6121ec376a Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 23 Jun 2024 20:31:05 +0900 Subject: [PATCH 116/277] docs: add "Examples of Controller/Methods and URIs" --- .../source/incoming/auto_routing_improved.rst | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/user_guide_src/source/incoming/auto_routing_improved.rst b/user_guide_src/source/incoming/auto_routing_improved.rst index 95e65e387def..8043995e0f67 100644 --- a/user_guide_src/source/incoming/auto_routing_improved.rst +++ b/user_guide_src/source/incoming/auto_routing_improved.rst @@ -304,6 +304,32 @@ called if the URL contains *only* the sub-directory. Simply put a controller in there that matches the name of your default controller as specified in your **app/Config/Routing.php** file. +*************************************** +Examples of Controller/Methods and URIs +*************************************** + +In the case of a **GET** request with the default configuration, the mapping +between controller/methods and URIs is as follows: + +============================ ============================ ============================================= +Controller/Method URI Description +============================ ============================ ============================================= +``Home::getIndex()`` / The default controller and the default method. +``Blog::getIndex()`` /blog The default method. +``UserProfile::getIndex()`` /user-profile The default method. +``Blog::getTags()`` /blog/tags +``Blog::getNews($id)`` /blog/news/123 +``Blog\Home::getIndex()`` /blog Sub-directory ``Blog`` and the default + controller and the default method. If there + is ``Blog`` controller, it takes precedence. +``Blog\Tags::getIndex()`` /blog/tags Sub-directory ``Blog`` and the default + method. If there is ``Blog`` controller, it + takes precedence. +``Blog\News::getIndex($id)`` /blog/news/123 Sub-directory ``Blog`` and the default method + fallback. If there is ``Blog`` controller, it + takes precedence. +============================ ============================ ============================================= + .. _routing-auto-routing-improved-configuration-options: ********************* From fd00dfab7825091d0630aeb2bb7513757ebe07dd Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 23 Jun 2024 21:40:47 +0900 Subject: [PATCH 117/277] docs: update :ref: ids --- user_guide_src/source/changelogs/v4.4.0.rst | 2 +- user_guide_src/source/changelogs/v4.5.0.rst | 2 +- user_guide_src/source/incoming/auto_routing_improved.rst | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 59092ccd9860..df0b712f3f35 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -261,7 +261,7 @@ Others - If a controller is found that corresponds to a URI segment and that controller does not have a method defined for the URI segment, the default method will now be executed. This addition allows for more flexible handling of URIs in - auto routing. See :ref:`controller-default-method-fallback` for details. + auto routing. See :ref:`auto-routing-improved-default-method-fallback` for details. - **Filters:** Now you can use Filter Arguments with :ref:`$filters property `. - **Request:** Added ``IncomingRequest::setValidLocales()`` method to set valid locales. - **Table:** Added ``Table::setSyncRowsWithHeading()`` method to synchronize row columns with headings. See :ref:`table-sync-rows-with-headings` for details. diff --git a/user_guide_src/source/changelogs/v4.5.0.rst b/user_guide_src/source/changelogs/v4.5.0.rst index 1f978a4dcfc6..97cfe9ec10b0 100644 --- a/user_guide_src/source/changelogs/v4.5.0.rst +++ b/user_guide_src/source/changelogs/v4.5.0.rst @@ -62,7 +62,7 @@ Routing - **AutoRouting Improved:** The ``$translateUriToCamelCase`` option has been added that allows using CamelCase controller and method names. See - :ref:`controller-translate-uri-to-camelcase`. + :ref:`translate-uri-to-camelcase`. - **Others:** - Added option ``$multipleSegmentsOneParam``. When this option is enabled, a placeholder that matches multiple segments, such as ``(:any)``, will diff --git a/user_guide_src/source/incoming/auto_routing_improved.rst b/user_guide_src/source/incoming/auto_routing_improved.rst index 8043995e0f67..c9ee8562cc82 100644 --- a/user_guide_src/source/incoming/auto_routing_improved.rst +++ b/user_guide_src/source/incoming/auto_routing_improved.rst @@ -52,7 +52,7 @@ If you know it well, these are some changes in **Auto Routing (Improved)**: in 404. - It does not support ``_remap()`` method. - It restricts one-to-one correspondence between controller methods and URIs. - But it has :ref:`controller-default-method-fallback`. + But it has :ref:`auto-routing-improved-default-method-fallback`. - Can't access controllers in Defined Routes. - It completely separates controllers accessible via **Auto Routing** from those accessible via **Defined Routes**. @@ -220,7 +220,7 @@ By default, the Default Controller is ``Home``. For more information, please refer to the :ref:`routing-auto-routing-improved-configuration-options`. -.. _controller-default-method-fallback: +.. _auto-routing-improved-default-method-fallback: *********************** Default Method Fallback @@ -389,7 +389,7 @@ controller existed, the ``Products::getListAll()`` method would be executed:: name. In the example above, you can access **example.com/products**, but if you access **example.com/products/listall**, it will be not found. -.. _controller-translate-uri-to-camelcase: +.. _translate-uri-to-camelcase: Translate URI To CamelCase ========================== From ecfbc2f7786f3a0f9e449c6757c9c854666c32ee Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 23 Jun 2024 21:54:56 +0900 Subject: [PATCH 118/277] docs: add notes --- user_guide_src/source/incoming/auto_routing_improved.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/user_guide_src/source/incoming/auto_routing_improved.rst b/user_guide_src/source/incoming/auto_routing_improved.rst index c9ee8562cc82..35d821c5b200 100644 --- a/user_guide_src/source/incoming/auto_routing_improved.rst +++ b/user_guide_src/source/incoming/auto_routing_improved.rst @@ -207,6 +207,10 @@ Your method will be passed URI segments 3 and 4 (``'sandals'`` and ``'123'``): .. literalinclude:: auto_routing_improved/022.php +.. note:: If there are more parameters in the URI than the method parameters, + Auto Routing (Improved) does not execute the method, and it results in 404 + Not Found. + ****************** Default Controller ****************** @@ -217,6 +221,10 @@ your site root URL is requested. By default, the Default Controller is ``Home``. +.. note:: Define only the default method (``getIndex()`` for GET requests) + in the default controller. If you define any other public method, that method + will not be executed. + For more information, please refer to the :ref:`routing-auto-routing-improved-configuration-options`. From 23ae69c26d7b9e809b3525241d82701724c567c9 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 24 Jun 2024 18:38:40 +0900 Subject: [PATCH 119/277] docs: add upgrade_460 --- user_guide_src/source/installation/upgrade_460.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/installation/upgrade_460.rst b/user_guide_src/source/installation/upgrade_460.rst index 04388acf7d08..8546df1903f2 100644 --- a/user_guide_src/source/installation/upgrade_460.rst +++ b/user_guide_src/source/installation/upgrade_460.rst @@ -91,7 +91,10 @@ and it is recommended that you merge the updated versions with your application: Config ------ -- @TODO +- app/Config/Feature.php + - ``Config\Feature::$autoRoutesImproved`` has been changed to ``true``. +- app/Config/Routing.php + - ``Config\Routing::$translateUriToCamelCase`` has been changed to ``true``. All Changes =========== From 9efac84b51e9668962b346d2fd5848118e33fe17 Mon Sep 17 00:00:00 2001 From: ducng99 Date: Tue, 25 Jun 2024 09:39:45 +1200 Subject: [PATCH 120/277] test: Skip running tests if DB is not MySQLi --- tests/system/Database/Live/MySQLi/FoundRowsTest.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/system/Database/Live/MySQLi/FoundRowsTest.php b/tests/system/Database/Live/MySQLi/FoundRowsTest.php index 4e5299f8d7aa..d5b88b49af28 100644 --- a/tests/system/Database/Live/MySQLi/FoundRowsTest.php +++ b/tests/system/Database/Live/MySQLi/FoundRowsTest.php @@ -28,7 +28,9 @@ final class FoundRowsTest extends CIUnitTestCase use DatabaseTestTrait; /** - * @var array + * Database config for tests + * + * @var array */ private $tests; @@ -37,8 +39,6 @@ final class FoundRowsTest extends CIUnitTestCase protected function setUp(): void { - parent::setUp(); - $config = config('Database'); $this->tests = $config->tests; @@ -46,6 +46,8 @@ protected function setUp(): void if ($this->tests['DBDriver'] !== 'MySQLi') { $this->markTestSkipped('Only MySQLi can complete this test.'); } + + parent::setUp(); } public function testEnableFoundRows(): void From 50baeddcc8d6582d7d2dc00acd73145c0f9d2110 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 25 Jun 2024 07:56:41 +0900 Subject: [PATCH 121/277] docs: change note Even if you don't change the default controller name, you must remove the defined route `$routes->get('/', 'Home::index');`. --- .../source/incoming/auto_routing_improved.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/user_guide_src/source/incoming/auto_routing_improved.rst b/user_guide_src/source/incoming/auto_routing_improved.rst index 35d821c5b200..fe5b9013420a 100644 --- a/user_guide_src/source/incoming/auto_routing_improved.rst +++ b/user_guide_src/source/incoming/auto_routing_improved.rst @@ -73,6 +73,12 @@ And you need to change the property ``$autoRoutesImproved`` to ``true`` in public bool $autoRoutesImproved = true; +.. important:: When you use Auto Routing (Improved), you must remove the line + ``$routes->get('/', 'Home::index');`` in **app/Config/Routes.php**. Because + defined routes take precedence over Auto Routing, and controllers defined in + the defined routes are denied access by Auto Routing (Improved) for security + reasons. + ************ URI Segments ************ @@ -361,12 +367,6 @@ The default value for this is ``Home`` which matches the controller at public string $defaultController = 'Home'; -.. important:: If you change the default controller name, you must remove the - line ``$routes->get('/', 'Home::index');`` in **app/Config/Routes.php**. - Because defined routes take precedence over Auto Routing, and controllers - defined in the defined routes are denied access by Auto Routing (Improved) - for security reasons. - For Directory URI ----------------- From c481d2e9a3036b58a2f0e404d6a65074bde4a968 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 25 Jun 2024 08:04:34 +0900 Subject: [PATCH 122/277] docs: improved descriptions --- .../source/incoming/auto_routing_improved.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/user_guide_src/source/incoming/auto_routing_improved.rst b/user_guide_src/source/incoming/auto_routing_improved.rst index fe5b9013420a..7056ba92402d 100644 --- a/user_guide_src/source/incoming/auto_routing_improved.rst +++ b/user_guide_src/source/incoming/auto_routing_improved.rst @@ -41,18 +41,19 @@ familiar with it, go to the next section. If you know it well, these are some changes in **Auto Routing (Improved)**: - A controller method needs HTTP verb prefix like ``getIndex()``, ``postCreate()``. - - Developers always know the HTTP method, so requests by an unexpected HTTP - method does not pass. + - Since developers always know the HTTP method, a request with an unexpected + HTTP method will never execute the controller. - The Default Controller (``Home`` by default) and the Default Method (``index`` by default) must be omitted in the URI. - It restricts one-to-one correspondence between controller methods and URIs. - E.g. by default, you can access ``/``, but ``/home`` and ``/home/index`` - will be 404. + will be 404 Not Found. - It checks method parameter count. - If there are more parameters in the URI than the method parameters, it results - in 404. + in 404 Not Found. - It does not support ``_remap()`` method. - It restricts one-to-one correspondence between controller methods and URIs. - But it has :ref:`auto-routing-improved-default-method-fallback`. + - But it has the :ref:`auto-routing-improved-default-method-fallback` feature + instead. - Can't access controllers in Defined Routes. - It completely separates controllers accessible via **Auto Routing** from those accessible via **Defined Routes**. From c953a68c120c0a6e309bd3f42930c7d0f4995f12 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 25 Jun 2024 08:09:19 +0900 Subject: [PATCH 123/277] docs: remove `index.php` in sample URLs --- .../source/incoming/auto_routing_improved.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/user_guide_src/source/incoming/auto_routing_improved.rst b/user_guide_src/source/incoming/auto_routing_improved.rst index 7056ba92402d..f0232f146ce2 100644 --- a/user_guide_src/source/incoming/auto_routing_improved.rst +++ b/user_guide_src/source/incoming/auto_routing_improved.rst @@ -95,7 +95,7 @@ usually represent:: Consider this URI:: - http://example.com/index.php/hello-world/hello/1 + http://example.com/hello-world/hello/1 In the above example, when you send an HTTP request with **GET** method, Auto Routing (Improved) would attempt to find a controller named @@ -137,7 +137,7 @@ any new controller. Now visit your site using a URL similar to this:: - http://example.com/index.php/hello-world + http://example.com/hello-world The system automatically translates URI with dashes (``-``) to CamelCase in the controller and method URI segments. @@ -195,7 +195,7 @@ Let's try it. Add a new method to your controller: Now load the following URL to see the ``getComment()`` method:: - http://example.com/index.php/hello-world/comment/ + http://example.com/hello-world/comment/ You should see your new message. @@ -208,7 +208,7 @@ method as parameters. For example, let's say you have a URI like this:: - http://example.com/index.php/products/shoes/sandals/123 + http://example.com/products/shoes/sandals/123 Your method will be passed URI segments 3 and 4 (``'sandals'`` and ``'123'``): @@ -251,7 +251,7 @@ are passed to the default method for execution. Load the following URL:: - http://example.com/index.php/product/15/edit + http://example.com/product/15/edit The method will be passed URI segments 2 and 3 (``'15'`` and ``'edit'``): @@ -274,7 +274,7 @@ For example, when you have the following default controller ``Home`` in the Load the following URL:: - http://example.com/index.php/news/101 + http://example.com/news/101 The ``News\Home`` controller and the default ``getIndex()`` method will be found. So the default method will get the second URI segment (``'101'``): @@ -307,7 +307,7 @@ specify the directory. For example, let's say you have a controller located here To call the above controller your URI will look something like this:: - http://example.com/index.php/products/shoes/show/123 + http://example.com/products/shoes/show/123 .. note:: You cannot have directories with the same name in **app/Controllers** and **public**. From c267bc0f18156bf2c5a3bd3a8ced51ada5387944 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 25 Jun 2024 08:12:00 +0900 Subject: [PATCH 124/277] docs: improve explanation --- user_guide_src/source/incoming/auto_routing_improved.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/user_guide_src/source/incoming/auto_routing_improved.rst b/user_guide_src/source/incoming/auto_routing_improved.rst index f0232f146ce2..2ad1fed2ed7d 100644 --- a/user_guide_src/source/incoming/auto_routing_improved.rst +++ b/user_guide_src/source/incoming/auto_routing_improved.rst @@ -87,11 +87,11 @@ URI Segments The segments in the URL, in following with the Model-View-Controller approach, usually represent:: - http://example.com/class/method/ID + http://example.com/{class}/{method}/{param1} 1. The first segment represents the controller **class** that should be invoked. 2. The second segment represents the class **method** that should be called. -3. The third, and any additional segments, represent the ID and any variables that will be passed to the controller. +3. The third, and any additional segments, represent any **parameters** that will be passed to the controller. Consider this URI:: @@ -100,7 +100,7 @@ Consider this URI:: In the above example, when you send an HTTP request with **GET** method, Auto Routing (Improved) would attempt to find a controller named ``App\Controllers\HelloWorld`` and executes ``getHello()`` method with passing -``'1'`` as the first argument. +``'1'`` as the first parameter. .. note:: A controller method that will be executed by Auto Routing (Improved) needs HTTP verb (``get``, ``post``, ``put``, etc.) prefix like ``getIndex()``, From 02e645c3c8e5f315b750a0ed019a7737c2c3471a Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 25 Jun 2024 08:26:24 +0900 Subject: [PATCH 125/277] docs: add sub section titles and explanations --- .../source/incoming/auto_routing_improved.rst | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/user_guide_src/source/incoming/auto_routing_improved.rst b/user_guide_src/source/incoming/auto_routing_improved.rst index 2ad1fed2ed7d..0726b1876783 100644 --- a/user_guide_src/source/incoming/auto_routing_improved.rst +++ b/user_guide_src/source/incoming/auto_routing_improved.rst @@ -115,26 +115,32 @@ Let's try it: Hello World! Let's create a simple controller so you can see it in action. +Create a Controller +=================== + Using your text editor, create a file called **HelloWorld.php** in your **app/Controllers** directory, and put the following code in it. .. literalinclude:: auto_routing_improved/020.php +.. important:: The file must be called **HelloWorld.php**. When you use Auto + Routing (Improved), controller class names MUST be CamelCase. + You will notice that the ``HelloWorld`` Controller is extending the ``BaseController``. -you can also extend the ``CodeIgniter\Controller`` if you do not need the functionality +You can also extend the ``CodeIgniter\Controller`` if you do not need the functionality of the BaseController. The BaseController provides a convenient place for loading components and performing functions that are needed by all your controllers. You can extend this class in any new controller. -.. important:: The file must be called **HelloWorld.php**. When you use Auto - Routing (Improved), controller class names MUST be CamelCase. - .. important:: A controller method that will be executed by Auto Routing (Improved) needs HTTP verb (``get``, ``post``, ``put``, etc.) prefix like ``getIndex()``, ``postCreate()``. +Visit Your Site +=============== + Now visit your site using a URL similar to this:: http://example.com/hello-world @@ -149,21 +155,21 @@ If you did it right you should see:: Hello World! -This is valid: +Controller Name Examples +======================== + +This is an valid controller name. Because ``App/Controllers/HelloWorld`` is CamelCase. .. literalinclude:: auto_routing_improved/009.php -This is **not** valid: +This is **not** valid. Because the first letter (``h``) is not capital. .. literalinclude:: auto_routing_improved/010.php -This is **not** valid: +This is also **not** valid. Because the first letter (``h``) is not capital. .. literalinclude:: auto_routing_improved/011.php -Also, always make sure your controller extends the parent controller -class so that it can inherit all its methods. - ******* Methods ******* From 75861b0f844c7fe97c42450dd9bc0f5d6f09638f Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 25 Jun 2024 08:38:36 +0900 Subject: [PATCH 126/277] docs: add section "Check the Routes" --- .../source/incoming/auto_routing_improved.rst | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/incoming/auto_routing_improved.rst b/user_guide_src/source/incoming/auto_routing_improved.rst index 0726b1876783..122178e1c15e 100644 --- a/user_guide_src/source/incoming/auto_routing_improved.rst +++ b/user_guide_src/source/incoming/auto_routing_improved.rst @@ -138,6 +138,27 @@ any new controller. needs HTTP verb (``get``, ``post``, ``put``, etc.) prefix like ``getIndex()``, ``postCreate()``. +Check the Routes +================ + +You can check your routes with the ``spark routes`` command. + +.. code-block:: console + + php spark routes + +If you did it right, you should see: + +.. code-block:: none + + +-----------+-------------+------+---------------------------------------+----------------+---------------+ + | Method | Route | Name | Handler | Before Filters | After Filters | + +-----------+-------------+------+---------------------------------------+----------------+---------------+ + | GET(auto) | hello-world | | \App\Controllers\HelloWorld::getIndex | | | + +-----------+-------------+------+---------------------------------------+----------------+---------------+ + +See :ref:`routing-spark-routes` for the output details. + Visit Your Site =============== @@ -151,7 +172,7 @@ controller and method URI segments. For example, the URI ``sub-dir/hello-controller/some-method`` will execute the ``SubDir\HelloController::getSomeMethod()`` method. -If you did it right you should see:: +If you did it right, you should see:: Hello World! From 939766ebd80da11c7a99ec06877f94f3962a6065 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 25 Jun 2024 08:40:46 +0900 Subject: [PATCH 127/277] docs: add `http://` to example URLs --- user_guide_src/source/incoming/auto_routing_improved.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/user_guide_src/source/incoming/auto_routing_improved.rst b/user_guide_src/source/incoming/auto_routing_improved.rst index 122178e1c15e..617ce9519c0e 100644 --- a/user_guide_src/source/incoming/auto_routing_improved.rst +++ b/user_guide_src/source/incoming/auto_routing_improved.rst @@ -386,7 +386,7 @@ Default Controller For Site Root URI ----------------- -When a user visits the root of your site (i.e., **example.com**) the controller +When a user visits the root of your site (i.e., **http://example.com**) the controller to use is determined by the value set to the ``$defaultController`` property, unless a route exists for it explicitly. @@ -400,12 +400,13 @@ For Directory URI The default controller is also used when no matching route has been found, and the URI would point to a directory in the controllers directory. For example, if -the user visits **example.com/admin**, if a controller was found at +the user visits **http://example.com/admin**, if a controller was found at **app/Controllers/Admin/Home.php**, it would be used. .. important:: You cannot access the default controller with the URI of the controller name. When the default controller is ``Home``, you can access - **example.com/**, but if you access **example.com/home**, it will be not found. + **http://example.com/**, but if you access **http://example.com/home**, it + will be not found. .. _routing-auto-routing-improved-default-method: From 990e89e3c4a4b2da47517f6b8a41f7615b3ea718 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 25 Jun 2024 08:49:00 +0900 Subject: [PATCH 128/277] docs: fix FQCN --- user_guide_src/source/incoming/auto_routing_improved.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/incoming/auto_routing_improved.rst b/user_guide_src/source/incoming/auto_routing_improved.rst index 617ce9519c0e..e0beeffdaefe 100644 --- a/user_guide_src/source/incoming/auto_routing_improved.rst +++ b/user_guide_src/source/incoming/auto_routing_improved.rst @@ -179,7 +179,7 @@ If you did it right, you should see:: Controller Name Examples ======================== -This is an valid controller name. Because ``App/Controllers/HelloWorld`` is CamelCase. +This is an valid controller name. Because ``App\Controllers\HelloWorld`` is CamelCase. .. literalinclude:: auto_routing_improved/009.php From 70ebb3383396a0133e82ebac5ed6c31669d2714a Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 25 Jun 2024 08:49:17 +0900 Subject: [PATCH 129/277] docs: improve section titles --- .../source/incoming/auto_routing_improved.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/user_guide_src/source/incoming/auto_routing_improved.rst b/user_guide_src/source/incoming/auto_routing_improved.rst index e0beeffdaefe..200440624b1f 100644 --- a/user_guide_src/source/incoming/auto_routing_improved.rst +++ b/user_guide_src/source/incoming/auto_routing_improved.rst @@ -176,8 +176,9 @@ If you did it right, you should see:: Hello World! -Controller Name Examples -======================== +**************************** +Examples of Controller Names +**************************** This is an valid controller name. Because ``App\Controllers\HelloWorld`` is CamelCase. @@ -191,9 +192,9 @@ This is also **not** valid. Because the first letter (``h``) is not capital. .. literalinclude:: auto_routing_improved/011.php -******* -Methods -******* +****************** +Controller Methods +****************** Method Visibility ================= From 9494c40cb6626977041103cb0b409f5e71c732e2 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 25 Jun 2024 10:59:27 +0900 Subject: [PATCH 130/277] docs: replace "This is" --- user_guide_src/source/incoming/auto_routing_improved.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/user_guide_src/source/incoming/auto_routing_improved.rst b/user_guide_src/source/incoming/auto_routing_improved.rst index 200440624b1f..74249b64f7a8 100644 --- a/user_guide_src/source/incoming/auto_routing_improved.rst +++ b/user_guide_src/source/incoming/auto_routing_improved.rst @@ -180,15 +180,16 @@ If you did it right, you should see:: Examples of Controller Names **************************** -This is an valid controller name. Because ``App\Controllers\HelloWorld`` is CamelCase. +The following is an valid controller name. Because ``App\Controllers\HelloWorld`` +is CamelCase. .. literalinclude:: auto_routing_improved/009.php -This is **not** valid. Because the first letter (``h``) is not capital. +The following is **not** valid. Because the first letter (``h``) is not capital. .. literalinclude:: auto_routing_improved/010.php -This is also **not** valid. Because the first letter (``h``) is not capital. +The following is also **not** valid. Because the first letter (``h``) is not capital. .. literalinclude:: auto_routing_improved/011.php From 0ad823c91989f5d9bf6dc333a2252e32ba5ae6e8 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 25 Jun 2024 10:59:46 +0900 Subject: [PATCH 131/277] docs: fix description --- user_guide_src/source/incoming/auto_routing_improved.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/incoming/auto_routing_improved.rst b/user_guide_src/source/incoming/auto_routing_improved.rst index 74249b64f7a8..62c1d5ea764a 100644 --- a/user_guide_src/source/incoming/auto_routing_improved.rst +++ b/user_guide_src/source/incoming/auto_routing_improved.rst @@ -91,7 +91,7 @@ usually represent:: 1. The first segment represents the controller **class** that should be invoked. 2. The second segment represents the class **method** that should be called. -3. The third, and any additional segments, represent any **parameters** that will be passed to the controller. +3. The third, and any additional segments, represent any **parameters** that will be passed to the controller method. Consider this URI:: From f0db26ff3d0fff42779b44389361e1c5077988a4 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 25 Jun 2024 14:41:11 +0900 Subject: [PATCH 132/277] docs: add section "Applying Filters" --- .../source/incoming/auto_routing_improved.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/user_guide_src/source/incoming/auto_routing_improved.rst b/user_guide_src/source/incoming/auto_routing_improved.rst index 62c1d5ea764a..1ed1cb304c6b 100644 --- a/user_guide_src/source/incoming/auto_routing_improved.rst +++ b/user_guide_src/source/incoming/auto_routing_improved.rst @@ -374,6 +374,17 @@ Controller/Method URI Description takes precedence. ============================ ============================ ============================================= +**************** +Applying Filters +**************** + +Applying controller filters allows you to add processing before and after the +controller method execution. This is especially handy during authentication or +api logging. + +If you use Auto Routing, set the filters to be applied in **app/Config/Filters.php**. +See :doc:`Controller Filters ` for more information on setting up filters. + .. _routing-auto-routing-improved-configuration-options: ********************* From ddf52ff74e6a7ad2f725296de4c582757f309b68 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 12 Apr 2024 11:34:24 +0900 Subject: [PATCH 133/277] feat: add resetTransStatus() --- system/Database/BaseConnection.php | 10 ++++ .../Live/TransactionDBDebugTrueTest.php | 48 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index 6581dc7c5ac0..663f9c120a0c 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -897,6 +897,16 @@ public function transRollback(): bool return false; } + /** + * Reset transaction status - to restart transactions after strict mode failure + */ + public function resetTransStatus(): static + { + $this->transStatus = true; + + return $this; + } + /** * Begin Transaction */ diff --git a/tests/system/Database/Live/TransactionDBDebugTrueTest.php b/tests/system/Database/Live/TransactionDBDebugTrueTest.php index da77edb9b975..cbfa6a1ff289 100644 --- a/tests/system/Database/Live/TransactionDBDebugTrueTest.php +++ b/tests/system/Database/Live/TransactionDBDebugTrueTest.php @@ -208,6 +208,54 @@ public function testTransStrictFalse(): void $this->seeInDatabase('job', ['name' => 'Comedian']); } + public function testTransStrictTrueAndResetTransStatus(): void + { + $builder = $this->db->table('job'); + + // The first transaction group + $this->db->transStart(); + + $jobData = [ + 'name' => 'Grocery Sales', + 'description' => 'Discount!', + ]; + $builder->insert($jobData); + + $this->assertTrue($this->db->transStatus()); + + // Duplicate entry '1' for key 'PRIMARY' + $jobData = [ + 'id' => 1, + 'name' => 'Comedian', + 'description' => 'Theres something in your teeth', + ]; + $builder->insert($jobData); + + $this->assertFalse($this->db->transStatus()); + + $this->db->transComplete(); + + $this->dontSeeInDatabase('job', ['name' => 'Grocery Sales']); + + // Resets TransStatus + $this->db->resetTransStatus(); + + // The second transaction group + $this->db->transStart(); + + $jobData = [ + 'name' => 'Comedian', + 'description' => 'Theres something in your teeth', + ]; + $builder->insert($jobData); + + $this->assertTrue($this->db->transStatus()); + + $this->db->transComplete(); + + $this->seeInDatabase('job', ['name' => 'Comedian']); + } + public function testTransBegin(): void { $builder = $this->db->table('job'); From fe0f3e7d6895f56a7d52ae098fa4c94a38d28f01 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 12 Apr 2024 11:47:37 +0900 Subject: [PATCH 134/277] docs: add empty lines --- user_guide_src/source/database/transactions.rst | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/user_guide_src/source/database/transactions.rst b/user_guide_src/source/database/transactions.rst index 4fe44c8929e5..5d01d883a5d5 100644 --- a/user_guide_src/source/database/transactions.rst +++ b/user_guide_src/source/database/transactions.rst @@ -52,11 +52,13 @@ or failure of any given query. Strict Mode =========== -By default, CodeIgniter runs all transactions in Strict Mode. When strict -mode is enabled, if you are running multiple groups of transactions, if -one group fails all subsequent groups will be rolled back. If strict mode is -disabled, each group is treated independently, meaning a failure of one -group will not affect any others. +By default, CodeIgniter runs all transactions in Strict Mode. + +When strict mode is enabled, if you are running multiple groups of transactions, +if one group fails all subsequent groups will be rolled back. + +If strict mode is disabled, each group is treated independently, meaning a failure +of one group will not affect any others. Strict Mode can be disabled as follows: From df90b23c73a90bb7c9fc6f8217a71b836710824c Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 12 Apr 2024 11:47:51 +0900 Subject: [PATCH 135/277] docs: add "Resetting Transaction Status" --- user_guide_src/source/database/transactions.rst | 17 ++++++++++++++++- .../source/database/transactions/009.php | 3 +++ 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 user_guide_src/source/database/transactions/009.php diff --git a/user_guide_src/source/database/transactions.rst b/user_guide_src/source/database/transactions.rst index 5d01d883a5d5..5c48f2806240 100644 --- a/user_guide_src/source/database/transactions.rst +++ b/user_guide_src/source/database/transactions.rst @@ -14,7 +14,7 @@ transactions. .. contents:: :local: - :depth: 2 + :depth: 3 CodeIgniter's Approach to Transactions ====================================== @@ -64,6 +64,21 @@ Strict Mode can be disabled as follows: .. literalinclude:: transactions/002.php +.. _transactions-resetting-transaction-status: + +Resetting Transaction Status +---------------------------- + +.. versionadded:: 4.6.0 + +When strict mode is enabled, if one transaction fails, all subsequent transactions +will be rolled back. + +If you wan to restart transactions after a failure, you can reset the transaction +status: + +.. literalinclude:: transactions/009.php + .. _transactions-managing-errors: Managing Errors diff --git a/user_guide_src/source/database/transactions/009.php b/user_guide_src/source/database/transactions/009.php new file mode 100644 index 000000000000..6b92adf1793f --- /dev/null +++ b/user_guide_src/source/database/transactions/009.php @@ -0,0 +1,3 @@ +db->resetTransStatus(); From 723d9e70585ecb0bc62c27dcc45ba05ed22ac81b Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 26 Apr 2024 09:20:40 +0900 Subject: [PATCH 136/277] docs: add changelog --- user_guide_src/source/changelogs/v4.6.0.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index f76902fc3588..dfbf02a7ae0c 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -134,6 +134,8 @@ Others ------ - Added a new configuration ``foundRows`` for MySQLi to use ``MYSQLI_CLIENT_FOUND_ROWS``. +- Added the ``BaseConnection::resetTransStatus()`` method to reset the transaction + status. See :ref:`transactions-resetting-transaction-status` for details. Model ===== From c2006388ff81f45a8eb1d0ae2ce4004224be833f Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 24 Jun 2024 10:37:40 +0900 Subject: [PATCH 137/277] test: add test Filters filter argments --- tests/system/Filters/FiltersTest.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/system/Filters/FiltersTest.php b/tests/system/Filters/FiltersTest.php index 003e6f7c5cbf..f64f4b458c6e 100644 --- a/tests/system/Filters/FiltersTest.php +++ b/tests/system/Filters/FiltersTest.php @@ -282,6 +282,10 @@ public function testProcessMethodProcessesFiltersBefore(): void 'before' => ['admin/*'], 'after' => ['/users/*'], ], + 'bar: arg1, arg2' => [ + 'before' => ['admin/*'], + 'after' => ['/users/*'], + ], ], ]; $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config); @@ -289,7 +293,7 @@ public function testProcessMethodProcessesFiltersBefore(): void $uri = 'admin/foo/bar'; $expected = [ - 'before' => ['foo'], + 'before' => ['foo', 'bar:arg1,arg2'], 'after' => [], ]; $this->assertSame($expected, $filters->initialize($uri)->getFilters()); @@ -311,6 +315,10 @@ public function testProcessMethodProcessesFiltersAfter(): void 'before' => ['admin/*'], 'after' => ['/users/*'], ], + 'bar: arg1, arg2' => [ + 'before' => ['admin/*'], + 'after' => ['/users/*'], + ], ], ]; $filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config); @@ -319,9 +327,7 @@ public function testProcessMethodProcessesFiltersAfter(): void $uri = 'users/foo/bar'; $expected = [ 'before' => [], - 'after' => [ - 'foo', - ], + 'after' => ['bar:arg1,arg2', 'foo'], ]; $this->assertSame($expected, $filters->initialize($uri)->getFilters()); } From c6518fe9858859b7dd085cbb469a3bf8a0443f93 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 24 Jun 2024 10:39:02 +0900 Subject: [PATCH 138/277] feat: normailze Filters filter arguments For consistency. Route filter arguments are already normalized. --- system/Filters/Filters.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index 9cfd6bca793b..6241e932fb23 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -778,6 +778,10 @@ protected function processFilters(?string $uri = null) $filters = []; foreach ($this->config->filters as $alias => $settings) { + // Normalize the arguments. + [$alias, $arguments] = $this->getCleanName($alias); + $alias = ($arguments === []) ? $alias : $alias . ':' . implode(',', $arguments); + // Look for inclusion rules if (isset($settings['before'])) { $path = $settings['before']; From 0237a11d51f2925a550c9a387d90254768f7ae86 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 24 Jun 2024 10:51:49 +0900 Subject: [PATCH 139/277] refactor: rename variable names --- phpstan-baseline.php | 6 ------ system/Filters/Filters.php | 34 ++++++++++++++++++---------------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/phpstan-baseline.php b/phpstan-baseline.php index f30467c66aa1..4998ba4c7f2c 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -5257,12 +5257,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/Filters/Filters.php', ]; -$ignoreErrors[] = [ - // identifier: missingType.iterableValue - 'message' => '#^Method CodeIgniter\\\\Filters\\\\Filters\\:\\:enableFilters\\(\\) has parameter \\$names with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Filters/Filters.php', -]; $ignoreErrors[] = [ // identifier: missingType.iterableValue 'message' => '#^Method CodeIgniter\\\\Filters\\\\Filters\\:\\:getRequiredFilters\\(\\) return type has no value type specified in iterable type array\\.$#', diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index 6241e932fb23..62475b614759 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -574,14 +574,14 @@ public function addFilter(string $class, ?string $alias = null, string $position * after the filter name, followed by a comma-separated list of arguments that * are passed to the filter when executed. * - * @param string $name filter_name or filter_name:arguments like 'role:admin,manager' + * @param string $filter filter_name or filter_name:arguments like 'role:admin,manager' * or filter classname. * @phpstan-param 'before'|'after' $position */ - private function enableFilter(string $name, string $position = 'before'): void + private function enableFilter(string $filter, string $position = 'before'): void { // Normalize the arguments. - [$alias, $arguments] = $this->getCleanName($name); + [$alias, $arguments] = $this->getCleanName($filter); $filter = ($arguments === []) ? $alias : $alias . ':' . implode(',', $arguments); if (class_exists($alias)) { @@ -602,24 +602,26 @@ private function enableFilter(string $name, string $position = 'before'): void /** * Get clean name and arguments * - * @param string $name filter_name or filter_name:arguments like 'role:admin,manager' + * @param string $filter filter_name or filter_name:arguments like 'role:admin,manager' * * @return array{0: string, 1: list} [name, arguments] */ - private function getCleanName(string $name): array + private function getCleanName(string $filter): array { $arguments = []; - if (str_contains($name, ':')) { - [$name, $arguments] = explode(':', $name); + if (str_contains($filter, ':')) { + [$alias, $arguments] = explode(':', $filter); $arguments = explode(',', $arguments); array_walk($arguments, static function (&$item) { $item = trim($item); }); + + return [$alias, $arguments]; } - return [$name, $arguments]; + return [$filter, $arguments]; } /** @@ -629,13 +631,13 @@ private function getCleanName(string $name): array * after the filter name, followed by a comma-separated list of arguments that * are passed to the filter when executed. * - * @params array $names filter_name or filter_name:arguments like 'role:admin,manager' + * @param list $filters filter_name or filter_name:arguments like 'role:admin,manager' * * @return Filters */ - public function enableFilters(array $names, string $when = 'before') + public function enableFilters(array $filters, string $when = 'before') { - foreach ($names as $filter) { + foreach ($filters as $filter) { $this->enableFilter($filter, $when); } @@ -777,17 +779,17 @@ protected function processFilters(?string $uri = null) // Add any filters that apply to this URI $filters = []; - foreach ($this->config->filters as $alias => $settings) { + foreach ($this->config->filters as $filter => $settings) { // Normalize the arguments. - [$alias, $arguments] = $this->getCleanName($alias); - $alias = ($arguments === []) ? $alias : $alias . ':' . implode(',', $arguments); + [$alias, $arguments] = $this->getCleanName($filter); + $filter = ($arguments === []) ? $alias : $alias . ':' . implode(',', $arguments); // Look for inclusion rules if (isset($settings['before'])) { $path = $settings['before']; if ($this->pathApplies($uri, $path)) { - $filters['before'][] = $alias; + $filters['before'][] = $filter; } } @@ -795,7 +797,7 @@ protected function processFilters(?string $uri = null) $path = $settings['after']; if ($this->pathApplies($uri, $path)) { - $filters['after'][] = $alias; + $filters['after'][] = $filter; } } } From f3e550470f75b1058cf838970293ac83d5dc78c0 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 24 Jun 2024 11:12:41 +0900 Subject: [PATCH 140/277] refactor: shorten block of if --- system/Filters/Filters.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index 62475b614759..6ecb30717673 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -610,18 +610,18 @@ private function getCleanName(string $filter): array { $arguments = []; - if (str_contains($filter, ':')) { - [$alias, $arguments] = explode(':', $filter); + if (! str_contains($filter, ':')) { + return [$filter, $arguments]; + } - $arguments = explode(',', $arguments); - array_walk($arguments, static function (&$item) { - $item = trim($item); - }); + [$alias, $arguments] = explode(':', $filter); - return [$alias, $arguments]; - } + $arguments = explode(',', $arguments); + array_walk($arguments, static function (&$item) { + $item = trim($item); + }); - return [$filter, $arguments]; + return [$alias, $arguments]; } /** From e7f7ec582f0472851f67cc3c1ef08f5b78dcf239 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 30 Jun 2024 11:56:55 +0900 Subject: [PATCH 141/277] refactor: add `: void` --- system/Filters/Filters.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index 6ecb30717673..121d251df63a 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -617,7 +617,7 @@ private function getCleanName(string $filter): array [$alias, $arguments] = explode(':', $filter); $arguments = explode(',', $arguments); - array_walk($arguments, static function (&$item) { + array_walk($arguments, static function (&$item): void { $item = trim($item); }); From 652264ebc6147595b89112568b9f49e09a7e8298 Mon Sep 17 00:00:00 2001 From: Glenn <1964713+pyromanci@users.noreply.github.com> Date: Wed, 15 May 2024 09:46:19 -0400 Subject: [PATCH 142/277] feat: Adding BaseService::updateServicesCache refreshes and looks for new services class post Autoload. --- system/Config/BaseService.php | 31 +++++++++++++++++++ .../AfterAutoloadModule/Config/Services.php | 29 +++++++++++++++++ .../Test/AfterAutoloadModule/Test.php | 18 +++++++++++ tests/system/Config/ServicesTest.php | 21 +++++++++++++ user_guide_src/source/changelogs/v4.6.0.rst | 3 ++ user_guide_src/source/concepts/services.rst | 3 ++ 6 files changed, 105 insertions(+) create mode 100644 tests/_support/Test/AfterAutoloadModule/Config/Services.php create mode 100644 tests/_support/Test/AfterAutoloadModule/Test.php diff --git a/system/Config/BaseService.php b/system/Config/BaseService.php index 68885acd0e54..8254b0036dcc 100644 --- a/system/Config/BaseService.php +++ b/system/Config/BaseService.php @@ -419,4 +419,35 @@ protected static function buildServicesCache(): void static::$discovered = true; } } + + /** + * Update the services cache. + */ + public static function updateServicesCache(): void + { + if ((new Modules())->shouldDiscover('services')) { + $locator = static::locator(); + $files = $locator->search('Config/Services'); + + $systemPath = static::autoloader()->getNamespace('CodeIgniter')[0]; + + // Get instances of all service classes and cache them locally. + foreach ($files as $file) { + // Does not search `CodeIgniter` namespace to prevent from loading twice. + if (str_starts_with($file, $systemPath)) { + continue; + } + + $classname = $locator->findQualifiedNameFromPath($file); + + if ($classname === false) { + continue; + } + + if ($classname !== Services::class && ! in_array($classname, self::$serviceNames, true)) { + self::$serviceNames[] = $classname; + } + } + } + } } diff --git a/tests/_support/Test/AfterAutoloadModule/Config/Services.php b/tests/_support/Test/AfterAutoloadModule/Config/Services.php new file mode 100644 index 000000000000..c62b436488ad --- /dev/null +++ b/tests/_support/Test/AfterAutoloadModule/Config/Services.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace AfterAutoloadModule\Config; + +use AfterAutoloadModule\Test; +use CodeIgniter\Config\BaseService; + +class Services extends BaseService +{ + public static function test(bool $getShared = true): Test + { + if ($getShared) { + return static::getSharedInstance('test'); + } + + return new Test(); + } +} diff --git a/tests/_support/Test/AfterAutoloadModule/Test.php b/tests/_support/Test/AfterAutoloadModule/Test.php new file mode 100644 index 000000000000..8fab895b1545 --- /dev/null +++ b/tests/_support/Test/AfterAutoloadModule/Test.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace AfterAutoloadModule; + +class Test +{ +} diff --git a/tests/system/Config/ServicesTest.php b/tests/system/Config/ServicesTest.php index 008f2804c9f2..321017363c45 100644 --- a/tests/system/Config/ServicesTest.php +++ b/tests/system/Config/ServicesTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Config; +use AfterAutoloadModule\Test; use CodeIgniter\Autoloader\Autoloader; use CodeIgniter\Autoloader\FileLocator; use CodeIgniter\Database\MigrationRunner; @@ -350,6 +351,26 @@ public function testResetSingleCaseInsensitive(): void $this->assertNotInstanceOf(MockResponse::class, $someService); } + #[PreserveGlobalState(false)] + #[RunInSeparateProcess] + public function testUpdateServiceCache(): void + { + Services::injectMock('response', new MockResponse(new App())); + $someService = service('response'); + $this->assertInstanceOf(MockResponse::class, $someService); + service('response')->setStatusCode(200); + + Services::autoloader()->addNamespace('AfterAutoloadModule', TESTPATH . '_support/Test/AfterAutoloadModule/'); + Services::updateServicesCache(); + + $someService = service('response'); + $this->assertInstanceOf(MockResponse::class, $someService); + $this->assertSame(200, $someService->getStatusCode()); + + $someService = service('test'); + $this->assertInstanceOf(Test::class, $someService); + } + public function testFilters(): void { $result = Services::filters(); diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index fe3e80b9b4fc..9c6672349911 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -154,6 +154,9 @@ Helpers and Functions Others ====== +- **Services:** Added ``BaseService::updateServicesCache`` method to allow + updating the services cache file post autoloading. This will only look for + new services and add them to the class name cache. - **Filters:** Now you can execute a filter more than once with the different arguments in before or after. diff --git a/user_guide_src/source/concepts/services.rst b/user_guide_src/source/concepts/services.rst index 92eec136e1c8..8c478dc20c67 100644 --- a/user_guide_src/source/concepts/services.rst +++ b/user_guide_src/source/concepts/services.rst @@ -173,3 +173,6 @@ would simply use the framework's ``Config\Services`` class to grab your service: .. literalinclude:: services/012.php .. note:: If multiple Services files have the same method name, the first one found will be the instance returned. + +There may be times when you need to have Service Discovery refresh it's cache after the inital autoload proccess. This can be done by running :php:meth:`Config\\Services::updateServicesCache()`. +This will force the service discovery to re-scan the directories for any new services files. From f89a7001005d4ca8bcd3e648126bf9294af20298 Mon Sep 17 00:00:00 2001 From: Glenn <1964713+pyromanci@users.noreply.github.com> Date: Wed, 15 May 2024 12:18:27 -0400 Subject: [PATCH 143/277] chore: added comments to the test classes for BaseService::updateServicesCache() --- .../_support/Test/AfterAutoloadModule/Config/Services.php | 8 ++++++++ tests/_support/Test/AfterAutoloadModule/Test.php | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/tests/_support/Test/AfterAutoloadModule/Config/Services.php b/tests/_support/Test/AfterAutoloadModule/Config/Services.php index c62b436488ad..56a399a6962f 100644 --- a/tests/_support/Test/AfterAutoloadModule/Config/Services.php +++ b/tests/_support/Test/AfterAutoloadModule/Config/Services.php @@ -16,8 +16,16 @@ use AfterAutoloadModule\Test; use CodeIgniter\Config\BaseService; +/** + * Services for testing BaseService::updateServicesCache() + * + * This class should not be discovered by the autoloader until the test adds this namespace to the autoloader. + */ class Services extends BaseService { + /** + * Return a shared instance of the Test class for testing + */ public static function test(bool $getShared = true): Test { if ($getShared) { diff --git a/tests/_support/Test/AfterAutoloadModule/Test.php b/tests/_support/Test/AfterAutoloadModule/Test.php index 8fab895b1545..961be4895160 100644 --- a/tests/_support/Test/AfterAutoloadModule/Test.php +++ b/tests/_support/Test/AfterAutoloadModule/Test.php @@ -13,6 +13,11 @@ namespace AfterAutoloadModule; +/** + * A simple class for testing BaseService::updateServicesCache() + * + * This class should not be discovered by the autoloader until the test adds this namespace to the autoloader. + */ class Test { } From b077db595238f30fed9ee25fd05fbb7da82be077 Mon Sep 17 00:00:00 2001 From: Glenn <1964713+pyromanci@users.noreply.github.com> Date: Wed, 15 May 2024 12:24:30 -0400 Subject: [PATCH 144/277] fix: Added missing class to codeigniter.additionalServices for testing. --- phpstan.neon.dist | 3 +++ 1 file changed, 3 insertions(+) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index eccd0f88db75..3577b0c768c4 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -42,3 +42,6 @@ parameters: booleansInConditions: true disallowedConstructs: true matchingInheritedMethodNames: true + codeigniter: + additionalServices: + - AfterAutoloadModule\Config\Services From 0070a7b61108e958717ff2c87051c0c29d74e198 Mon Sep 17 00:00:00 2001 From: Glenn <1964713+pyromanci@users.noreply.github.com> Date: Wed, 15 May 2024 12:40:49 -0400 Subject: [PATCH 145/277] fix: Forgot to update the exlude list from the fork rest. --- tests/system/CommonSingleServiceTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/system/CommonSingleServiceTest.php b/tests/system/CommonSingleServiceTest.php index a8b4c6dabc32..71c92260a9b7 100644 --- a/tests/system/CommonSingleServiceTest.php +++ b/tests/system/CommonSingleServiceTest.php @@ -111,6 +111,7 @@ public static function provideServiceNames(): iterable 'reset', 'resetSingle', 'injectMock', + 'updateServicesCache', 'encrypter', // Encrypter needs a starter key 'session', // Headers already sent ]; From 97cb84a7a13a117cd2ee949efe4c0fadcaed28c0 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 30 Jun 2024 10:51:28 +0900 Subject: [PATCH 146/277] feat: replace updateServicesCache() with resetServicesCache() --- system/Config/BaseService.php | 40 ++++++------------------ tests/system/CommonSingleServiceTest.php | 2 +- tests/system/Config/ServicesTest.php | 4 +-- 3 files changed, 12 insertions(+), 34 deletions(-) diff --git a/system/Config/BaseService.php b/system/Config/BaseService.php index 8254b0036dcc..2fc079d3c964 100644 --- a/system/Config/BaseService.php +++ b/system/Config/BaseService.php @@ -388,6 +388,15 @@ public static function injectMock(string $name, $mock) static::$mocks[strtolower($name)] = $mock; } + /** + * Resets the service cache. + */ + public static function resetServicesCache(): void + { + self::$serviceNames = []; + static::$discovered = false; + } + protected static function buildServicesCache(): void { if (! static::$discovered) { @@ -419,35 +428,4 @@ protected static function buildServicesCache(): void static::$discovered = true; } } - - /** - * Update the services cache. - */ - public static function updateServicesCache(): void - { - if ((new Modules())->shouldDiscover('services')) { - $locator = static::locator(); - $files = $locator->search('Config/Services'); - - $systemPath = static::autoloader()->getNamespace('CodeIgniter')[0]; - - // Get instances of all service classes and cache them locally. - foreach ($files as $file) { - // Does not search `CodeIgniter` namespace to prevent from loading twice. - if (str_starts_with($file, $systemPath)) { - continue; - } - - $classname = $locator->findQualifiedNameFromPath($file); - - if ($classname === false) { - continue; - } - - if ($classname !== Services::class && ! in_array($classname, self::$serviceNames, true)) { - self::$serviceNames[] = $classname; - } - } - } - } } diff --git a/tests/system/CommonSingleServiceTest.php b/tests/system/CommonSingleServiceTest.php index 71c92260a9b7..343f17e22f5a 100644 --- a/tests/system/CommonSingleServiceTest.php +++ b/tests/system/CommonSingleServiceTest.php @@ -110,8 +110,8 @@ public static function provideServiceNames(): iterable 'serviceExists', 'reset', 'resetSingle', + 'resetServicesCache', 'injectMock', - 'updateServicesCache', 'encrypter', // Encrypter needs a starter key 'session', // Headers already sent ]; diff --git a/tests/system/Config/ServicesTest.php b/tests/system/Config/ServicesTest.php index 321017363c45..4b4574f5c29a 100644 --- a/tests/system/Config/ServicesTest.php +++ b/tests/system/Config/ServicesTest.php @@ -353,7 +353,7 @@ public function testResetSingleCaseInsensitive(): void #[PreserveGlobalState(false)] #[RunInSeparateProcess] - public function testUpdateServiceCache(): void + public function testResetServiceCache(): void { Services::injectMock('response', new MockResponse(new App())); $someService = service('response'); @@ -361,7 +361,7 @@ public function testUpdateServiceCache(): void service('response')->setStatusCode(200); Services::autoloader()->addNamespace('AfterAutoloadModule', TESTPATH . '_support/Test/AfterAutoloadModule/'); - Services::updateServicesCache(); + Services::resetServicesCache(); $someService = service('response'); $this->assertInstanceOf(MockResponse::class, $someService); From 8a2b115ce820d2999f0640a5b5d8366593ffcf87 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 30 Jun 2024 11:04:34 +0900 Subject: [PATCH 147/277] docs: update docs --- user_guide_src/source/changelogs/v4.6.0.rst | 5 ++--- user_guide_src/source/concepts/services.rst | 17 +++++++++++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index 9c6672349911..e675b871a9c3 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -154,12 +154,11 @@ Helpers and Functions Others ====== -- **Services:** Added ``BaseService::updateServicesCache`` method to allow - updating the services cache file post autoloading. This will only look for - new services and add them to the class name cache. - **Filters:** Now you can execute a filter more than once with the different arguments in before or after. +- **Services:** Added ``BaseService::resetServicesCache()`` method to reset + the services cache. See :ref:`resetting-services-cache`. *************** Message Changes diff --git a/user_guide_src/source/concepts/services.rst b/user_guide_src/source/concepts/services.rst index 8c478dc20c67..ec5a9832b77f 100644 --- a/user_guide_src/source/concepts/services.rst +++ b/user_guide_src/source/concepts/services.rst @@ -174,5 +174,18 @@ would simply use the framework's ``Config\Services`` class to grab your service: .. note:: If multiple Services files have the same method name, the first one found will be the instance returned. -There may be times when you need to have Service Discovery refresh it's cache after the inital autoload proccess. This can be done by running :php:meth:`Config\\Services::updateServicesCache()`. -This will force the service discovery to re-scan the directories for any new services files. +.. _resetting-services-cache: + +Resetting Services Cache +======================== + +.. versionadded:: 4.6.0 + +When Services is first called fairly early in the framework initialization process, +the Services classes discovered by auto-discovery is cached in a property. + +If modules are dynamically loaded later, and there are Services in the modules, +the cache must be updated. + +This can be done by running ``Config\Services::resetServicesCache()``. This will +clear the cache, and force the service discovery again when needed. From bae26f058c7cc8479d292ad44d29cea22fef3df6 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 30 Jun 2024 11:10:41 +0900 Subject: [PATCH 148/277] test: rename varible names --- tests/system/Config/ServicesTest.php | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/tests/system/Config/ServicesTest.php b/tests/system/Config/ServicesTest.php index 4b4574f5c29a..ed7078514317 100644 --- a/tests/system/Config/ServicesTest.php +++ b/tests/system/Config/ServicesTest.php @@ -356,19 +356,22 @@ public function testResetSingleCaseInsensitive(): void public function testResetServiceCache(): void { Services::injectMock('response', new MockResponse(new App())); - $someService = service('response'); - $this->assertInstanceOf(MockResponse::class, $someService); + $response = service('response'); + $this->assertInstanceOf(MockResponse::class, $response); service('response')->setStatusCode(200); - Services::autoloader()->addNamespace('AfterAutoloadModule', TESTPATH . '_support/Test/AfterAutoloadModule/'); + Services::autoloader()->addNamespace( + 'AfterAutoloadModule', + TESTPATH . '_support/Test/AfterAutoloadModule/' + ); Services::resetServicesCache(); - $someService = service('response'); - $this->assertInstanceOf(MockResponse::class, $someService); - $this->assertSame(200, $someService->getStatusCode()); + $response = service('response'); + $this->assertInstanceOf(MockResponse::class, $response); + $this->assertSame(200, $response->getStatusCode()); - $someService = service('test'); - $this->assertInstanceOf(Test::class, $someService); + $test = service('test'); + $this->assertInstanceOf(Test::class, $test); } public function testFilters(): void From 94e43fdbe33b10c15d0a10ebf4e01b77d7232126 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 30 Jun 2024 11:15:00 +0900 Subject: [PATCH 149/277] test: update comments --- tests/_support/Test/AfterAutoloadModule/Config/Services.php | 5 +++-- tests/_support/Test/AfterAutoloadModule/Test.php | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/_support/Test/AfterAutoloadModule/Config/Services.php b/tests/_support/Test/AfterAutoloadModule/Config/Services.php index 56a399a6962f..34dc48476628 100644 --- a/tests/_support/Test/AfterAutoloadModule/Config/Services.php +++ b/tests/_support/Test/AfterAutoloadModule/Config/Services.php @@ -17,9 +17,10 @@ use CodeIgniter\Config\BaseService; /** - * Services for testing BaseService::updateServicesCache() + * Services for testing BaseService::resetServicesCache(). * - * This class should not be discovered by the autoloader until the test adds this namespace to the autoloader. + * This class should not be discovered by the autoloader until the test adds + * this namespace to the autoloader. */ class Services extends BaseService { diff --git a/tests/_support/Test/AfterAutoloadModule/Test.php b/tests/_support/Test/AfterAutoloadModule/Test.php index 961be4895160..3664f1c7c85d 100644 --- a/tests/_support/Test/AfterAutoloadModule/Test.php +++ b/tests/_support/Test/AfterAutoloadModule/Test.php @@ -14,9 +14,10 @@ namespace AfterAutoloadModule; /** - * A simple class for testing BaseService::updateServicesCache() + * A simple class for testing BaseService::resetServicesCache(). * - * This class should not be discovered by the autoloader until the test adds this namespace to the autoloader. + * This class should not be discovered by the autoloader until the test adds + * this namespace to the autoloader. */ class Test { From 0edfd6f516492645574b74840aec7ef928dc33c4 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 30 Jun 2024 11:23:54 +0900 Subject: [PATCH 150/277] test: move class files for testing The module will be added dynamically. So it is special. --- .../Config/Services.php | 0 .../{Test/AfterAutoloadModule => _AfterAutoloadModule}/Test.php | 0 tests/system/Config/ServicesTest.php | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename tests/_support/{Test/AfterAutoloadModule => _AfterAutoloadModule}/Config/Services.php (100%) rename tests/_support/{Test/AfterAutoloadModule => _AfterAutoloadModule}/Test.php (100%) diff --git a/tests/_support/Test/AfterAutoloadModule/Config/Services.php b/tests/_support/_AfterAutoloadModule/Config/Services.php similarity index 100% rename from tests/_support/Test/AfterAutoloadModule/Config/Services.php rename to tests/_support/_AfterAutoloadModule/Config/Services.php diff --git a/tests/_support/Test/AfterAutoloadModule/Test.php b/tests/_support/_AfterAutoloadModule/Test.php similarity index 100% rename from tests/_support/Test/AfterAutoloadModule/Test.php rename to tests/_support/_AfterAutoloadModule/Test.php diff --git a/tests/system/Config/ServicesTest.php b/tests/system/Config/ServicesTest.php index ed7078514317..4beaed54bfc7 100644 --- a/tests/system/Config/ServicesTest.php +++ b/tests/system/Config/ServicesTest.php @@ -362,7 +362,7 @@ public function testResetServiceCache(): void Services::autoloader()->addNamespace( 'AfterAutoloadModule', - TESTPATH . '_support/Test/AfterAutoloadModule/' + SUPPORTPATH . '_AfterAutoloadModule/' ); Services::resetServicesCache(); From a4563e53b7cd6197df337af7aa2130129662d3fe Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 30 Jun 2024 11:52:05 +0900 Subject: [PATCH 151/277] docs: fix description --- user_guide_src/source/concepts/services.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/concepts/services.rst b/user_guide_src/source/concepts/services.rst index ec5a9832b77f..55809de99796 100644 --- a/user_guide_src/source/concepts/services.rst +++ b/user_guide_src/source/concepts/services.rst @@ -181,8 +181,9 @@ Resetting Services Cache .. versionadded:: 4.6.0 -When Services is first called fairly early in the framework initialization process, -the Services classes discovered by auto-discovery is cached in a property. +When the Services class is first called fairly early in the framework initialization +process, the Services classes discovered by auto-discovery are cached in a property, +and it will not be updated. If modules are dynamically loaded later, and there are Services in the modules, the cache must be updated. From 7b235d07c496df2c65c56dc4e0037dda7dbade2f Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 11 Jul 2024 10:29:21 +0900 Subject: [PATCH 152/277] feat: add page for 400 request --- app/Views/errors/html/error_400.php | 84 +++++++++++++++++++++++++++++ system/Language/en/Errors.php | 2 + 2 files changed, 86 insertions(+) create mode 100644 app/Views/errors/html/error_400.php diff --git a/app/Views/errors/html/error_400.php b/app/Views/errors/html/error_400.php new file mode 100644 index 000000000000..555da042be30 --- /dev/null +++ b/app/Views/errors/html/error_400.php @@ -0,0 +1,84 @@ + + + + + <?= lang('Errors.badRequest') ?> + + + + +
+

400

+ +

+ + + + + +

+
+ + diff --git a/system/Language/en/Errors.php b/system/Language/en/Errors.php index 29d6ad364687..76b08c824443 100644 --- a/system/Language/en/Errors.php +++ b/system/Language/en/Errors.php @@ -15,6 +15,8 @@ return [ 'pageNotFound' => '404 - Page Not Found', 'sorryCannotFind' => 'Sorry! Cannot seem to find the page you were looking for.', + 'badRequest' => '400 - Bad Request', + 'sorryBadRequest' => 'Sorry! Something wrong with your request.', 'whoops' => 'Whoops!', 'weHitASnag' => 'We seem to have hit a snag. Please try again later...', ]; From d758faf198b3e13cd2e958d7f32bb3937dbc3823 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 12 Jul 2024 17:32:45 +0900 Subject: [PATCH 153/277] docs: add user guide --- user_guide_src/source/changelogs/v4.6.0.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index e675b871a9c3..57a36d19cf4e 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -159,12 +159,14 @@ Others arguments in before or after. - **Services:** Added ``BaseService::resetServicesCache()`` method to reset the services cache. See :ref:`resetting-services-cache`. +- **Errors:** Added a default error page for "400 Bad Request". *************** Message Changes *************** - Added ``Validation.min_dims`` message +- Added ``Errors.badRequest`` and ``Errors.sorryBadRequest`` ******* Changes From 1e06ee5078d87fde5f71ddf6ae352be59cb22599 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 18 Jul 2024 05:25:58 +0900 Subject: [PATCH 154/277] docs: fix by proofreading Co-authored-by: John Paul E. Balandan, CPA --- system/Language/en/Errors.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Language/en/Errors.php b/system/Language/en/Errors.php index 76b08c824443..bd2fc963412b 100644 --- a/system/Language/en/Errors.php +++ b/system/Language/en/Errors.php @@ -16,7 +16,7 @@ 'pageNotFound' => '404 - Page Not Found', 'sorryCannotFind' => 'Sorry! Cannot seem to find the page you were looking for.', 'badRequest' => '400 - Bad Request', - 'sorryBadRequest' => 'Sorry! Something wrong with your request.', + 'sorryBadRequest' => 'Sorry! Something is wrong with your request.', 'whoops' => 'Whoops!', 'weHitASnag' => 'We seem to have hit a snag. Please try again later...', ]; From 16e7a6cf5868d7f94174854fd5402f8bfb84f1e3 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 26 Jul 2024 16:10:44 +0900 Subject: [PATCH 155/277] test: add @psalm-suppress Error: tests/system/CodeIgniterTest.php:138:33: UndefinedClass: Class, interface or enum named Tests\Support\Errors does not exist (see https://psalm.dev/019) --- tests/system/CodeIgniterTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/system/CodeIgniterTest.php b/tests/system/CodeIgniterTest.php index e65ba03c25c0..fca72bf10583 100644 --- a/tests/system/CodeIgniterTest.php +++ b/tests/system/CodeIgniterTest.php @@ -126,6 +126,9 @@ public function testRunClosureRoute(): void $this->assertStringContainsString('You want to see "about" page.', $output); } + /** + * @psalm-suppress UndefinedClass + */ public function testRun404Override(): void { $_SERVER['REQUEST_METHOD'] = 'GET'; From 191028fef566fa67f3848076b407ca2f80bbfb62 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 25 Jul 2024 13:33:47 +0900 Subject: [PATCH 156/277] fix: add check for duplicate registrar auto-discovery runs --- system/Config/BaseConfig.php | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/system/Config/BaseConfig.php b/system/Config/BaseConfig.php index 8f579ffc88f3..d2df549c171d 100644 --- a/system/Config/BaseConfig.php +++ b/system/Config/BaseConfig.php @@ -11,6 +11,7 @@ namespace CodeIgniter\Config; +use CodeIgniter\Exceptions\ConfigException; use CodeIgniter\Exceptions\RuntimeException; use Config\Encryption; use Config\Modules; @@ -45,12 +46,22 @@ class BaseConfig public static bool $override = true; /** - * Has module discovery happened yet? + * Has module discovery completed? * * @var bool */ protected static $didDiscovery = false; + /** + * Is module discovery running or not? + */ + protected static bool $discovering = false; + + /** + * The processing Registrar file for error message. + */ + protected static string $registrarFile = ''; + /** * The modules configuration. * @@ -230,10 +241,24 @@ protected function registerProperties() } if (! static::$didDiscovery) { + // Discovery must be completed before the first instantiation of any Config class. + if (static::$discovering) { + throw new ConfigException( + 'During Auto-Discovery of Registrars,' + . ' "' . static::class . '" executes Auto-Discovery again.' + . ' "' . clean_path(static::$registrarFile) . '" seems to have bad code.' + ); + } + + static::$discovering = true; + $locator = service('locator'); $registrarsFiles = $locator->search('Config/Registrar.php'); foreach ($registrarsFiles as $file) { + // Saves the file for error message. + static::$registrarFile = $file; + $className = $locator->findQualifiedNameFromPath($file); if ($className === false) { @@ -244,6 +269,7 @@ protected function registerProperties() } static::$didDiscovery = true; + static::$discovering = false; } $shortName = (new ReflectionClass($this))->getShortName(); From eb53f8623b4c88178acc40b668a14220cf885461 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 26 Jul 2024 15:52:06 +0900 Subject: [PATCH 157/277] docs: add docs --- user_guide_src/source/changelogs/v4.6.0.rst | 7 ++++ .../source/installation/upgrade_460.rst | 22 +++++++++++++ .../source/installation/upgrade_460/001.php | 33 +++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 user_guide_src/source/installation/upgrade_460/001.php diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index 57a36d19cf4e..8872615a3fc6 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -50,6 +50,13 @@ The ``Filters`` class has been changed to allow multiple runs of the same filter with different arguments in before or after. See :ref:`Upgrading Guide ` for details. +Registrars +---------- + +Added check to prevent Auto-Discovery of Registrars from running twice. If it is +executed twice, an exception will be thrown. See +:ref:`upgrade-460-registrars-with-dirty-hack`. + .. _v460-interface-changes: Interface Changes diff --git a/user_guide_src/source/installation/upgrade_460.rst b/user_guide_src/source/installation/upgrade_460.rst index 8546df1903f2..5eccc23abd05 100644 --- a/user_guide_src/source/installation/upgrade_460.rst +++ b/user_guide_src/source/installation/upgrade_460.rst @@ -29,6 +29,28 @@ See :ref:`ChangeLog ` for details. If you have code that catches these exceptions, change the exception classes. +.. _upgrade-460-registrars-with-dirty-hack: + +Registrars with Dirty Hack +========================== + +To prevent Auto-Discovery of :ref:`registrars` from running twice, when a registrar +class is loaded or instantiated, if it instantiates a Config class (which extends +``CodeIgniter\Config\BaseConfig``), ``ConfigException`` will be raised. + +This is because if Auto-Discovery of Registrars is performed twice, duplicate +values may be added to properties of Config classes. + +All registrar classes (**Config/Registrar.php** in all namespaces) must be modified +so that they do not instantiate any Config class when loaded or instantiated. + +If the packages/modules you are using provide such registrar classes, the registrar +classes in the packages/modules need to be fixed. + +The following is an example of code that will no longer work: + +.. literalinclude:: upgrade_460/001.php + Interface Changes ================= diff --git a/user_guide_src/source/installation/upgrade_460/001.php b/user_guide_src/source/installation/upgrade_460/001.php new file mode 100644 index 000000000000..6d55ef67d336 --- /dev/null +++ b/user_guide_src/source/installation/upgrade_460/001.php @@ -0,0 +1,33 @@ + [ + 'module_pager' => 'MyModule\Views\Pager', + ], + ]; + } + + public static function hack(): void + { + $config = config('Cache'); + + // Does something. + } +} + +Registrar::hack(); // Bad. When this class is loaded, Config\Cache will be instantiated. From f0fd56aae2c838d5431f4db94bfc54baedaba638 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 26 Jul 2024 22:28:25 +0900 Subject: [PATCH 158/277] test: remove incorrect comments --- tests/system/I18n/TimeTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/system/I18n/TimeTest.php b/tests/system/I18n/TimeTest.php index 34516948ef65..f00b3a055be9 100644 --- a/tests/system/I18n/TimeTest.php +++ b/tests/system/I18n/TimeTest.php @@ -61,7 +61,7 @@ public function testNewTimeNow(): void 'en_US', IntlDateFormatter::SHORT, IntlDateFormatter::SHORT, - 'America/Chicago', // Default for CodeIgniter + 'America/Chicago', IntlDateFormatter::GREGORIAN, 'yyyy-MM-dd HH:mm:ss' ); @@ -76,7 +76,7 @@ public function testTimeWithTimezone(): void 'en_US', IntlDateFormatter::SHORT, IntlDateFormatter::SHORT, - 'Europe/London', // Default for CodeIgniter + 'Europe/London', IntlDateFormatter::GREGORIAN, 'yyyy-MM-dd HH:mm:ss' ); @@ -92,7 +92,7 @@ public function testTimeWithTimezoneAndLocale(): void 'fr_FR', IntlDateFormatter::SHORT, IntlDateFormatter::SHORT, - 'Europe/London', // Default for CodeIgniter + 'Europe/London', IntlDateFormatter::GREGORIAN, 'yyyy-MM-dd HH:mm:ss' ); From 8d401343ea40cbe30f4b36eb83f90f123282bfa8 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 27 Jul 2024 11:28:23 +0900 Subject: [PATCH 159/277] test: fix typo in test method name --- tests/system/I18n/TimeTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/system/I18n/TimeTest.php b/tests/system/I18n/TimeTest.php index f00b3a055be9..53c58fa0ef72 100644 --- a/tests/system/I18n/TimeTest.php +++ b/tests/system/I18n/TimeTest.php @@ -875,7 +875,7 @@ public function testEqualWithString(): void $this->assertTrue($time1->equals('January 11, 2017 03:50:00', 'Europe/London')); } - public function testEqualWithStringAndNotimezone(): void + public function testEqualWithStringAndNoTimezone(): void { $time1 = Time::parse('January 10, 2017 21:50:00', 'America/Chicago'); From a37f845418d210a0074c54fc02a0a34f7f06163e Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 26 Jul 2024 22:38:39 +0900 Subject: [PATCH 160/277] test: add test for Time::createFromFormat() with microseconds --- tests/system/I18n/TimeTest.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/system/I18n/TimeTest.php b/tests/system/I18n/TimeTest.php index 53c58fa0ef72..fd53c93afd83 100644 --- a/tests/system/I18n/TimeTest.php +++ b/tests/system/I18n/TimeTest.php @@ -234,6 +234,13 @@ public function testCreateFromFormat(): void $this->assertCloseEnoughString(date('2017-01-15 H:i:s', $now->getTimestamp()), $time->toDateTimeString()); } + public function testCreateFromFormatWithMicroseconds(): void + { + $time = Time::createFromFormat('Y-m-d H:i:s.u', '2024-07-09 09:13:34.654321'); + + $this->assertSame('2024-07-09 09:13:34.654321', $time->format('Y-m-d H:i:s.u')); + } + public function testCreateFromFormatWithTimezoneString(): void { $time = Time::createFromFormat('F j, Y', 'January 15, 2017', 'Europe/London'); From a302f835a160cc64f0f4df8f961552de2f6c9d43 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 27 Jul 2024 11:30:39 +0900 Subject: [PATCH 161/277] test: add test for Time::equals() with microseconds --- tests/system/I18n/TimeTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/system/I18n/TimeTest.php b/tests/system/I18n/TimeTest.php index fd53c93afd83..ab5d62e7c41f 100644 --- a/tests/system/I18n/TimeTest.php +++ b/tests/system/I18n/TimeTest.php @@ -889,6 +889,14 @@ public function testEqualWithStringAndNoTimezone(): void $this->assertTrue($time1->equals('January 10, 2017 21:50:00')); } + public function testEqualWithDifferentMicroseconds(): void + { + $time1 = new Time('2024-01-01 12:00:00.654321'); + $time2 = new Time('2024-01-01 12:00:00'); + + $this->assertFalse($time1->equals($time2)); + } + public function testSameSuccess(): void { $time1 = Time::parse('January 10, 2017 21:50:00', 'America/Chicago'); From 5c3b74c545b2e86bbda600fe8477c48f9e122f03 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 27 Jul 2024 12:53:07 +0900 Subject: [PATCH 162/277] test: add tests for Time::isBefore() and isAfter() --- tests/system/I18n/TimeTest.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/system/I18n/TimeTest.php b/tests/system/I18n/TimeTest.php index ab5d62e7c41f..eeaa9e019e9c 100644 --- a/tests/system/I18n/TimeTest.php +++ b/tests/system/I18n/TimeTest.php @@ -936,6 +936,15 @@ public function testBefore(): void $this->assertFalse($time2->isBefore($time1)); } + public function testBeforeWithMicroseconds(): void + { + $time1 = new Time('2024-01-01 12:00:00.000000'); + $time2 = new Time('2024-01-01 12:00:00.654321'); + + $this->assertTrue($time1->isBefore($time2)); + $this->assertFalse($time2->isBefore($time1)); + } + public function testAfter(): void { $time1 = Time::parse('January 10, 2017 21:50:00', 'America/Chicago'); @@ -945,6 +954,15 @@ public function testAfter(): void $this->assertTrue($time2->isAfter($time1)); } + public function testAfterWithMicroseconds(): void + { + $time1 = new Time('2024-01-01 12:00:00.654321'); + $time2 = new Time('2024-01-01 12:00:00.000000'); + + $this->assertTrue($time1->isAfter($time2)); + $this->assertFalse($time2->isAfter($time1)); + } + public function testHumanizeYearsSingle(): void { Time::setTestNow('March 10, 2017', 'America/Chicago'); From d4cf9bc20f98667d6e24e4a051a8d98bf621b288 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 27 Jul 2024 13:12:14 +0900 Subject: [PATCH 163/277] test: add tests for Time::isBefore() and isAfter() with same time --- tests/system/I18n/TimeTest.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/system/I18n/TimeTest.php b/tests/system/I18n/TimeTest.php index eeaa9e019e9c..0e5bc733407c 100644 --- a/tests/system/I18n/TimeTest.php +++ b/tests/system/I18n/TimeTest.php @@ -936,6 +936,15 @@ public function testBefore(): void $this->assertFalse($time2->isBefore($time1)); } + public function testBeforeSameTime(): void + { + $time1 = new Time('2024-01-01 12:00:00.000000'); + $time2 = new Time('2024-01-01 12:00:00.000000'); + + $this->assertFalse($time1->isBefore($time2)); + $this->assertFalse($time2->isBefore($time1)); + } + public function testBeforeWithMicroseconds(): void { $time1 = new Time('2024-01-01 12:00:00.000000'); @@ -954,6 +963,15 @@ public function testAfter(): void $this->assertTrue($time2->isAfter($time1)); } + public function testAfterSameTime(): void + { + $time1 = new Time('2024-01-01 12:00:00.000000'); + $time2 = new Time('2024-01-01 12:00:00.000000'); + + $this->assertFalse($time1->isAfter($time2)); + $this->assertFalse($time2->isAfter($time1)); + } + public function testAfterWithMicroseconds(): void { $time1 = new Time('2024-01-01 12:00:00.654321'); From 6b32d5838d22e1266978d18ecf02e149e2cc7768 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 26 Jul 2024 22:39:48 +0900 Subject: [PATCH 164/277] fix: Time loses microseconds --- system/I18n/TimeTrait.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/system/I18n/TimeTrait.php b/system/I18n/TimeTrait.php index fefb2fc544a3..c73ca5bd7368 100644 --- a/system/I18n/TimeTrait.php +++ b/system/I18n/TimeTrait.php @@ -81,10 +81,10 @@ public function __construct(?string $time = null, $timezone = null, ?string $loc if ($time === '' && static::$testNow instanceof self) { if ($timezone !== null) { $testNow = static::$testNow->setTimezone($timezone); - $time = $testNow->format('Y-m-d H:i:s'); + $time = $testNow->format('Y-m-d H:i:s.u'); } else { $timezone = static::$testNow->getTimezone(); - $time = static::$testNow->format('Y-m-d H:i:s'); + $time = static::$testNow->format('Y-m-d H:i:s.u'); } } @@ -97,7 +97,7 @@ public function __construct(?string $time = null, $timezone = null, ?string $loc if ($time !== '' && static::hasRelativeKeywords($time)) { $instance = new DateTime('now', $this->timezone); $instance->modify($time); - $time = $instance->format('Y-m-d H:i:s'); + $time = $instance->format('Y-m-d H:i:s.u'); } parent::__construct($time, $this->timezone); @@ -253,7 +253,7 @@ public static function createFromFormat($format, $datetime, $timezone = null) throw I18nException::forInvalidFormat($format); } - return new self($date->format('Y-m-d H:i:s'), $timezone); + return new self($date->format('Y-m-d H:i:s.u'), $timezone); } /** @@ -283,7 +283,7 @@ public static function createFromTimestamp(int $timestamp, $timezone = null, ?st */ public static function createFromInstance(DateTimeInterface $dateTime, ?string $locale = null) { - $date = $dateTime->format('Y-m-d H:i:s'); + $date = $dateTime->format('Y-m-d H:i:s.u'); $timezone = $dateTime->getTimezone(); return new self($date, $timezone, $locale); @@ -348,7 +348,7 @@ public static function setTestNow($datetime = null, $timezone = null, ?string $l if (is_string($datetime)) { $datetime = new self($datetime, $timezone, $locale); } elseif ($datetime instanceof DateTimeInterface && ! $datetime instanceof self) { - $datetime = new self($datetime->format('Y-m-d H:i:s'), $timezone); + $datetime = new self($datetime->format('Y-m-d H:i:s.u'), $timezone); } static::$testNow = $datetime; @@ -941,9 +941,9 @@ public function equals($testTime, ?string $timezone = null): bool $ourTime = $this->toDateTime() ->setTimezone(new DateTimeZone('UTC')) - ->format('Y-m-d H:i:s'); + ->format('Y-m-d H:i:s.u'); - return $testTime->format('Y-m-d H:i:s') === $ourTime; + return $testTime->format('Y-m-d H:i:s.u') === $ourTime; } /** @@ -956,15 +956,15 @@ public function equals($testTime, ?string $timezone = null): bool public function sameAs($testTime, ?string $timezone = null): bool { if ($testTime instanceof DateTimeInterface) { - $testTime = $testTime->format('Y-m-d H:i:s'); + $testTime = $testTime->format('Y-m-d H:i:s.u O'); } elseif (is_string($testTime)) { $timezone = $timezone ?: $this->timezone; $timezone = $timezone instanceof DateTimeZone ? $timezone : new DateTimeZone($timezone); $testTime = new DateTime($testTime, $timezone); - $testTime = $testTime->format('Y-m-d H:i:s'); + $testTime = $testTime->format('Y-m-d H:i:s.u O'); } - $ourTime = $this->toDateTimeString(); + $ourTime = $this->format('Y-m-d H:i:s.u O'); return $testTime === $ourTime; } From b193a62e4c8858c7c22536b4eaa0d54c247d9e41 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 27 Jul 2024 11:14:48 +0900 Subject: [PATCH 165/277] fix: Time::toDateTime() loses microseconds --- system/I18n/TimeTrait.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/system/I18n/TimeTrait.php b/system/I18n/TimeTrait.php index c73ca5bd7368..07028d661b3d 100644 --- a/system/I18n/TimeTrait.php +++ b/system/I18n/TimeTrait.php @@ -314,10 +314,11 @@ public static function instance(DateTime $dateTime, ?string $locale = null) */ public function toDateTime() { - $dateTime = new DateTime('', $this->getTimezone()); - $dateTime->setTimestamp(parent::getTimestamp()); - - return $dateTime; + return DateTime::createFromFormat( + 'Y-m-d H:i:s.u', + $this->format('Y-m-d H:i:s.u'), + $this->getTimezone() + ); } // -------------------------------------------------------------------- From f3b15c03a61660123305742fd046657c0802af69 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 27 Jul 2024 12:54:04 +0900 Subject: [PATCH 166/277] fix: Time::isBefore() and isAfter() loses microseconds --- system/I18n/TimeTrait.php | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/system/I18n/TimeTrait.php b/system/I18n/TimeTrait.php index 07028d661b3d..3257ad06189f 100644 --- a/system/I18n/TimeTrait.php +++ b/system/I18n/TimeTrait.php @@ -980,10 +980,16 @@ public function sameAs($testTime, ?string $timezone = null): bool */ public function isBefore($testTime, ?string $timezone = null): bool { - $testTime = $this->getUTCObject($testTime, $timezone)->getTimestamp(); - $ourTime = $this->getTimestamp(); + $testTime = $this->getUTCObject($testTime, $timezone); + + $testTimestamp = $testTime->getTimestamp(); + $ourTimestamp = $this->getTimestamp(); + + if ($ourTimestamp === $testTimestamp) { + return $this->format('u') < $testTime->format('u'); + } - return $ourTime < $testTime; + return $ourTimestamp < $testTimestamp; } /** @@ -996,10 +1002,16 @@ public function isBefore($testTime, ?string $timezone = null): bool */ public function isAfter($testTime, ?string $timezone = null): bool { - $testTime = $this->getUTCObject($testTime, $timezone)->getTimestamp(); - $ourTime = $this->getTimestamp(); + $testTime = $this->getUTCObject($testTime, $timezone); + + $testTimestamp = $testTime->getTimestamp(); + $ourTimestamp = $this->getTimestamp(); + + if ($ourTimestamp === $testTimestamp) { + return $this->format('u') > $testTime->format('u'); + } - return $ourTime > $testTime; + return $ourTimestamp > $testTimestamp; } // -------------------------------------------------------------------- From 4932e44f098a6f90ad01f6f40c7b24bf6cce1bad Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 27 Jul 2024 07:21:36 +0900 Subject: [PATCH 167/277] docs: add docs --- user_guide_src/source/changelogs/v4.6.0.rst | 6 ++++ .../source/installation/upgrade_460.rst | 36 +++++++++++++++++++ .../source/installation/upgrade_460/002.php | 8 +++++ .../source/installation/upgrade_460/003.php | 8 +++++ .../source/installation/upgrade_460/004.php | 8 +++++ .../source/installation/upgrade_460/005.php | 9 +++++ .../source/installation/upgrade_460/006.php | 10 ++++++ .../source/installation/upgrade_460/007.php | 17 +++++++++ 8 files changed, 102 insertions(+) create mode 100644 user_guide_src/source/installation/upgrade_460/002.php create mode 100644 user_guide_src/source/installation/upgrade_460/003.php create mode 100644 user_guide_src/source/installation/upgrade_460/004.php create mode 100644 user_guide_src/source/installation/upgrade_460/005.php create mode 100644 user_guide_src/source/installation/upgrade_460/006.php create mode 100644 user_guide_src/source/installation/upgrade_460/007.php diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index 8872615a3fc6..e001f5c9d4c2 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -59,6 +59,12 @@ executed twice, an exception will be thrown. See .. _v460-interface-changes: +Time with Microseconds +---------------------- + +Fixed bugs that some methods in ``Time`` to lose microseconds have been fixed. +See :ref:`Upgrading Guide ` for details. + Interface Changes ================= diff --git a/user_guide_src/source/installation/upgrade_460.rst b/user_guide_src/source/installation/upgrade_460.rst index 5eccc23abd05..7078110b7068 100644 --- a/user_guide_src/source/installation/upgrade_460.rst +++ b/user_guide_src/source/installation/upgrade_460.rst @@ -29,6 +29,42 @@ See :ref:`ChangeLog ` for details. If you have code that catches these exceptions, change the exception classes. +.. _upgrade-460-time-keeps-microseconds: + +Time keeps Microseconds +======================= + +In previous versions, :doc:`Time <../libraries/time>` lost microseconds in some +cases. But the bugs have been fixed. + +The results of the ``Time`` comparison may differ due to these fixes: + +.. literalinclude:: upgrade_460/006.php + :lines: 2- + +In a such case, you need to remove the microseconds: + +.. literalinclude:: upgrade_460/007.php + :lines: 2- + +The following cases now keeps microseconds: + +.. literalinclude:: upgrade_460/002.php + :lines: 2- + +.. literalinclude:: upgrade_460/003.php + :lines: 2- + +Note that ``Time`` with the current time has been holding microseconds since before. + +.. literalinclude:: upgrade_460/004.php + :lines: 2- + +Also, methods that returns an ``int`` still lose the microseconds. + +.. literalinclude:: upgrade_460/005.php + :lines: 2- + .. _upgrade-460-registrars-with-dirty-hack: Registrars with Dirty Hack diff --git a/user_guide_src/source/installation/upgrade_460/002.php b/user_guide_src/source/installation/upgrade_460/002.php new file mode 100644 index 000000000000..91747d7b5464 --- /dev/null +++ b/user_guide_src/source/installation/upgrade_460/002.php @@ -0,0 +1,8 @@ +format('Y-m-d H:i:s.u'); +// Before: 2024-07-09 09:13:34.000000 +// After: 2024-07-09 09:13:34.654321 diff --git a/user_guide_src/source/installation/upgrade_460/003.php b/user_guide_src/source/installation/upgrade_460/003.php new file mode 100644 index 000000000000..65914ece9c7c --- /dev/null +++ b/user_guide_src/source/installation/upgrade_460/003.php @@ -0,0 +1,8 @@ +format('Y-m-d H:i:s.u'); +// Before: 2024-07-26 21:05:57.000000 +// After: 2024-07-26 21:05:57.857235 diff --git a/user_guide_src/source/installation/upgrade_460/004.php b/user_guide_src/source/installation/upgrade_460/004.php new file mode 100644 index 000000000000..dab6489fd68b --- /dev/null +++ b/user_guide_src/source/installation/upgrade_460/004.php @@ -0,0 +1,8 @@ +format('Y-m-d H:i:s.u'); +// Before: 2024-07-26 21:39:32.249072 +// After: 2024-07-26 21:39:32.249072 diff --git a/user_guide_src/source/installation/upgrade_460/005.php b/user_guide_src/source/installation/upgrade_460/005.php new file mode 100644 index 000000000000..a5e80c09b778 --- /dev/null +++ b/user_guide_src/source/installation/upgrade_460/005.php @@ -0,0 +1,9 @@ +getTimestamp(); // 1704110400 + +$time2 = new Time('2024-01-01 12:00:00.654321'); +echo $time2->getTimestamp(); // 1704110400 diff --git a/user_guide_src/source/installation/upgrade_460/006.php b/user_guide_src/source/installation/upgrade_460/006.php new file mode 100644 index 000000000000..316d802ada48 --- /dev/null +++ b/user_guide_src/source/installation/upgrade_460/006.php @@ -0,0 +1,10 @@ +equals($time2); +// Before: true +// After: false diff --git a/user_guide_src/source/installation/upgrade_460/007.php b/user_guide_src/source/installation/upgrade_460/007.php new file mode 100644 index 000000000000..22cadc8c0773 --- /dev/null +++ b/user_guide_src/source/installation/upgrade_460/007.php @@ -0,0 +1,17 @@ +format('Y-m-d H:i:s'), + $time1->getTimezone() +); + +$time1->equals($time2); +// Before: true +// After: true From 7b0d8c3ec1942f59a42f4c87f9804c2891427c01 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 29 Jul 2024 10:29:55 +0900 Subject: [PATCH 168/277] test: add test for DataConverter to convert datetime to db with microseconds --- .../system/DataConverter/DataConverterTest.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/system/DataConverter/DataConverterTest.php b/tests/system/DataConverter/DataConverterTest.php index 6592f4d801c6..f56a18d10e48 100644 --- a/tests/system/DataConverter/DataConverterTest.php +++ b/tests/system/DataConverter/DataConverterTest.php @@ -379,6 +379,23 @@ public function testDateTimeConvertDataToDB(): void $this->assertSame('2023-11-18 14:18:18', $data['date']); } + public function testDateTimeConvertDataToDBWithFormat(): void + { + $types = [ + 'id' => 'int', + 'date' => 'datetime[us]', + ]; + $converter = $this->createDataConverter($types, [], db_connect()); + + $phpData = [ + 'id' => '1', + 'date' => Time::parse('2009-02-15 00:00:01.123456'), + ]; + $data = $converter->toDataSource($phpData); + + $this->assertSame('2009-02-15 00:00:01.123456', $data['date']); + } + public function testTimestampConvertDataFromDB(): void { $types = [ From 3c69a6133586da2a27b66c3334bacf6bc0825e4e Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 29 Jul 2024 10:32:01 +0900 Subject: [PATCH 169/277] fix: DatetimeCast::set() loses microseconds --- system/DataCaster/Cast/DatetimeCast.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/system/DataCaster/Cast/DatetimeCast.php b/system/DataCaster/Cast/DatetimeCast.php index b6fa97f9ddda..22fbcc6171db 100644 --- a/system/DataCaster/Cast/DatetimeCast.php +++ b/system/DataCaster/Cast/DatetimeCast.php @@ -62,6 +62,19 @@ public static function set( self::invalidTypeValueError($value); } - return (string) $value; + if (! $helper instanceof BaseConnection) { + $message = 'The parameter $helper must be BaseConnection.'; + + throw new InvalidArgumentException($message); + } + + $format = match ($params[0] ?? '') { + '' => $helper->dateFormat['datetime'], + 'ms' => $helper->dateFormat['datetime-ms'], + 'us' => $helper->dateFormat['datetime-us'], + default => throw new InvalidArgumentException('Invalid parameter: ' . $params[0]), + }; + + return $value->format($format); } } From 94adfc55cfae99e152c7e4d943ca03820551cc66 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 29 Jul 2024 10:45:55 +0900 Subject: [PATCH 170/277] test: update test code --- tests/system/DataConverter/DataConverterTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/system/DataConverter/DataConverterTest.php b/tests/system/DataConverter/DataConverterTest.php index f56a18d10e48..53a6361bb0de 100644 --- a/tests/system/DataConverter/DataConverterTest.php +++ b/tests/system/DataConverter/DataConverterTest.php @@ -368,7 +368,7 @@ public function testDateTimeConvertDataToDB(): void 'id' => 'int', 'date' => 'datetime', ]; - $converter = $this->createDataConverter($types); + $converter = $this->createDataConverter($types, [], db_connect()); $phpData = [ 'id' => '1', @@ -620,7 +620,7 @@ public function testExtract(): void 'created_at' => 'datetime', 'updated_at' => 'datetime', ]; - $converter = $this->createDataConverter($types); + $converter = $this->createDataConverter($types, [], db_connect()); $phpData = [ 'id' => 1, @@ -652,7 +652,7 @@ public function testExtractWithExtractMethod(): void 'created_at' => 'datetime', 'updated_at' => 'datetime', ]; - $converter = $this->createDataConverter($types, [], null, 'toRawArray'); + $converter = $this->createDataConverter($types, [], db_connect(), 'toRawArray'); $phpData = [ 'id' => 1, From 9ad1748a4cd71b3d6373b4995e0ae3f060a0cd4e Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 29 Jul 2024 10:46:58 +0900 Subject: [PATCH 171/277] refactor: extract getDateTimeFormat() method --- system/DataCaster/Cast/DatetimeCast.php | 29 +++++++++++++++---------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/system/DataCaster/Cast/DatetimeCast.php b/system/DataCaster/Cast/DatetimeCast.php index 22fbcc6171db..25f1e5fad448 100644 --- a/system/DataCaster/Cast/DatetimeCast.php +++ b/system/DataCaster/Cast/DatetimeCast.php @@ -43,12 +43,7 @@ public static function get( /** * @see https://www.php.net/manual/en/datetimeimmutable.createfromformat.php#datetimeimmutable.createfromformat.parameters */ - $format = match ($params[0] ?? '') { - '' => $helper->dateFormat['datetime'], - 'ms' => $helper->dateFormat['datetime-ms'], - 'us' => $helper->dateFormat['datetime-us'], - default => throw new InvalidArgumentException('Invalid parameter: ' . $params[0]), - }; + $format = self::getDateTimeFormat($params, $helper); return Time::createFromFormat($format, $value); } @@ -68,13 +63,23 @@ public static function set( throw new InvalidArgumentException($message); } - $format = match ($params[0] ?? '') { - '' => $helper->dateFormat['datetime'], - 'ms' => $helper->dateFormat['datetime-ms'], - 'us' => $helper->dateFormat['datetime-us'], - default => throw new InvalidArgumentException('Invalid parameter: ' . $params[0]), - }; + $format = self::getDateTimeFormat($params, $helper); return $value->format($format); } + + /** + * Gets DateTime format from the DB connection. + * + * @param list $params Additional param + */ + protected static function getDateTimeFormat(array $params, BaseConnection $db): string + { + return match ($params[0] ?? '') { + '' => $db->dateFormat['datetime'], + 'ms' => $db->dateFormat['datetime-ms'], + 'us' => $db->dateFormat['datetime-us'], + default => throw new InvalidArgumentException('Invalid parameter: ' . $params[0]), + }; + } } From ed29b50c7a78ed4fe3c37c1a994e2bee6d554d14 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 29 Jul 2024 11:51:54 +0900 Subject: [PATCH 172/277] docs: add notes for datetime ms/us in model and query builder --- user_guide_src/source/models/model.rst | 12 ++++++++++++ user_guide_src/source/models/model/063.php | 13 +++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 user_guide_src/source/models/model/063.php diff --git a/user_guide_src/source/models/model.rst b/user_guide_src/source/models/model.rst index c77e68cb81b8..5d9bb6ebdb4e 100644 --- a/user_guide_src/source/models/model.rst +++ b/user_guide_src/source/models/model.rst @@ -395,6 +395,18 @@ The datetime format is set in the ``dateFormat`` array of the :ref:`database configuration ` in the **app/Config/Database.php** file. +.. note:: + When you set ``ms`` or ``us`` as a parameter, **Model** takes care of second's + fractional part of the Time. But **Query Builder** does not. So you still need + to use the ``format()`` method when you pass the Time to Query Builder's methods + like ``where()``: + + .. literalinclude:: model/063.php + :lines: 2- + +.. note:: Prior to v4.6.0, you cannot use ``ms`` or ``us`` as a parameter. + Because the second's fractional part of Time was lost due to bugs. + Custom Casting ============== diff --git a/user_guide_src/source/models/model/063.php b/user_guide_src/source/models/model/063.php new file mode 100644 index 000000000000..cc75d0648421 --- /dev/null +++ b/user_guide_src/source/models/model/063.php @@ -0,0 +1,13 @@ +where('my_dt_field', $now->format('Y-m-d H:i:s.u'))->findAll(); +// Generates: SELECT * FROM `my_table` WHERE `my_dt_field` = '2024-07-28 18:57:58.900326' + +// But the following code loses the microseconds. +$model->where('my_dt_field', $now)->findAll(); +// Generates: SELECT * FROM `my_table` WHERE `my_dt_field` = '2024-07-28 18:57:58' From dc5c001200a9e5edbe7516b5a551646a50b78e11 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 1 Apr 2024 13:34:03 +0900 Subject: [PATCH 173/277] refactor: rename variable names --- system/Database/BaseBuilder.php | 4 ++-- system/Database/BaseConnection.php | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index 26a6de0a6d21..6dcc327364a6 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -3038,10 +3038,10 @@ protected function trackAliases($table) $table = preg_replace('/\s+AS\s+/i', ' ', $table); // Grab the alias - $table = trim(strrchr($table, ' ')); + $alias = trim(strrchr($table, ' ')); // Store the alias, if it doesn't already exist - $this->db->addTableAlias($table); + $this->db->addTableAlias($alias); } } diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index e918f10aaea2..e6133002bc7d 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -340,7 +340,7 @@ abstract class BaseConnection implements ConnectionInterface /** * Array of table aliases. * - * @var array + * @var list */ protected $aliasedTables = []; @@ -576,10 +576,10 @@ public function setAliasedTables(array $aliases) * * @return $this */ - public function addTableAlias(string $table) + public function addTableAlias(string $alias) { - if (! in_array($table, $this->aliasedTables, true)) { - $this->aliasedTables[] = $table; + if (! in_array($alias, $this->aliasedTables, true)) { + $this->aliasedTables[] = $alias; } return $this; From 0572f9849b33af0603afb9c2602bb01076eedbaa Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 2 Apr 2024 09:06:16 +0900 Subject: [PATCH 174/277] feat: escapeIdentifier() handles empty string --- system/Database/BaseConnection.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index e6133002bc7d..e36a80b8535b 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -1224,6 +1224,10 @@ private function protectDotItem(string $item, string $alias, bool $protectIdenti */ public function escapeIdentifier(string $item): string { + if ($item === '') { + return ''; + } + return $this->escapeChar . str_replace( $this->escapeChar, From 583a080705025b277c5d80c5fc67ce5d1cfe9333 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 1 Apr 2024 13:34:33 +0900 Subject: [PATCH 175/277] feat: add TableName class --- system/Database/TableName.php | 133 ++++++++++++++++++++++++ tests/system/Database/TableNameTest.php | 107 +++++++++++++++++++ 2 files changed, 240 insertions(+) create mode 100644 system/Database/TableName.php create mode 100644 tests/system/Database/TableNameTest.php diff --git a/system/Database/TableName.php b/system/Database/TableName.php new file mode 100644 index 000000000000..4371501973f5 --- /dev/null +++ b/system/Database/TableName.php @@ -0,0 +1,133 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Database; + +/** + * Represents a table name in SQL. + * + * @see \CodeIgniter\Database\TableNameTest + */ +class TableName +{ + /** + * @param string $actualTable Actual table name + * @param string $logicalTable Logical table name (w/o DB prefix) + * @param string $schema Schema name + * @param string $database Database name + * @param string $alias Alias name + */ + protected function __construct( + private readonly string $actualTable, + private readonly string $logicalTable = '', + private readonly string $schema = '', + private readonly string $database = '', + private readonly string $alias = '' + ) { + } + + /** + * Creates a new instance. + * + * @param string $table Table name (w/o DB prefix) + * @param string $alias Alias name + */ + public static function create(BaseConnection $db, string $table, string $alias = ''): self + { + return new self( + $db->DBPrefix . $table, + $table, + '', + '', + $alias + ); + } + + /** + * Creates a new instance from an actual table name. + * + * @param string $actualTable Actual table name with DB prefix + * @param string $alias Alias name + */ + public static function fromActualName(BaseConnection $db, string $actualTable, string $alias = ''): self + { + $prefix = $db->DBPrefix; + $logicalTable = ''; + + if (str_starts_with($actualTable, $prefix)) { + $logicalTable = substr($actualTable, strlen($prefix)); + } + + return new self( + $actualTable, + $logicalTable, + '', + $alias + ); + } + + /** + * Creates a new instance from full name. + * + * @param string $table Table name (w/o DB prefix) + * @param string $schema Schema name + * @param string $database Database name + * @param string $alias Alias name + */ + public static function fromFullName( + BaseConnection $db, + string $table, + string $schema = '', + string $database = '', + string $alias = '' + ): self { + return new self( + $db->DBPrefix . $table, + $table, + $schema, + $database, + $alias + ); + } + + /** + * Returns the single segment table name w/o DB prefix. + */ + public function getTableName(): string + { + return $this->logicalTable; + } + + /** + * Returns the actual single segment table name w/z DB prefix. + */ + public function getActualTableName(): string + { + return $this->actualTable; + } + + public function getAlias(): string + { + return $this->alias; + } + + public function getSchema(): string + { + return $this->schema; + } + + public function getDatabase(): string + { + return $this->database; + } +} diff --git a/tests/system/Database/TableNameTest.php b/tests/system/Database/TableNameTest.php new file mode 100644 index 000000000000..fffc1528a713 --- /dev/null +++ b/tests/system/Database/TableNameTest.php @@ -0,0 +1,107 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Database; + +use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\Mock\MockConnection; + +/** + * @internal + * + * @group Others + */ +final class TableNameTest extends CIUnitTestCase +{ + protected function setUp(): void + { + parent::setUp(); + + $this->db = new MockConnection([ + 'database' => 'test', + 'DBPrefix' => 'db_', + 'schema' => 'dbo', + ]); + } + + public function testInstantiate(): void + { + $table = 'table'; + + $tableName = TableName::create($this->db, $table); + + $this->assertInstanceOf(TableName::class, $tableName); + } + + public function testCreateAndTableName(): void + { + $table = 'table'; + + $tableName = TableName::create($this->db, $table); + + $this->assertSame($table, $tableName->getTableName()); + $this->assertSame('db_table', $tableName->getActualTableName()); + } + + public function testFromActualNameAndTableNameWithPrefix(): void + { + $actualTable = 'db_table'; + + $tableName = TableName::fromActualName($this->db, $actualTable); + + $this->assertSame('table', $tableName->getTableName()); + $this->assertSame($actualTable, $tableName->getActualTableName()); + } + + public function testFromActualNameAndTableNameWithoutPrefix(): void + { + $actualTable = 'table'; + + $tableName = TableName::fromActualName($this->db, $actualTable); + + $this->assertSame('', $tableName->getTableName()); + $this->assertSame($actualTable, $tableName->getActualTableName()); + } + + public function testGetAlias(): void + { + $table = 'table'; + $alias = 't'; + + $tableName = TableName::create($this->db, $table, $alias); + + $this->assertSame($alias, $tableName->getAlias()); + } + + public function testGetSchema(): void + { + $table = 'table'; + $schema = 'dbo'; + $database = 'test'; + + $tableName = TableName::fromFullName($this->db, $table, $schema, $database); + + $this->assertSame($schema, $tableName->getSchema()); + } + + public function testGetDatabase(): void + { + $table = 'table'; + $schema = 'dbo'; + $database = 'test'; + + $tableName = TableName::fromFullName($this->db, $table, $schema, $database); + + $this->assertSame($database, $tableName->getDatabase()); + } +} From 0cb31bbb1e737f25e0697d2d808e5f5249fae5c5 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 1 Apr 2024 14:25:18 +0900 Subject: [PATCH 176/277] fix!: spark db:table causes errors w/z table name including speciali chars --- phpstan-baseline.php | 6 --- system/Commands/Database/ShowTableInfo.php | 5 ++- system/Database/BaseBuilder.php | 13 ++++-- system/Database/BaseConnection.php | 50 ++++++++++++++++----- system/Database/MySQLi/Connection.php | 14 +++++- system/Database/OCI8/Connection.php | 18 +++++--- system/Database/Postgre/Connection.php | 14 ++++-- system/Database/SQLSRV/Connection.php | 13 +++++- system/Database/SQLite3/Connection.php | 21 +++++++-- system/Test/Mock/MockConnection.php | 5 ++- tests/system/Database/Builder/AliasTest.php | 11 +++++ tests/system/Database/TableNameTest.php | 3 +- 12 files changed, 132 insertions(+), 41 deletions(-) diff --git a/phpstan-baseline.php b/phpstan-baseline.php index 41ccc064bed9..24c8799e64c2 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -2137,12 +2137,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/Database/BaseConnection.php', ]; -$ignoreErrors[] = [ - // identifier: missingType.iterableValue - 'message' => '#^Property CodeIgniter\\\\Database\\\\BaseConnection\\:\\:\\$aliasedTables type has no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Database/BaseConnection.php', -]; $ignoreErrors[] = [ // identifier: missingType.iterableValue 'message' => '#^Property CodeIgniter\\\\Database\\\\BaseConnection\\:\\:\\$dataCache type has no value type specified in iterable type array\\.$#', diff --git a/system/Commands/Database/ShowTableInfo.php b/system/Commands/Database/ShowTableInfo.php index a413d2e39ff8..954cf33aaa1f 100644 --- a/system/Commands/Database/ShowTableInfo.php +++ b/system/Commands/Database/ShowTableInfo.php @@ -16,6 +16,7 @@ use CodeIgniter\CLI\BaseCommand; use CodeIgniter\CLI\CLI; use CodeIgniter\Database\BaseConnection; +use CodeIgniter\Database\TableName; use CodeIgniter\Exceptions\InvalidArgumentException; use Config\Database; @@ -199,7 +200,7 @@ private function showDataOfTable(string $tableName, int $limitRows, int $limitFi CLI::newLine(); $this->removeDBPrefix(); - $thead = $this->db->getFieldNames($tableName); + $thead = $this->db->getFieldNames(TableName::fromActualName($this->db, $tableName)); $this->restoreDBPrefix(); // If there is a field named `id`, sort by it. @@ -277,7 +278,7 @@ private function makeTableRows( $this->tbody = []; $this->removeDBPrefix(); - $builder = $this->db->table($tableName); + $builder = $this->db->table(TableName::fromActualName($this->db, $tableName)); $builder->limit($limitRows); if ($sortField !== null) { $builder->orderBy($sortField, $this->sortDesc ? 'DESC' : 'ASC'); diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index 6dcc327364a6..00a6dcdf5807 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -298,7 +298,7 @@ class BaseBuilder /** * Constructor * - * @param array|string $tableName tablename or tablenames with or without aliases + * @param array|string|TableName $tableName tablename or tablenames with or without aliases * * Examples of $tableName: `mytable`, `jobs j`, `jobs j, users u`, `['jobs j','users u']` * @@ -315,15 +315,20 @@ public function __construct($tableName, ConnectionInterface $db, ?array $options */ $this->db = $db; + if ($tableName instanceof TableName) { + $this->tableName = $tableName->getTableName(); + $this->QBFrom[] = $this->db->escapeIdentifier($tableName); + $this->db->addTableAlias($tableName->getAlias()); + } // If it contains `,`, it has multiple tables - if (is_string($tableName) && ! str_contains($tableName, ',')) { + elseif (is_string($tableName) && ! str_contains($tableName, ',')) { $this->tableName = $tableName; // @TODO remove alias if exists + $this->from($tableName); } else { $this->tableName = ''; + $this->from($tableName); } - $this->from($tableName); - if ($options !== null && $options !== []) { foreach ($options as $key => $value) { if (property_exists($this, $key)) { diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index e36a80b8535b..79e7f9159f79 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -578,6 +578,10 @@ public function setAliasedTables(array $aliases) */ public function addTableAlias(string $alias) { + if ($alias === '') { + return $this; + } + if (! in_array($alias, $this->aliasedTables, true)) { $this->aliasedTables[] = $alias; } @@ -925,7 +929,7 @@ abstract protected function _transRollback(): bool; /** * Returns a non-shared new instance of the query builder for this connection. * - * @param array|string $tableName + * @param array|string|TableName $tableName * * @return BaseBuilder * @@ -1054,10 +1058,10 @@ public function getConnectDuration(int $decimals = 6): string * insert the table prefix (if it exists) in the proper position, and escape only * the correct identifiers. * - * @param array|int|string $item - * @param bool $prefixSingle Prefix a table name with no segments? - * @param bool $protectIdentifiers Protect table or column names? - * @param bool $fieldExists Supplied $item contains a column name? + * @param array|int|string|TableName $item + * @param bool $prefixSingle Prefix a table name with no segments? + * @param bool $protectIdentifiers Protect table or column names? + * @param bool $fieldExists Supplied $item contains a column name? * * @return array|string * @phpstan-return ($item is array ? array : string) @@ -1078,6 +1082,11 @@ public function protectIdentifiers($item, bool $prefixSingle = false, ?bool $pro return $escapedArray; } + if ($item instanceof TableName) { + /** @psalm-suppress NoValue I don't know why ERROR. */ + return $this->escapeTableName($item); + } + // If you pass `['column1', 'column2']`, `$item` will be int because the array keys are int. $item = (string) $item; @@ -1220,14 +1229,18 @@ private function protectDotItem(string $item, string $alias, bool $protectIdenti * * This function escapes single identifier. * - * @param non-empty-string $item + * @param non-empty-string|TableName $item */ - public function escapeIdentifier(string $item): string + public function escapeIdentifier($item): string { if ($item === '') { return ''; } + if ($item instanceof TableName) { + return $this->escapeTableName($item); + } + return $this->escapeChar . str_replace( $this->escapeChar, @@ -1237,6 +1250,17 @@ public function escapeIdentifier(string $item): string . $this->escapeChar; } + /** + * Returns escaped table name with alias. + */ + private function escapeTableName(TableName $tableName): string + { + $alias = $tableName->getAlias(); + + return $this->escapeIdentifier($tableName->getActualTableName()) + . (($alias !== '') ? ' ' . $this->escapeIdentifier($alias) : ''); + } + /** * Escape the SQL Identifiers * @@ -1550,12 +1574,16 @@ public function tableExists(string $tableName, bool $cached = true): bool /** * Fetch Field Names * + * @param string|TableName $tableName + * * @return false|list * * @throws DatabaseException */ - public function getFieldNames(string $table) + public function getFieldNames($tableName) { + $table = ($tableName instanceof TableName) ? $tableName->getTableName() : $tableName; + // Is there a cached result? if (isset($this->dataCache['field_names'][$table])) { return $this->dataCache['field_names'][$table]; @@ -1565,7 +1593,7 @@ public function getFieldNames(string $table) $this->initialize(); } - if (false === ($sql = $this->_listColumns($table))) { + if (false === ($sql = $this->_listColumns($tableName))) { if ($this->DBDebug) { throw new DatabaseException('This feature is not available for the database you are using.'); } @@ -1781,9 +1809,11 @@ abstract protected function _listTables(bool $constrainByPrefix = false, ?string /** * Generates a platform-specific query string so that the column names can be fetched. * + * @param string|TableName $table + * * @return false|string */ - abstract protected function _listColumns(string $table = ''); + abstract protected function _listColumns($table = ''); /** * Platform-specific field data information. diff --git a/system/Database/MySQLi/Connection.php b/system/Database/MySQLi/Connection.php index a0d02ccbfb4d..b20422d90aad 100644 --- a/system/Database/MySQLi/Connection.php +++ b/system/Database/MySQLi/Connection.php @@ -15,6 +15,7 @@ use CodeIgniter\Database\BaseConnection; use CodeIgniter\Database\Exceptions\DatabaseException; +use CodeIgniter\Database\TableName; use CodeIgniter\Exceptions\LogicException; use mysqli; use mysqli_result; @@ -422,10 +423,19 @@ protected function _listTables(bool $prefixLimit = false, ?string $tableName = n /** * Generates a platform-specific query string so that the column names can be fetched. + * + * @param string|TableName $table */ - protected function _listColumns(string $table = ''): string + protected function _listColumns($table = ''): string { - return 'SHOW COLUMNS FROM ' . $this->protectIdentifiers($table, true, null, false); + $tableName = $this->protectIdentifiers( + $table, + true, + null, + false + ); + + return 'SHOW COLUMNS FROM ' . $tableName; } /** diff --git a/system/Database/OCI8/Connection.php b/system/Database/OCI8/Connection.php index 2b870b755b44..e21519ea5259 100644 --- a/system/Database/OCI8/Connection.php +++ b/system/Database/OCI8/Connection.php @@ -16,6 +16,7 @@ use CodeIgniter\Database\BaseConnection; use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Database\Query; +use CodeIgniter\Database\TableName; use ErrorException; use stdClass; @@ -284,18 +285,25 @@ protected function _listTables(bool $prefixLimit = false, ?string $tableName = n /** * Generates a platform-specific query string so that the column names can be fetched. + * + * @param string|TableName $table */ - protected function _listColumns(string $table = ''): string + protected function _listColumns($table = ''): string { - if (str_contains($table, '.')) { - sscanf($table, '%[^.].%s', $owner, $table); + if ($table instanceof TableName) { + $tableName = $this->escape(strtoupper($table->getActualTableName())); + $owner = $this->username; + } elseif (str_contains($table, '.')) { + sscanf($table, '%[^.].%s', $owner, $tableName); + $tableName = $this->escape(strtoupper($this->DBPrefix . $tableName)); } else { - $owner = $this->username; + $owner = $this->username; + $tableName = $this->escape(strtoupper($this->DBPrefix . $table)); } return 'SELECT COLUMN_NAME FROM ALL_TAB_COLUMNS WHERE UPPER(OWNER) = ' . $this->escape(strtoupper($owner)) . ' - AND UPPER(TABLE_NAME) = ' . $this->escape(strtoupper($this->DBPrefix . $table)); + AND UPPER(TABLE_NAME) = ' . $tableName; } /** diff --git a/system/Database/Postgre/Connection.php b/system/Database/Postgre/Connection.php index ec886dbd4d8b..d687e30f7def 100644 --- a/system/Database/Postgre/Connection.php +++ b/system/Database/Postgre/Connection.php @@ -16,6 +16,7 @@ use CodeIgniter\Database\BaseConnection; use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Database\RawSql; +use CodeIgniter\Database\TableName; use ErrorException; use PgSql\Connection as PgSqlConnection; use PgSql\Result as PgSqlResult; @@ -300,13 +301,20 @@ protected function _listTables(bool $prefixLimit = false, ?string $tableName = n /** * Generates a platform-specific query string so that the column names can be fetched. + * + * @param string|TableName $table */ - protected function _listColumns(string $table = ''): string + protected function _listColumns($table = ''): string { + if ($table instanceof TableName) { + $tableName = $this->escape($table->getActualTableName()); + } else { + $tableName = $this->escape($this->DBPrefix . strtolower($table)); + } + return 'SELECT "column_name" FROM "information_schema"."columns" - WHERE LOWER("table_name") = ' - . $this->escape($this->DBPrefix . strtolower($table)) + WHERE LOWER("table_name") = ' . $tableName . ' ORDER BY "ordinal_position"'; } diff --git a/system/Database/SQLSRV/Connection.php b/system/Database/SQLSRV/Connection.php index 7e008e27c451..2a1abf620271 100644 --- a/system/Database/SQLSRV/Connection.php +++ b/system/Database/SQLSRV/Connection.php @@ -15,6 +15,7 @@ use CodeIgniter\Database\BaseConnection; use CodeIgniter\Database\Exceptions\DatabaseException; +use CodeIgniter\Database\TableName; use stdClass; /** @@ -225,12 +226,20 @@ protected function _listTables(bool $prefixLimit = false, ?string $tableName = n /** * Generates a platform-specific query string so that the column names can be fetched. + * + * @param string|TableName $table */ - protected function _listColumns(string $table = ''): string + protected function _listColumns($table = ''): string { + if ($table instanceof TableName) { + $tableName = $this->escape(strtolower($table->getActualTableName())); + } else { + $tableName = $this->escape($this->DBPrefix . strtolower($table)); + } + return 'SELECT [COLUMN_NAME] ' . ' FROM [INFORMATION_SCHEMA].[COLUMNS]' - . ' WHERE [TABLE_NAME] = ' . $this->escape($this->DBPrefix . $table) + . ' WHERE [TABLE_NAME] = ' . $tableName . ' AND [TABLE_SCHEMA] = ' . $this->escape($this->schema); } diff --git a/system/Database/SQLite3/Connection.php b/system/Database/SQLite3/Connection.php index 9945d41de11b..03eb3ab130f7 100644 --- a/system/Database/SQLite3/Connection.php +++ b/system/Database/SQLite3/Connection.php @@ -15,6 +15,7 @@ use CodeIgniter\Database\BaseConnection; use CodeIgniter\Database\Exceptions\DatabaseException; +use CodeIgniter\Database\TableName; use Exception; use SQLite3; use SQLite3Result; @@ -209,19 +210,31 @@ protected function _listTables(bool $prefixLimit = false, ?string $tableName = n /** * Generates a platform-specific query string so that the column names can be fetched. + * + * @param string|TableName $table */ - protected function _listColumns(string $table = ''): string + protected function _listColumns($table = ''): string { - return 'PRAGMA TABLE_INFO(' . $this->protectIdentifiers($table, true, null, false) . ')'; + if ($table instanceof TableName) { + $tableName = $this->escapeIdentifier($table); + } else { + $tableName = $this->protectIdentifiers($table, true, null, false); + } + + return 'PRAGMA TABLE_INFO(' . $tableName . ')'; } /** + * @param string|TableName $tableName + * * @return false|list * * @throws DatabaseException */ - public function getFieldNames(string $table) + public function getFieldNames($tableName) { + $table = ($tableName instanceof TableName) ? $tableName->getTableName() : $tableName; + // Is there a cached result? if (isset($this->dataCache['field_names'][$table])) { return $this->dataCache['field_names'][$table]; @@ -231,7 +244,7 @@ public function getFieldNames(string $table) $this->initialize(); } - $sql = $this->_listColumns($table); + $sql = $this->_listColumns($tableName); $query = $this->query($sql); $this->dataCache['field_names'][$table] = []; diff --git a/system/Test/Mock/MockConnection.php b/system/Test/Mock/MockConnection.php index ed814240bad3..7870d51b42cb 100644 --- a/system/Test/Mock/MockConnection.php +++ b/system/Test/Mock/MockConnection.php @@ -17,6 +17,7 @@ use CodeIgniter\Database\BaseConnection; use CodeIgniter\Database\BaseResult; use CodeIgniter\Database\Query; +use CodeIgniter\Database\TableName; /** * @extends BaseConnection @@ -202,8 +203,10 @@ protected function _listTables(bool $constrainByPrefix = false, ?string $tableNa /** * Generates a platform-specific query string so that the column names can be fetched. + * + * @param string|TableName $table */ - protected function _listColumns(string $table = ''): string + protected function _listColumns($table = ''): string { return ''; } diff --git a/tests/system/Database/Builder/AliasTest.php b/tests/system/Database/Builder/AliasTest.php index 9528ba6aabb2..491bc282c509 100644 --- a/tests/system/Database/Builder/AliasTest.php +++ b/tests/system/Database/Builder/AliasTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Database\Builder; +use CodeIgniter\Database\TableName; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Mock\MockConnection; use PHPUnit\Framework\Attributes\Group; @@ -41,6 +42,16 @@ public function testAlias(): void $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); } + public function testTableName(): void + { + $tableName = TableName::create($this->db, 'jobs', 'j'); + $builder = $this->db->table($tableName); + + $expectedSQL = 'SELECT * FROM "jobs" "j"'; + + $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); + } + public function testAliasSupportsArrayOfNames(): void { $builder = $this->db->table(['jobs j', 'users u']); diff --git a/tests/system/Database/TableNameTest.php b/tests/system/Database/TableNameTest.php index fffc1528a713..d01891a914da 100644 --- a/tests/system/Database/TableNameTest.php +++ b/tests/system/Database/TableNameTest.php @@ -18,9 +18,8 @@ /** * @internal - * - * @group Others */ +#[\PHPUnit\Framework\Attributes\Group('Others')] final class TableNameTest extends CIUnitTestCase { protected function setUp(): void From 62ef13f05878e1b992873bea947261ea8770b810 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 2 May 2024 09:56:49 +0900 Subject: [PATCH 177/277] docs: add changelog and upgrade --- user_guide_src/source/changelogs/v4.6.0.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index 8872615a3fc6..978e7421dc47 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -84,6 +84,22 @@ Method Signature Changes - **View:** The return type of the ``renderSection()`` method has been changed to ``string``, and now the method does not call ``echo``. +Removed Type Definitions +------------------------ + +- **Database:** + - The type ``string`` of the first parameter in + ``BaseConnection::escapeIdentifier()`` has been removed. + - The type ``string`` of the first parameter in + ``BaseConnection::getFieldNames()`` and ``SQLite3\Connection::getFieldNames()`` + have been removed. + - The type ``string`` of the first parameter in + ``BaseConnection::_listColumns()`` and ``MySQLi\Connection::_listColumns()`` + and ``OCI8\Connection::_listColumns()`` + and ``Postgre\Connection::_listColumns()`` + and ``SQLSRV\Connection::_listColumns()`` + and ``SQLite3\Connection::_listColumns()`` have been removed. + .. _v460-removed-deprecated-items: Removed Deprecated Items From 023bae43039767067904552f7aa440523f7a45b0 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 6 May 2024 19:24:38 +0900 Subject: [PATCH 178/277] test: replace @group with Group attribute --- tests/system/Database/TableNameTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/system/Database/TableNameTest.php b/tests/system/Database/TableNameTest.php index d01891a914da..6d0e1967d81a 100644 --- a/tests/system/Database/TableNameTest.php +++ b/tests/system/Database/TableNameTest.php @@ -15,11 +15,12 @@ use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Mock\MockConnection; +use PHPUnit\Framework\Attributes\Group; /** * @internal */ -#[\PHPUnit\Framework\Attributes\Group('Others')] +#[Group('Others')] final class TableNameTest extends CIUnitTestCase { protected function setUp(): void From 58197a4374425bcb8b3f1decbb48f596aa902883 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 31 Jul 2024 11:23:29 +0900 Subject: [PATCH 179/277] docs: add @internal to TableName class --- system/Database/TableName.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/system/Database/TableName.php b/system/Database/TableName.php index 4371501973f5..44f916809c52 100644 --- a/system/Database/TableName.php +++ b/system/Database/TableName.php @@ -16,6 +16,8 @@ /** * Represents a table name in SQL. * + * @interal + * * @see \CodeIgniter\Database\TableNameTest */ class TableName From 640de452ed07d024c7cd70ebeab9141e590c1827 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 5 Aug 2024 09:32:31 +0900 Subject: [PATCH 180/277] refactor: change 1st param type Remove dependency on BaseConnection. --- system/Commands/Database/ShowTableInfo.php | 4 ++-- system/Database/TableName.php | 12 ++++++------ tests/system/Database/Builder/AliasTest.php | 2 +- tests/system/Database/TableNameTest.php | 14 +++++++------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/system/Commands/Database/ShowTableInfo.php b/system/Commands/Database/ShowTableInfo.php index 954cf33aaa1f..844cbd4ab358 100644 --- a/system/Commands/Database/ShowTableInfo.php +++ b/system/Commands/Database/ShowTableInfo.php @@ -200,7 +200,7 @@ private function showDataOfTable(string $tableName, int $limitRows, int $limitFi CLI::newLine(); $this->removeDBPrefix(); - $thead = $this->db->getFieldNames(TableName::fromActualName($this->db, $tableName)); + $thead = $this->db->getFieldNames(TableName::fromActualName($this->db->DBPrefix, $tableName)); $this->restoreDBPrefix(); // If there is a field named `id`, sort by it. @@ -278,7 +278,7 @@ private function makeTableRows( $this->tbody = []; $this->removeDBPrefix(); - $builder = $this->db->table(TableName::fromActualName($this->db, $tableName)); + $builder = $this->db->table(TableName::fromActualName($this->db->DBPrefix, $tableName)); $builder->limit($limitRows); if ($sortField !== null) { $builder->orderBy($sortField, $this->sortDesc ? 'DESC' : 'ASC'); diff --git a/system/Database/TableName.php b/system/Database/TableName.php index 44f916809c52..fcceaa835e6f 100644 --- a/system/Database/TableName.php +++ b/system/Database/TableName.php @@ -44,10 +44,10 @@ protected function __construct( * @param string $table Table name (w/o DB prefix) * @param string $alias Alias name */ - public static function create(BaseConnection $db, string $table, string $alias = ''): self + public static function create(string $dbPrefix, string $table, string $alias = ''): self { return new self( - $db->DBPrefix . $table, + $dbPrefix . $table, $table, '', '', @@ -61,9 +61,9 @@ public static function create(BaseConnection $db, string $table, string $alias = * @param string $actualTable Actual table name with DB prefix * @param string $alias Alias name */ - public static function fromActualName(BaseConnection $db, string $actualTable, string $alias = ''): self + public static function fromActualName(string $dbPrefix, string $actualTable, string $alias = ''): self { - $prefix = $db->DBPrefix; + $prefix = $dbPrefix; $logicalTable = ''; if (str_starts_with($actualTable, $prefix)) { @@ -87,14 +87,14 @@ public static function fromActualName(BaseConnection $db, string $actualTable, s * @param string $alias Alias name */ public static function fromFullName( - BaseConnection $db, + string $dbPrefix, string $table, string $schema = '', string $database = '', string $alias = '' ): self { return new self( - $db->DBPrefix . $table, + $dbPrefix . $table, $table, $schema, $database, diff --git a/tests/system/Database/Builder/AliasTest.php b/tests/system/Database/Builder/AliasTest.php index 491bc282c509..a5e3dd0ad29e 100644 --- a/tests/system/Database/Builder/AliasTest.php +++ b/tests/system/Database/Builder/AliasTest.php @@ -44,7 +44,7 @@ public function testAlias(): void public function testTableName(): void { - $tableName = TableName::create($this->db, 'jobs', 'j'); + $tableName = TableName::create($this->db->DBPrefix, 'jobs', 'j'); $builder = $this->db->table($tableName); $expectedSQL = 'SELECT * FROM "jobs" "j"'; diff --git a/tests/system/Database/TableNameTest.php b/tests/system/Database/TableNameTest.php index 6d0e1967d81a..99f447879f5d 100644 --- a/tests/system/Database/TableNameTest.php +++ b/tests/system/Database/TableNameTest.php @@ -38,7 +38,7 @@ public function testInstantiate(): void { $table = 'table'; - $tableName = TableName::create($this->db, $table); + $tableName = TableName::create($this->db->DBPrefix, $table); $this->assertInstanceOf(TableName::class, $tableName); } @@ -47,7 +47,7 @@ public function testCreateAndTableName(): void { $table = 'table'; - $tableName = TableName::create($this->db, $table); + $tableName = TableName::create($this->db->DBPrefix, $table); $this->assertSame($table, $tableName->getTableName()); $this->assertSame('db_table', $tableName->getActualTableName()); @@ -57,7 +57,7 @@ public function testFromActualNameAndTableNameWithPrefix(): void { $actualTable = 'db_table'; - $tableName = TableName::fromActualName($this->db, $actualTable); + $tableName = TableName::fromActualName($this->db->DBPrefix, $actualTable); $this->assertSame('table', $tableName->getTableName()); $this->assertSame($actualTable, $tableName->getActualTableName()); @@ -67,7 +67,7 @@ public function testFromActualNameAndTableNameWithoutPrefix(): void { $actualTable = 'table'; - $tableName = TableName::fromActualName($this->db, $actualTable); + $tableName = TableName::fromActualName($this->db->DBPrefix, $actualTable); $this->assertSame('', $tableName->getTableName()); $this->assertSame($actualTable, $tableName->getActualTableName()); @@ -78,7 +78,7 @@ public function testGetAlias(): void $table = 'table'; $alias = 't'; - $tableName = TableName::create($this->db, $table, $alias); + $tableName = TableName::create($this->db->DBPrefix, $table, $alias); $this->assertSame($alias, $tableName->getAlias()); } @@ -89,7 +89,7 @@ public function testGetSchema(): void $schema = 'dbo'; $database = 'test'; - $tableName = TableName::fromFullName($this->db, $table, $schema, $database); + $tableName = TableName::fromFullName($this->db->DBPrefix, $table, $schema, $database); $this->assertSame($schema, $tableName->getSchema()); } @@ -100,7 +100,7 @@ public function testGetDatabase(): void $schema = 'dbo'; $database = 'test'; - $tableName = TableName::fromFullName($this->db, $table, $schema, $database); + $tableName = TableName::fromFullName($this->db->DBPrefix, $table, $schema, $database); $this->assertSame($database, $tableName->getDatabase()); } From 5f7c543e2aea1f282f1739a0bb51ce466bd202c6 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 7 Aug 2024 10:32:42 +0900 Subject: [PATCH 181/277] test: add test for Entity Casts datetime with timestamp --- tests/system/Entity/EntityTest.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/system/Entity/EntityTest.php b/tests/system/Entity/EntityTest.php index 8dd7ceac31fa..b0ec5ebbda92 100644 --- a/tests/system/Entity/EntityTest.php +++ b/tests/system/Entity/EntityTest.php @@ -460,6 +460,27 @@ public function testCastDateTime(): void $this->assertSame('2017-03-12', $entity->eighth->format('Y-m-d')); } + public function testCastDateTimeWithTimestampTimezone(): void + { + // Save the current timezone. + $tz = date_default_timezone_get(); + + // Change the timezone other than UTC. + date_default_timezone_set('Asia/Tokyo'); // +09:00 + + $entity = $this->getCastEntity(); + + $entity->eighth = 1722988800; // 2024-08-07 00:00:00 UTC + + $this->assertInstanceOf(DateTimeInterface::class, $entity->eighth); + // The timezone is the default timezone, not UTC. + $this->assertSame('2024-08-07 09:00:00', $entity->eighth->format('Y-m-d H:i:s')); + $this->assertSame('Asia/Tokyo', $entity->eighth->getTimezoneName()); + + // Restore timezone. + date_default_timezone_set($tz); + } + public function testCastTimestamp(): void { $entity = $this->getCastEntity(); From 1f081ec7cbd33cba13387b282315156e590fe49e Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 7 Aug 2024 11:00:20 +0900 Subject: [PATCH 182/277] test: add timezone for DataCaster TimestampCast --- .../DataConverter/DataConverterTest.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/system/DataConverter/DataConverterTest.php b/tests/system/DataConverter/DataConverterTest.php index 53a6361bb0de..3383bbfdc102 100644 --- a/tests/system/DataConverter/DataConverterTest.php +++ b/tests/system/DataConverter/DataConverterTest.php @@ -398,6 +398,12 @@ public function testDateTimeConvertDataToDBWithFormat(): void public function testTimestampConvertDataFromDB(): void { + // Save the current timezone. + $tz = date_default_timezone_get(); + + // Change the timezone other than UTC. + date_default_timezone_set('Asia/Tokyo'); // +09:00 + $types = [ 'id' => 'int', 'date' => 'timestamp', @@ -412,10 +418,20 @@ public function testTimestampConvertDataFromDB(): void $this->assertInstanceOf(Time::class, $data['date']); $this->assertSame(1_700_285_831, $data['date']->getTimestamp()); + $this->assertSame('Asia/Tokyo', $data['date']->getTimezoneName()); + + // Restore timezone. + date_default_timezone_set($tz); } public function testTimestampConvertDataToDB(): void { + // Save the current timezone. + $tz = date_default_timezone_get(); + + // Change the timezone other than UTC. + date_default_timezone_set('Asia/Tokyo'); // +09:00 + $types = [ 'id' => 'int', 'date' => 'timestamp', @@ -429,6 +445,9 @@ public function testTimestampConvertDataToDB(): void $data = $converter->toDataSource($phpData); $this->assertSame(1_700_285_831, $data['date']); + + // Restore timezone. + date_default_timezone_set($tz); } public function testURIConvertDataFromDB(): void From da76c864b3ec3aacc978742dbfb3746c1b17f774 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 6 Aug 2024 09:52:37 +0900 Subject: [PATCH 183/277] fix!: now Time::createFromTimestamp() returns Time with UTC by default --- system/I18n/TimeTrait.php | 2 +- tests/system/I18n/TimeTest.php | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/system/I18n/TimeTrait.php b/system/I18n/TimeTrait.php index 3257ad06189f..1ca8f5d9999a 100644 --- a/system/I18n/TimeTrait.php +++ b/system/I18n/TimeTrait.php @@ -269,7 +269,7 @@ public static function createFromTimestamp(int $timestamp, $timezone = null, ?st { $time = new self(gmdate('Y-m-d H:i:s', $timestamp), 'UTC', $locale); - $timezone ??= date_default_timezone_get(); + $timezone ??= 'UTC'; return $time->setTimezone($timezone); } diff --git a/tests/system/I18n/TimeTest.php b/tests/system/I18n/TimeTest.php index 0e5bc733407c..96482554cbb8 100644 --- a/tests/system/I18n/TimeTest.php +++ b/tests/system/I18n/TimeTest.php @@ -277,10 +277,11 @@ public function testCreateFromTimestamp(): void $timestamp = strtotime('2017-03-18 midnight'); + // The timezone will be UTC if you don't specify. $time = Time::createFromTimestamp($timestamp); - $this->assertSame('Asia/Tokyo', $time->getTimezone()->getName()); - $this->assertSame('2017-03-18 00:00:00', $time->format('Y-m-d H:i:s')); + $this->assertSame('UTC', $time->getTimezone()->getName()); + $this->assertSame('2017-03-17 15:00:00', $time->format('Y-m-d H:i:s')); // Restore timezone. date_default_timezone_set($tz); From cc6a54d094f79acab0ee28e98d286cc9c4270410 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 6 Aug 2024 09:53:27 +0900 Subject: [PATCH 184/277] docs: add docs --- user_guide_src/source/changelogs/v4.6.0.rst | 10 +++++++++- .../source/installation/upgrade_460.rst | 20 +++++++++++++++++++ user_guide_src/source/libraries/time.rst | 6 ++++-- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index 1cd78a3d51e2..d4cd6b3ae606 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -57,7 +57,13 @@ Added check to prevent Auto-Discovery of Registrars from running twice. If it is executed twice, an exception will be thrown. See :ref:`upgrade-460-registrars-with-dirty-hack`. -.. _v460-interface-changes: +Time::createFromTimestamp() +--------------------------- + +``Time::createFromTimestamp()`` handles timezones differently. If ``$timezone`` +is not explicitly passed then the instance has timezone set to UTC unlike earlier +where the currently set default timezone was used. +See :ref:`Upgrading Guide ` for details. Time with Microseconds ---------------------- @@ -65,6 +71,8 @@ Time with Microseconds Fixed bugs that some methods in ``Time`` to lose microseconds have been fixed. See :ref:`Upgrading Guide ` for details. +.. _v460-interface-changes: + Interface Changes ================= diff --git a/user_guide_src/source/installation/upgrade_460.rst b/user_guide_src/source/installation/upgrade_460.rst index 7078110b7068..5cbfd664bf92 100644 --- a/user_guide_src/source/installation/upgrade_460.rst +++ b/user_guide_src/source/installation/upgrade_460.rst @@ -29,6 +29,26 @@ See :ref:`ChangeLog ` for details. If you have code that catches these exceptions, change the exception classes. +.. _upgrade-460-time-create-from-timestamp: + +Time::createFromTimestamp() Timezone Change +=========================================== + +When you do not explicitly pass a timezone, now +:ref:`Time::createFromTimestamp() ` returns a Time +instance with **UTC**. In v4.4.6 to prior to v4.6.0, a Time instance with the +currently set default timezone was returned. + +This behavior change normalizes behavior with changes in PHP 8.4 which adds a +new ``DateTimeInterface::createFromTimestamp()`` method. + +If you want to keep the default timezone, you need to pass the timezone as the +second parameter:: + + use CodeIgniter\I18n\Time; + + $time = Time::createFromTimestamp(1501821586, date_default_timezone_get()); + .. _upgrade-460-time-keeps-microseconds: Time keeps Microseconds diff --git a/user_guide_src/source/libraries/time.rst b/user_guide_src/source/libraries/time.rst index b18e56350403..ccc008f5c533 100644 --- a/user_guide_src/source/libraries/time.rst +++ b/user_guide_src/source/libraries/time.rst @@ -126,8 +126,10 @@ This method takes a UNIX timestamp and, optionally, the timezone and locale, to .. literalinclude:: time/012.php -.. note:: Due to a bug, prior to v4.4.6, this method returned a Time instance - in timezone UTC when you do not specify a timezone. +If you do not explicitly pass a timezone, it returns a Time instance with **UTC**. + +.. note:: In v4.4.6 to prior to v4.6.0, this method returned a Time instance + with the default timezone when you do not specify a timezone. createFromInstance() ==================== From d9ad9bc5fc733f2ba5681a3073709ba78bdfcfb1 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 6 Aug 2024 10:36:53 +0900 Subject: [PATCH 185/277] docs: break long line --- user_guide_src/source/libraries/time.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/libraries/time.rst b/user_guide_src/source/libraries/time.rst index ccc008f5c533..1b844429aef6 100644 --- a/user_guide_src/source/libraries/time.rst +++ b/user_guide_src/source/libraries/time.rst @@ -122,7 +122,8 @@ and returns a ``Time`` instance, instead of DateTimeImmutable: createFromTimestamp() ===================== -This method takes a UNIX timestamp and, optionally, the timezone and locale, to create a new Time instance: +This method takes a UNIX timestamp and, optionally, the timezone and locale, to +create a new Time instance: .. literalinclude:: time/012.php From e1413fd908d77f3180890524c2053d391933106d Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 6 Aug 2024 10:39:16 +0900 Subject: [PATCH 186/277] docs: add note for timezone --- user_guide_src/source/libraries/time.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/user_guide_src/source/libraries/time.rst b/user_guide_src/source/libraries/time.rst index 1b844429aef6..9fd986d31421 100644 --- a/user_guide_src/source/libraries/time.rst +++ b/user_guide_src/source/libraries/time.rst @@ -129,6 +129,9 @@ create a new Time instance: If you do not explicitly pass a timezone, it returns a Time instance with **UTC**. +.. note:: We recommend to always call ``createFromTimestamp()`` with 2 parameters + (i.e. explicitly pass a timezone) unless using UTC as the default timezone. + .. note:: In v4.4.6 to prior to v4.6.0, this method returned a Time instance with the default timezone when you do not specify a timezone. From 75a92304941e9f359cdb5d192214866d5d79573d Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 6 Aug 2024 10:49:39 +0900 Subject: [PATCH 187/277] fix!: signature of createFromTimestamp() for PHP 8.4 --- system/I18n/TimeTrait.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/system/I18n/TimeTrait.php b/system/I18n/TimeTrait.php index 1ca8f5d9999a..b934a6b307bd 100644 --- a/system/I18n/TimeTrait.php +++ b/system/I18n/TimeTrait.php @@ -261,11 +261,9 @@ public static function createFromFormat($format, $datetime, $timezone = null) * * @param DateTimeZone|string|null $timezone * - * @return self - * * @throws Exception */ - public static function createFromTimestamp(int $timestamp, $timezone = null, ?string $locale = null) + public static function createFromTimestamp(float|int $timestamp, $timezone = null, ?string $locale = null): static { $time = new self(gmdate('Y-m-d H:i:s', $timestamp), 'UTC', $locale); From 1e90ad55c3043cda24deac7ff46c924f7eb08864 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 6 Aug 2024 11:21:58 +0900 Subject: [PATCH 188/277] fix: Time::createFromTimestamp() loses microseconds --- system/I18n/TimeTrait.php | 2 +- tests/system/I18n/TimeTest.php | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/system/I18n/TimeTrait.php b/system/I18n/TimeTrait.php index b934a6b307bd..f96b870610b4 100644 --- a/system/I18n/TimeTrait.php +++ b/system/I18n/TimeTrait.php @@ -265,7 +265,7 @@ public static function createFromFormat($format, $datetime, $timezone = null) */ public static function createFromTimestamp(float|int $timestamp, $timezone = null, ?string $locale = null): static { - $time = new self(gmdate('Y-m-d H:i:s', $timestamp), 'UTC', $locale); + $time = new self('@' . $timestamp, 'UTC', $locale); $timezone ??= 'UTC'; diff --git a/tests/system/I18n/TimeTest.php b/tests/system/I18n/TimeTest.php index 96482554cbb8..bf8512dbeaf5 100644 --- a/tests/system/I18n/TimeTest.php +++ b/tests/system/I18n/TimeTest.php @@ -287,6 +287,17 @@ public function testCreateFromTimestamp(): void date_default_timezone_set($tz); } + public function testCreateFromTimestampWithMicrotime(): void + { + $timestamp = 1489762800.654321; + + // The timezone will be UTC if you don't specify. + $time = Time::createFromTimestamp($timestamp); + + // float cannot handle the number precisely. + $this->assertSame('2017-03-17 15:00:00.654300', $time->format('Y-m-d H:i:s.u')); + } + public function testCreateFromTimestampWithTimezone(): void { // Set the timezone temporarily to UTC to make sure the test timestamp is correct From 613aba2f6b83cc9d488174a8c6f3196bc4a286d3 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 6 Aug 2024 11:28:19 +0900 Subject: [PATCH 189/277] docs: add changelog --- user_guide_src/source/changelogs/v4.6.0.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index d4cd6b3ae606..51c43456ae28 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -97,6 +97,9 @@ Method Signature Changes changed. The ``RouteCollection`` typehint has been changed to ``RouteCollectionInterface``. - **View:** The return type of the ``renderSection()`` method has been changed to ``string``, and now the method does not call ``echo``. +- **Time:** The first parameter type of the ``createFromTimestamp()`` has been + changed from ``int`` to ``int|float``, and the return type ``static``` has been + added. Removed Type Definitions ------------------------ From 8992ac024c405f5ecd82b952c26d03fc4e294766 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 6 Aug 2024 13:14:48 +0900 Subject: [PATCH 190/277] refactor: replace self with static --- phpstan-baseline.php | 24 ------------ system/I18n/Time.php | 2 + system/I18n/TimeLegacy.php | 2 + system/I18n/TimeTrait.php | 80 +++++++++++++++++++------------------- 4 files changed, 44 insertions(+), 64 deletions(-) diff --git a/phpstan-baseline.php b/phpstan-baseline.php index 0f022eca7ac8..7a78a6d3a104 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -7483,18 +7483,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/I18n/Time.php', ]; -$ignoreErrors[] = [ - // identifier: method.childReturnType - 'message' => '#^Return type \\(CodeIgniter\\\\I18n\\\\Time\\) of method CodeIgniter\\\\I18n\\\\Time\\:\\:setTimestamp\\(\\) should be covariant with return type \\(static\\(DateTimeImmutable\\)\\) of method DateTimeImmutable\\:\\:setTimestamp\\(\\)$#', - 'count' => 1, - 'path' => __DIR__ . '/system/I18n/Time.php', -]; -$ignoreErrors[] = [ - // identifier: method.childReturnType - 'message' => '#^Return type \\(CodeIgniter\\\\I18n\\\\Time\\) of method CodeIgniter\\\\I18n\\\\Time\\:\\:setTimezone\\(\\) should be covariant with return type \\(static\\(DateTimeImmutable\\)\\) of method DateTimeImmutable\\:\\:setTimezone\\(\\)$#', - 'count' => 1, - 'path' => __DIR__ . '/system/I18n/Time.php', -]; $ignoreErrors[] = [ // identifier: ternary.shortNotAllowed 'message' => '#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#', @@ -7507,18 +7495,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/I18n/TimeLegacy.php', ]; -$ignoreErrors[] = [ - // identifier: method.childReturnType - 'message' => '#^Return type \\(CodeIgniter\\\\I18n\\\\TimeLegacy\\) of method CodeIgniter\\\\I18n\\\\TimeLegacy\\:\\:setTimestamp\\(\\) should be covariant with return type \\(static\\(DateTime\\)\\) of method DateTime\\:\\:setTimestamp\\(\\)$#', - 'count' => 1, - 'path' => __DIR__ . '/system/I18n/TimeLegacy.php', -]; -$ignoreErrors[] = [ - // identifier: method.childReturnType - 'message' => '#^Return type \\(CodeIgniter\\\\I18n\\\\TimeLegacy\\) of method CodeIgniter\\\\I18n\\\\TimeLegacy\\:\\:setTimezone\\(\\) should be covariant with return type \\(static\\(DateTime\\)\\) of method DateTime\\:\\:setTimezone\\(\\)$#', - 'count' => 1, - 'path' => __DIR__ . '/system/I18n/TimeLegacy.php', -]; $ignoreErrors[] = [ // identifier: ternary.shortNotAllowed 'message' => '#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#', diff --git a/system/I18n/Time.php b/system/I18n/Time.php index 906479470b17..255f879a90ec 100644 --- a/system/I18n/Time.php +++ b/system/I18n/Time.php @@ -39,6 +39,8 @@ * @property-read string $weekOfYear * @property-read string $year * + * @phpstan-consistent-constructor + * * @see \CodeIgniter\I18n\TimeTest */ class Time extends DateTimeImmutable implements Stringable diff --git a/system/I18n/TimeLegacy.php b/system/I18n/TimeLegacy.php index 403fced2108d..b62877ec40d6 100644 --- a/system/I18n/TimeLegacy.php +++ b/system/I18n/TimeLegacy.php @@ -39,6 +39,8 @@ * @property string $weekOfYear read-only * @property string $year read-only * + * @phpstan-consistent-constructor + * * @deprecated Use Time instead. * @see \CodeIgniter\I18n\TimeLegacyTest */ diff --git a/system/I18n/TimeTrait.php b/system/I18n/TimeTrait.php index f96b870610b4..1f4f630750b2 100644 --- a/system/I18n/TimeTrait.php +++ b/system/I18n/TimeTrait.php @@ -78,7 +78,7 @@ public function __construct(?string $time = null, $timezone = null, ?string $loc $time ??= ''; // If a test instance has been provided, use it instead. - if ($time === '' && static::$testNow instanceof self) { + if ($time === '' && static::$testNow instanceof static) { if ($timezone !== null) { $testNow = static::$testNow->setTimezone($timezone); $time = $testNow->format('Y-m-d H:i:s.u'); @@ -108,13 +108,13 @@ public function __construct(?string $time = null, $timezone = null, ?string $loc * * @param DateTimeZone|string|null $timezone * - * @return self + * @return static * * @throws Exception */ public static function now($timezone = null, ?string $locale = null) { - return new self(null, $timezone, $locale); + return new static(null, $timezone, $locale); } /** @@ -125,13 +125,13 @@ public static function now($timezone = null, ?string $locale = null) * * @param DateTimeZone|string|null $timezone * - * @return self + * @return static * * @throws Exception */ public static function parse(string $datetime, $timezone = null, ?string $locale = null) { - return new self($datetime, $timezone, $locale); + return new static($datetime, $timezone, $locale); } /** @@ -139,13 +139,13 @@ public static function parse(string $datetime, $timezone = null, ?string $locale * * @param DateTimeZone|string|null $timezone * - * @return self + * @return static * * @throws Exception */ public static function today($timezone = null, ?string $locale = null) { - return new self(date('Y-m-d 00:00:00'), $timezone, $locale); + return new static(date('Y-m-d 00:00:00'), $timezone, $locale); } /** @@ -153,13 +153,13 @@ public static function today($timezone = null, ?string $locale = null) * * @param DateTimeZone|string|null $timezone * - * @return self + * @return static * * @throws Exception */ public static function yesterday($timezone = null, ?string $locale = null) { - return new self(date('Y-m-d 00:00:00', strtotime('-1 day')), $timezone, $locale); + return new static(date('Y-m-d 00:00:00', strtotime('-1 day')), $timezone, $locale); } /** @@ -167,13 +167,13 @@ public static function yesterday($timezone = null, ?string $locale = null) * * @param DateTimeZone|string|null $timezone * - * @return self + * @return static * * @throws Exception */ public static function tomorrow($timezone = null, ?string $locale = null) { - return new self(date('Y-m-d 00:00:00', strtotime('+1 day')), $timezone, $locale); + return new static(date('Y-m-d 00:00:00', strtotime('+1 day')), $timezone, $locale); } /** @@ -182,7 +182,7 @@ public static function tomorrow($timezone = null, ?string $locale = null) * * @param DateTimeZone|string|null $timezone * - * @return self + * @return static * * @throws Exception */ @@ -196,7 +196,7 @@ public static function createFromDate(?int $year = null, ?int $month = null, ?in * * @param DateTimeZone|string|null $timezone * - * @return self + * @return static * * @throws Exception */ @@ -210,7 +210,7 @@ public static function createFromTime(?int $hour = null, ?int $minutes = null, ? * * @param DateTimeZone|string|null $timezone * - * @return self + * @return static * * @throws Exception */ @@ -231,7 +231,7 @@ public static function create( $minutes ??= 0; $seconds ??= 0; - return new self(date('Y-m-d H:i:s', strtotime("{$year}-{$month}-{$day} {$hour}:{$minutes}:{$seconds}")), $timezone, $locale); + return new static(date('Y-m-d H:i:s', strtotime("{$year}-{$month}-{$day} {$hour}:{$minutes}:{$seconds}")), $timezone, $locale); } /** @@ -242,7 +242,7 @@ public static function create( * @param string $datetime * @param DateTimeZone|string|null $timezone * - * @return self + * @return static * * @throws Exception */ @@ -253,7 +253,7 @@ public static function createFromFormat($format, $datetime, $timezone = null) throw I18nException::forInvalidFormat($format); } - return new self($date->format('Y-m-d H:i:s.u'), $timezone); + return new static($date->format('Y-m-d H:i:s.u'), $timezone); } /** @@ -265,7 +265,7 @@ public static function createFromFormat($format, $datetime, $timezone = null) */ public static function createFromTimestamp(float|int $timestamp, $timezone = null, ?string $locale = null): static { - $time = new self('@' . $timestamp, 'UTC', $locale); + $time = new static('@' . $timestamp, 'UTC', $locale); $timezone ??= 'UTC'; @@ -275,7 +275,7 @@ public static function createFromTimestamp(float|int $timestamp, $timezone = nul /** * Takes an instance of DateTimeInterface and returns an instance of Time with it's same values. * - * @return self + * @return static * * @throws Exception */ @@ -284,13 +284,13 @@ public static function createFromInstance(DateTimeInterface $dateTime, ?string $ $date = $dateTime->format('Y-m-d H:i:s.u'); $timezone = $dateTime->getTimezone(); - return new self($date, $timezone, $locale); + return new static($date, $timezone, $locale); } /** * Takes an instance of DateTime and returns an instance of Time with it's same values. * - * @return self + * @return static * * @throws Exception * @@ -300,7 +300,7 @@ public static function createFromInstance(DateTimeInterface $dateTime, ?string $ */ public static function instance(DateTime $dateTime, ?string $locale = null) { - return self::createFromInstance($dateTime, $locale); + return static::createFromInstance($dateTime, $locale); } /** @@ -345,9 +345,9 @@ public static function setTestNow($datetime = null, $timezone = null, ?string $l // Convert to a Time instance if (is_string($datetime)) { - $datetime = new self($datetime, $timezone, $locale); - } elseif ($datetime instanceof DateTimeInterface && ! $datetime instanceof self) { - $datetime = new self($datetime->format('Y-m-d H:i:s.u'), $timezone); + $datetime = new static($datetime, $timezone, $locale); + } elseif ($datetime instanceof DateTimeInterface && ! $datetime instanceof static) { + $datetime = new static($datetime->format('Y-m-d H:i:s.u'), $timezone); } static::$testNow = $datetime; @@ -475,7 +475,7 @@ public function getWeekOfYear(): string public function getAge() { // future dates have no age - return max(0, $this->difference(self::now())->getYears()); + return max(0, $this->difference(static::now())->getYears()); } /** @@ -532,7 +532,7 @@ public function getTimezoneName(): string * * @param int|string $value * - * @return self + * @return static * * @throws Exception */ @@ -546,7 +546,7 @@ public function setYear($value) * * @param int|string $value * - * @return self + * @return static * * @throws Exception */ @@ -568,7 +568,7 @@ public function setMonth($value) * * @param int|string $value * - * @return self + * @return static * * @throws Exception */ @@ -592,7 +592,7 @@ public function setDay($value) * * @param int|string $value * - * @return self + * @return static * * @throws Exception */ @@ -610,7 +610,7 @@ public function setHour($value) * * @param int|string $value * - * @return self + * @return static * * @throws Exception */ @@ -628,7 +628,7 @@ public function setMinute($value) * * @param int|string $value * - * @return self + * @return static * * @throws Exception */ @@ -646,7 +646,7 @@ public function setSecond($value) * * @param int $value * - * @return self + * @return static * * @throws Exception */ @@ -656,7 +656,7 @@ protected function setValue(string $name, $value) ${$name} = $value; - return self::create( + return static::create( (int) $year, (int) $month, (int) $day, @@ -673,7 +673,7 @@ protected function setValue(string $name, $value) * * @param DateTimeZone|string $timezone * - * @return self + * @return static * * @throws Exception */ @@ -682,7 +682,7 @@ public function setTimezone($timezone) { $timezone = $timezone instanceof DateTimeZone ? $timezone : new DateTimeZone($timezone); - return self::createFromInstance($this->toDateTime()->setTimezone($timezone), $this->locale); + return static::createFromInstance($this->toDateTime()->setTimezone($timezone), $this->locale); } /** @@ -690,7 +690,7 @@ public function setTimezone($timezone) * * @param int $timestamp * - * @return self + * @return static * * @throws Exception */ @@ -699,7 +699,7 @@ public function setTimestamp($timestamp) { $time = date('Y-m-d H:i:s', $timestamp); - return self::parse($time, $this->timezone, $this->locale); + return static::parse($time, $this->timezone, $this->locale); } // -------------------------------------------------------------------- @@ -1085,7 +1085,7 @@ public function difference($testTime, ?string $timezone = null) if (is_string($testTime)) { $timezone = ($timezone !== null) ? new DateTimeZone($timezone) : $this->timezone; $testTime = new DateTime($testTime, $timezone); - } elseif ($testTime instanceof self) { + } elseif ($testTime instanceof static) { $testTime = $testTime->toDateTime(); } @@ -1116,7 +1116,7 @@ public function difference($testTime, ?string $timezone = null) */ public function getUTCObject($time, ?string $timezone = null) { - if ($time instanceof self) { + if ($time instanceof static) { $time = $time->toDateTime(); } elseif (is_string($time)) { $timezone = $timezone ?: $this->timezone; From 57b511281396fd0442cf6432ec4b12ddb8cbb321 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 7 Aug 2024 10:39:15 +0900 Subject: [PATCH 191/277] fix: keep the behavior of Entity casts datetime with timestamp It returns Time with the default timezone as before. --- system/Entity/Cast/DatetimeCast.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Entity/Cast/DatetimeCast.php b/system/Entity/Cast/DatetimeCast.php index 2d01ad79b0ae..88b7b29267e0 100644 --- a/system/Entity/Cast/DatetimeCast.php +++ b/system/Entity/Cast/DatetimeCast.php @@ -40,7 +40,7 @@ public static function get($value, array $params = []) } if (is_numeric($value)) { - return Time::createFromTimestamp((int) $value); + return Time::createFromTimestamp((int) $value, date_default_timezone_get()); } if (is_string($value)) { From f38d4a397751bf7dd21ccd3e8e6c217e228ba884 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 7 Aug 2024 11:03:56 +0900 Subject: [PATCH 192/277] fix: keep the behavior of Model casts datetime with timestamp It returns Time with the default timezone as before. --- system/DataCaster/Cast/TimestampCast.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/DataCaster/Cast/TimestampCast.php b/system/DataCaster/Cast/TimestampCast.php index 52a4d88f9e46..f19d1a78810f 100644 --- a/system/DataCaster/Cast/TimestampCast.php +++ b/system/DataCaster/Cast/TimestampCast.php @@ -32,7 +32,7 @@ public static function get( self::invalidTypeValueError($value); } - return Time::createFromTimestamp((int) $value); + return Time::createFromTimestamp((int) $value, date_default_timezone_get()); } public static function set( From 97148ef46d4cd3ce1b98341318197df55673aa72 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 7 Aug 2024 11:10:47 +0900 Subject: [PATCH 193/277] docs: add description for timestamp cast timezone --- user_guide_src/source/models/model.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/user_guide_src/source/models/model.rst b/user_guide_src/source/models/model.rst index c1cbf21a4dbd..9057001ae785 100644 --- a/user_guide_src/source/models/model.rst +++ b/user_guide_src/source/models/model.rst @@ -407,6 +407,12 @@ The datetime format is set in the ``dateFormat`` array of the .. note:: Prior to v4.6.0, you cannot use ``ms`` or ``us`` as a parameter. Because the second's fractional part of Time was lost due to bugs. +timestamp +--------- + +The timezone of the ``Time`` instance created will be the default timezone +(app's timezone), not UTC. + Custom Casting ============== From 3e63b673e69ca4e1db888f80a4f57ae0717b7614 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 12 Aug 2024 08:35:05 +0900 Subject: [PATCH 194/277] feat: add important directives --- system/Security/CheckPhpIni.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/system/Security/CheckPhpIni.php b/system/Security/CheckPhpIni.php index 1e92cdc403aa..59cd587360d5 100644 --- a/system/Security/CheckPhpIni.php +++ b/system/Security/CheckPhpIni.php @@ -124,9 +124,11 @@ public static function checkIni(): array 'log_errors' => [], 'error_log' => [], 'default_charset' => ['recommended' => 'UTF-8'], + 'max_execution_time' => ['remark' => 'The default is 30.'], 'memory_limit' => ['remark' => '> post_max_size'], 'post_max_size' => ['remark' => '> upload_max_filesize'], 'upload_max_filesize' => ['remark' => '< post_max_size'], + 'max_input_vars' => ['remark' => 'The default is 1000.'], 'request_order' => ['recommended' => 'GP'], 'variables_order' => ['recommended' => 'GPCS'], 'date.timezone' => ['recommended' => 'UTC'], @@ -135,6 +137,7 @@ public static function checkIni(): array 'opcache.enable_cli' => [], 'opcache.jit' => [], 'opcache.jit_buffer_size' => [], + 'zend.assertions' => ['recommended' => '-1'], ]; $output = []; From aa6df4e7f74bcb97d5859f772b4c5bdfa65b7c3b Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 12 Aug 2024 08:37:28 +0900 Subject: [PATCH 195/277] feat: improve description --- system/Commands/Utilities/PhpIniCheck.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Commands/Utilities/PhpIniCheck.php b/system/Commands/Utilities/PhpIniCheck.php index 0426f9078b77..f65dcd855fa4 100644 --- a/system/Commands/Utilities/PhpIniCheck.php +++ b/system/Commands/Utilities/PhpIniCheck.php @@ -41,7 +41,7 @@ final class PhpIniCheck extends BaseCommand * * @var string */ - protected $description = 'Check your php.ini values.'; + protected $description = 'Check your php.ini values in production environment.'; /** * The Command's usage From f42b1a0c8185fa22f638fc5da978ba640edc3495 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 20 Aug 2024 10:56:57 +0900 Subject: [PATCH 196/277] test: fix test code for Time::createFromTimestamp() The microseconds should be preserved without rounding. --- tests/system/I18n/TimeTest.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/system/I18n/TimeTest.php b/tests/system/I18n/TimeTest.php index bf8512dbeaf5..10e38e2110a2 100644 --- a/tests/system/I18n/TimeTest.php +++ b/tests/system/I18n/TimeTest.php @@ -287,15 +287,14 @@ public function testCreateFromTimestamp(): void date_default_timezone_set($tz); } - public function testCreateFromTimestampWithMicrotime(): void + public function testCreateFromTimestampWithMicroseconds(): void { $timestamp = 1489762800.654321; // The timezone will be UTC if you don't specify. $time = Time::createFromTimestamp($timestamp); - // float cannot handle the number precisely. - $this->assertSame('2017-03-17 15:00:00.654300', $time->format('Y-m-d H:i:s.u')); + $this->assertSame('2017-03-17 15:00:00.654321', $time->format('Y-m-d H:i:s.u')); } public function testCreateFromTimestampWithTimezone(): void From 06b02bbf80206a74885e7279240f396dd6b8fa2d Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 20 Aug 2024 11:02:48 +0900 Subject: [PATCH 197/277] fix: microseconds are not kept to full precision --- system/I18n/TimeTrait.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/I18n/TimeTrait.php b/system/I18n/TimeTrait.php index 1f4f630750b2..397d9a5a870d 100644 --- a/system/I18n/TimeTrait.php +++ b/system/I18n/TimeTrait.php @@ -265,7 +265,7 @@ public static function createFromFormat($format, $datetime, $timezone = null) */ public static function createFromTimestamp(float|int $timestamp, $timezone = null, ?string $locale = null): static { - $time = new static('@' . $timestamp, 'UTC', $locale); + $time = new static(sprintf('@%.6f', $timestamp), 'UTC', $locale); $timezone ??= 'UTC'; From f3a9ebb5818329399eb4f6e5090376a3acb8f58f Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 6 Aug 2024 14:12:06 +0900 Subject: [PATCH 198/277] fix!: Time::setTimestamp()'s different behavior than DateTime --- system/I18n/TimeTrait.php | 4 +--- tests/system/I18n/TimeTest.php | 26 +++++++++++++++++++++----- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/system/I18n/TimeTrait.php b/system/I18n/TimeTrait.php index 397d9a5a870d..655b1094a79c 100644 --- a/system/I18n/TimeTrait.php +++ b/system/I18n/TimeTrait.php @@ -697,9 +697,7 @@ public function setTimezone($timezone) #[ReturnTypeWillChange] public function setTimestamp($timestamp) { - $time = date('Y-m-d H:i:s', $timestamp); - - return static::parse($time, $this->timezone, $this->locale); + return parent::setTimestamp($timestamp); } // -------------------------------------------------------------------- diff --git a/tests/system/I18n/TimeTest.php b/tests/system/I18n/TimeTest.php index 10e38e2110a2..25ae527575d8 100644 --- a/tests/system/I18n/TimeTest.php +++ b/tests/system/I18n/TimeTest.php @@ -18,6 +18,7 @@ use CodeIgniter\Test\CIUnitTestCase; use Config\App; use DateTime; +use DateTimeImmutable; use DateTimeZone; use IntlDateFormatter; use Locale; @@ -719,13 +720,28 @@ public function testSetTimezone(): void public function testSetTimestamp(): void { - $time = Time::parse('May 10, 2017', 'America/Chicago'); - $stamp = strtotime('April 1, 2017'); - $time2 = $time->setTimestamp($stamp); + $time1 = Time::parse('May 10, 2017', 'America/Chicago'); + + $stamp = strtotime('2017-04-01'); // We use UTC as the default timezone. + $time2 = $time1->setTimestamp($stamp); $this->assertInstanceOf(Time::class, $time2); - $this->assertNotSame($time, $time2); - $this->assertSame('2017-04-01 00:00:00', $time2->toDateTimeString()); + $this->assertSame('2017-05-10 00:00:00 -05:00', $time1->format('Y-m-d H:i:s P')); + $this->assertSame('2017-03-31 19:00:00 -05:00', $time2->format('Y-m-d H:i:s P')); + } + + public function testSetTimestampDateTimeImmutable(): void + { + $time1 = new DateTimeImmutable( + 'May 10, 2017', + new DateTimeZone('America/Chicago') + ); + + $stamp = strtotime('2017-04-01'); // We use UTC as the default timezone. + $time2 = $time1->setTimestamp($stamp); + + $this->assertSame('2017-05-10 00:00:00 -05:00', $time1->format('Y-m-d H:i:s P')); + $this->assertSame('2017-03-31 19:00:00 -05:00', $time2->format('Y-m-d H:i:s P')); } public function testToDateString(): void From b20df7c878e3ce09ed8534a0d0f18c66ce56d2ef Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 6 Aug 2024 14:26:37 +0900 Subject: [PATCH 199/277] fix: keep TimeLegacy::setTimestamp() behavior We do not fix the bug in TimeLegacy. --- system/I18n/TimeLegacy.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/system/I18n/TimeLegacy.php b/system/I18n/TimeLegacy.php index b62877ec40d6..7dfa0526304a 100644 --- a/system/I18n/TimeLegacy.php +++ b/system/I18n/TimeLegacy.php @@ -14,6 +14,8 @@ namespace CodeIgniter\I18n; use DateTime; +use Exception; +use ReturnTypeWillChange; /** * Legacy Time class. @@ -47,4 +49,21 @@ class TimeLegacy extends DateTime { use TimeTrait; + + /** + * Returns a new instance with the date set to the new timestamp. + * + * @param int $timestamp + * + * @return static + * + * @throws Exception + */ + #[ReturnTypeWillChange] + public function setTimestamp($timestamp) + { + $time = date('Y-m-d H:i:s', $timestamp); + + return static::parse($time, $this->timezone, $this->locale); + } } From 9395543622fc9db4b7ed3786b268a49c08949e8e Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 6 Aug 2024 14:30:58 +0900 Subject: [PATCH 200/277] refactor: remove setTimestamp() because it only returns parent method --- system/I18n/TimeTrait.php | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/system/I18n/TimeTrait.php b/system/I18n/TimeTrait.php index 655b1094a79c..e3808942e335 100644 --- a/system/I18n/TimeTrait.php +++ b/system/I18n/TimeTrait.php @@ -685,21 +685,6 @@ public function setTimezone($timezone) return static::createFromInstance($this->toDateTime()->setTimezone($timezone), $this->locale); } - /** - * Returns a new instance with the date set to the new timestamp. - * - * @param int $timestamp - * - * @return static - * - * @throws Exception - */ - #[ReturnTypeWillChange] - public function setTimestamp($timestamp) - { - return parent::setTimestamp($timestamp); - } - // -------------------------------------------------------------------- // Add/Subtract // -------------------------------------------------------------------- From 3884480a10f9db7f6975d14d5c73e39b7cb6568b Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 20 Aug 2024 17:06:00 +0900 Subject: [PATCH 201/277] docs: update user guide --- user_guide_src/source/changelogs/v4.6.0.rst | 6 ++++++ .../source/installation/upgrade_460.rst | 19 +++++++++++++++++++ .../source/installation/upgrade_460/008.php | 18 ++++++++++++++++++ .../source/installation/upgrade_460/009.php | 18 ++++++++++++++++++ user_guide_src/source/libraries/time.rst | 3 +++ user_guide_src/source/libraries/time/030.php | 4 +++- 6 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 user_guide_src/source/installation/upgrade_460/008.php create mode 100644 user_guide_src/source/installation/upgrade_460/009.php diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index 51c43456ae28..2077164e8ba1 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -71,6 +71,12 @@ Time with Microseconds Fixed bugs that some methods in ``Time`` to lose microseconds have been fixed. See :ref:`Upgrading Guide ` for details. +Time::setTimestamp() +-------------------- + +``Time::setTimestamp()`` behavior has been fixed. +See :ref:`Upgrading Guide ` for details. + .. _v460-interface-changes: Interface Changes diff --git a/user_guide_src/source/installation/upgrade_460.rst b/user_guide_src/source/installation/upgrade_460.rst index 5cbfd664bf92..da0875849156 100644 --- a/user_guide_src/source/installation/upgrade_460.rst +++ b/user_guide_src/source/installation/upgrade_460.rst @@ -85,6 +85,25 @@ Also, methods that returns an ``int`` still lose the microseconds. .. literalinclude:: upgrade_460/005.php :lines: 2- +.. _upgrade-460-time-set-timestamp: + +Time::setTimestamp() Behavior Fix +================================= + +In previous versions, if you call ``Time::setTimestamp()`` on a Time instance with +a timezone other than the default timezone might return a Time instance with the +wrong date/time. + +This bug has been fixed, and it now behaves in the same way as ``DateTimeImmutable``: + +.. literalinclude:: upgrade_460/008.php + :lines: 2- + +Note that if you use the default timezone, the behavior is not changed: + +.. literalinclude:: upgrade_460/009.php + :lines: 2- + .. _upgrade-460-registrars-with-dirty-hack: Registrars with Dirty Hack diff --git a/user_guide_src/source/installation/upgrade_460/008.php b/user_guide_src/source/installation/upgrade_460/008.php new file mode 100644 index 000000000000..5a778de09c53 --- /dev/null +++ b/user_guide_src/source/installation/upgrade_460/008.php @@ -0,0 +1,18 @@ +setTimestamp($stamp); + +echo $time2->format('Y-m-d H:i:s P'); +// Before: 2024-08-20 00:00:00 -05:00 +// After: 2024-08-19 19:00:00 -05:00 diff --git a/user_guide_src/source/installation/upgrade_460/009.php b/user_guide_src/source/installation/upgrade_460/009.php new file mode 100644 index 000000000000..f00daa0b63b0 --- /dev/null +++ b/user_guide_src/source/installation/upgrade_460/009.php @@ -0,0 +1,18 @@ +setTimestamp($stamp); + +echo $time2->format('Y-m-d H:i:s P'); +// Before: 2024-08-20 00:00:00 -05:00 +// After: 2024-08-20 00:00:00 -05:00 diff --git a/user_guide_src/source/libraries/time.rst b/user_guide_src/source/libraries/time.rst index 9fd986d31421..17af069c8c5f 100644 --- a/user_guide_src/source/libraries/time.rst +++ b/user_guide_src/source/libraries/time.rst @@ -314,6 +314,9 @@ Returns a new instance with the date set to the new timestamp: .. literalinclude:: time/030.php +.. note:: Prior to v4.6.0, due to a bug, this method might return incorrect + date/time. See :ref:`Upgrading Guide ` for details. + Modifying the Value =================== diff --git a/user_guide_src/source/libraries/time/030.php b/user_guide_src/source/libraries/time/030.php index 1e22876e9883..5ca37d326925 100644 --- a/user_guide_src/source/libraries/time/030.php +++ b/user_guide_src/source/libraries/time/030.php @@ -2,7 +2,9 @@ use CodeIgniter\I18n\Time; -$time = Time::parse('May 10, 2017', 'America/Chicago'); +// The Application Timezone is "America/Chicago". + +$time = Time::parse('May 10, 2017'); $time2 = $time->setTimestamp(strtotime('April 1, 2017')); echo $time->toDateTimeString(); // 2017-05-10 00:00:00 From f08c10350366da855034209be8d2686ac9e4c74f Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 22 Aug 2024 08:04:58 +0900 Subject: [PATCH 202/277] docs: remove unneeded "`" --- user_guide_src/source/changelogs/v4.6.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index 2077164e8ba1..7814caa04669 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -104,7 +104,7 @@ Method Signature Changes - **View:** The return type of the ``renderSection()`` method has been changed to ``string``, and now the method does not call ``echo``. - **Time:** The first parameter type of the ``createFromTimestamp()`` has been - changed from ``int`` to ``int|float``, and the return type ``static``` has been + changed from ``int`` to ``int|float``, and the return type ``static`` has been added. Removed Type Definitions From 6cf5c78b46c846f171d82d0063ac3118fb1714e7 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 21 Aug 2024 09:33:46 +0900 Subject: [PATCH 203/277] chore: add PHP 8.4 to test-phpunit.yml --- .github/workflows/test-phpunit.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-phpunit.yml b/.github/workflows/test-phpunit.yml index ba566ce5c9d1..3ee570bb1295 100644 --- a/.github/workflows/test-phpunit.yml +++ b/.github/workflows/test-phpunit.yml @@ -59,8 +59,9 @@ jobs: - '8.1' - '8.2' - '8.3' + - '8.4' include: - - php-version: '8.3' + - php-version: '8.4' composer-option: '--ignore-platform-req=php' uses: ./.github/workflows/reusable-phpunit-test.yml # @TODO Extract to codeigniter4/.github repo @@ -88,6 +89,7 @@ jobs: - '8.1' - '8.2' - '8.3' + - '8.4' db-platform: - MySQLi - OCI8 @@ -100,7 +102,7 @@ jobs: - php-version: '8.1' db-platform: MySQLi mysql-version: '5.7' - - php-version: '8.3' + - php-version: '8.4' composer-option: '--ignore-platform-req=php' uses: ./.github/workflows/reusable-phpunit-test.yml # @TODO Extract to codeigniter4/.github repo @@ -129,8 +131,9 @@ jobs: - '8.1' - '8.2' - '8.3' + - '8.4' include: - - php-version: '8.3' + - php-version: '8.4' composer-option: '--ignore-platform-req=php' uses: ./.github/workflows/reusable-phpunit-test.yml # @TODO Extract to codeigniter4/.github repo @@ -157,8 +160,9 @@ jobs: - '8.1' - '8.2' - '8.3' + - '8.4' include: - - php-version: '8.3' + - php-version: '8.4' composer-option: '--ignore-platform-req=php' uses: ./.github/workflows/reusable-phpunit-test.yml # @TODO Extract to codeigniter4/.github repo From 01ddd2c34945fb6c25479174d67f91e9e100da2b Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 21 Aug 2024 09:43:58 +0900 Subject: [PATCH 204/277] chore: add continue-on-error --- .github/workflows/test-phpunit.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test-phpunit.yml b/.github/workflows/test-phpunit.yml index 3ee570bb1295..bd13e73fe89f 100644 --- a/.github/workflows/test-phpunit.yml +++ b/.github/workflows/test-phpunit.yml @@ -63,6 +63,7 @@ jobs: include: - php-version: '8.4' composer-option: '--ignore-platform-req=php' + continue-on-error: true uses: ./.github/workflows/reusable-phpunit-test.yml # @TODO Extract to codeigniter4/.github repo with: @@ -104,6 +105,7 @@ jobs: mysql-version: '5.7' - php-version: '8.4' composer-option: '--ignore-platform-req=php' + continue-on-error: true uses: ./.github/workflows/reusable-phpunit-test.yml # @TODO Extract to codeigniter4/.github repo with: @@ -135,6 +137,7 @@ jobs: include: - php-version: '8.4' composer-option: '--ignore-platform-req=php' + continue-on-error: true uses: ./.github/workflows/reusable-phpunit-test.yml # @TODO Extract to codeigniter4/.github repo with: @@ -164,6 +167,7 @@ jobs: include: - php-version: '8.4' composer-option: '--ignore-platform-req=php' + continue-on-error: true uses: ./.github/workflows/reusable-phpunit-test.yml # @TODO Extract to codeigniter4/.github repo with: From 452b792327e46b0ffacb3167d2ae4156a8e43896 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 21 Aug 2024 09:56:09 +0900 Subject: [PATCH 205/277] chore: remove duplicated 8.4 --- .github/workflows/test-phpunit.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/test-phpunit.yml b/.github/workflows/test-phpunit.yml index bd13e73fe89f..59f285d2c747 100644 --- a/.github/workflows/test-phpunit.yml +++ b/.github/workflows/test-phpunit.yml @@ -59,7 +59,6 @@ jobs: - '8.1' - '8.2' - '8.3' - - '8.4' include: - php-version: '8.4' composer-option: '--ignore-platform-req=php' @@ -90,7 +89,6 @@ jobs: - '8.1' - '8.2' - '8.3' - - '8.4' db-platform: - MySQLi - OCI8 @@ -133,7 +131,6 @@ jobs: - '8.1' - '8.2' - '8.3' - - '8.4' include: - php-version: '8.4' composer-option: '--ignore-platform-req=php' @@ -163,7 +160,6 @@ jobs: - '8.1' - '8.2' - '8.3' - - '8.4' include: - php-version: '8.4' composer-option: '--ignore-platform-req=php' From 296c5a509119d20618b84b954380739e2e790cda Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 21 Aug 2024 10:22:14 +0900 Subject: [PATCH 206/277] chore: add "--ignore-platform-req=php" temporarily Error: Your requirements could not be resolved to an installable set of packages. Problem 1 - vimeo/psalm[5.0.0, ..., 5.13.1] require php ^7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0 -> your php version (8.4.0-dev) does not satisfy that requirement. - vimeo/psalm[5.14.0, ..., 5.25.0] require php ^7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 -> your php version (8.4.0-dev) does not satisfy that requirement. - Root composer.json requires vimeo/psalm ^5.0 -> satisfiable by vimeo/psalm[5.0.0, ..., 5.25.0]. Script @composer update --working-dir=utils handling the post-autoload-dump event returned with error code 2 Error: Process completed with exit code 2. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 303f15bd1ce8..c778e029f5dc 100644 --- a/composer.json +++ b/composer.json @@ -88,7 +88,7 @@ "CodeIgniter\\ComposerScripts::postUpdate" ], "post-autoload-dump": [ - "@composer update --working-dir=utils" + "@composer update --working-dir=utils --ignore-platform-req=php" ], "analyze": [ "Composer\\Config::disableProcessTimeout", From 312ffa0ae6bbc3744c33cf531d52285cb9e4a4c4 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 22 Aug 2024 08:20:18 +0900 Subject: [PATCH 207/277] chore: move continue-on-error to reusable-phpunit-test.yml --- .github/workflows/reusable-phpunit-test.yml | 1 + .github/workflows/test-phpunit.yml | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/reusable-phpunit-test.yml b/.github/workflows/reusable-phpunit-test.yml index 84ade1d08862..c5ee92e7a70c 100644 --- a/.github/workflows/reusable-phpunit-test.yml +++ b/.github/workflows/reusable-phpunit-test.yml @@ -203,6 +203,7 @@ jobs: DB: ${{ inputs.db-platform }} TACHYCARDIA_MONITOR_GA: ${{ inputs.enable-profiling && 'enabled' || '' }} TERM: xterm-256color + continue-on-error: ${{ inputs.php-version == '8.4' }} - name: Upload coverage results as artifact if: ${{ inputs.enable-artifact-upload }} diff --git a/.github/workflows/test-phpunit.yml b/.github/workflows/test-phpunit.yml index 59f285d2c747..349a4b658ec2 100644 --- a/.github/workflows/test-phpunit.yml +++ b/.github/workflows/test-phpunit.yml @@ -62,7 +62,6 @@ jobs: include: - php-version: '8.4' composer-option: '--ignore-platform-req=php' - continue-on-error: true uses: ./.github/workflows/reusable-phpunit-test.yml # @TODO Extract to codeigniter4/.github repo with: @@ -103,7 +102,6 @@ jobs: mysql-version: '5.7' - php-version: '8.4' composer-option: '--ignore-platform-req=php' - continue-on-error: true uses: ./.github/workflows/reusable-phpunit-test.yml # @TODO Extract to codeigniter4/.github repo with: @@ -134,7 +132,6 @@ jobs: include: - php-version: '8.4' composer-option: '--ignore-platform-req=php' - continue-on-error: true uses: ./.github/workflows/reusable-phpunit-test.yml # @TODO Extract to codeigniter4/.github repo with: @@ -163,7 +160,6 @@ jobs: include: - php-version: '8.4' composer-option: '--ignore-platform-req=php' - continue-on-error: true uses: ./.github/workflows/reusable-phpunit-test.yml # @TODO Extract to codeigniter4/.github repo with: From 10d10b6f138f7cabd5be5e6e05092126a52ef52c Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 22 Aug 2024 08:52:09 +0900 Subject: [PATCH 208/277] docs: add note for PHP and CI4 versions --- user_guide_src/source/intro/requirements.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/user_guide_src/source/intro/requirements.rst b/user_guide_src/source/intro/requirements.rst index db01ef5fb002..6c615f9bfd5b 100644 --- a/user_guide_src/source/intro/requirements.rst +++ b/user_guide_src/source/intro/requirements.rst @@ -22,6 +22,13 @@ PHP and Required Extensions - If you are still using PHP 7.4 or 8.0, you should upgrade immediately. - The end of life date for PHP 8.1 will be December 31, 2025. +.. note:: + - PHP 8.4 requires CodeIgniter 4.6.0 or later. + - PHP 8.3 requires CodeIgniter 4.4.4 or later. + - PHP 8.2 requires CodeIgniter 4.2.11 or later. + - PHP 8.1 requires CodeIgniter 4.1.6 or later. + - **Note that we only maintain the latest version.** + *********************** Optional PHP Extensions *********************** From de1f913e07ab593a80df83f90ba490a11a9ca05f Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 22 Aug 2024 08:52:52 +0900 Subject: [PATCH 209/277] docs: make line bold --- user_guide_src/source/intro/requirements.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/intro/requirements.rst b/user_guide_src/source/intro/requirements.rst index 6c615f9bfd5b..c90cb9c8a7eb 100644 --- a/user_guide_src/source/intro/requirements.rst +++ b/user_guide_src/source/intro/requirements.rst @@ -19,7 +19,7 @@ PHP and Required Extensions .. warning:: - The end of life date for PHP 7.4 was November 28, 2022. - The end of life date for PHP 8.0 was November 26, 2023. - - If you are still using PHP 7.4 or 8.0, you should upgrade immediately. + - **If you are still using PHP 7.4 or 8.0, you should upgrade immediately.** - The end of life date for PHP 8.1 will be December 31, 2025. .. note:: From 5437a4fb37500b3a36d4b4b386ffab15d9544d05 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 23 Aug 2024 14:55:34 +0900 Subject: [PATCH 210/277] refactor: extract isDisplayErrorsEnabled() --- system/Debug/ExceptionHandler.php | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/system/Debug/ExceptionHandler.php b/system/Debug/ExceptionHandler.php index d6f97b76a32b..ba3634393be5 100644 --- a/system/Debug/ExceptionHandler.php +++ b/system/Debug/ExceptionHandler.php @@ -134,13 +134,7 @@ protected function determineView( // Production environments should have a custom exception file. $view = 'production.php'; - if ( - in_array( - strtolower(ini_get('display_errors')), - ['1', 'true', 'on', 'yes'], - true - ) - ) { + if ($this->isDisplayErrorsEnabled()) { $view = 'error_exception.php'; } @@ -158,4 +152,13 @@ protected function determineView( return $view; } + + private function isDisplayErrorsEnabled(): bool + { + return in_array( + strtolower(ini_get('display_errors')), + ['1', 'true', 'on', 'yes'], + true + ); + } } From 0e2800e71b78617191972adac5c8f1921a097832 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 23 Aug 2024 15:03:57 +0900 Subject: [PATCH 211/277] fix!: change conditions to show error details for non-HTML request Change to behave the same as HTML requests. --- system/Debug/ExceptionHandler.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/system/Debug/ExceptionHandler.php b/system/Debug/ExceptionHandler.php index ba3634393be5..d63b599bf6c1 100644 --- a/system/Debug/ExceptionHandler.php +++ b/system/Debug/ExceptionHandler.php @@ -75,8 +75,10 @@ public function handle( ); } + // Handles non-HTML requests. if (! str_contains($request->getHeaderLine('accept'), 'text/html')) { - $data = (ENVIRONMENT === 'development' || ENVIRONMENT === 'testing') + // If display_errors is enabled, shows the error details. + $data = $this->isDisplayErrorsEnabled() ? $this->collectVars($exception, $statusCode) : ''; @@ -135,6 +137,7 @@ protected function determineView( $view = 'production.php'; if ($this->isDisplayErrorsEnabled()) { + // If display_errors is enabled, shows the error details. $view = 'error_exception.php'; } From fd21659131a4efe72e55d405e7be492c8e430f49 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 23 Aug 2024 15:25:37 +0900 Subject: [PATCH 212/277] docs: update docs --- user_guide_src/source/changelogs/v4.6.0.rst | 13 +++++++++++++ user_guide_src/source/general/errors.rst | 10 +++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index 7814caa04669..acfcc84ad025 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -77,6 +77,19 @@ Time::setTimestamp() ``Time::setTimestamp()`` behavior has been fixed. See :ref:`Upgrading Guide ` for details. +Error Reporting to Non-HTML Requests +------------------------------------ + +In previous versions, when a request does not accept HTML, CodeIgniter showed +error details only in the ``development`` and ``testing`` environments. + +But because it is not possible to display error details when using a custom +environment, this behavior has been fixed so that error details are displayed if +``display_errors`` in PHP ini setting is enabled. + +With this fix, the error details are now displayed under the same conditions for +both HTML requests and non-HTML requests. + .. _v460-interface-changes: Interface Changes diff --git a/user_guide_src/source/general/errors.rst b/user_guide_src/source/general/errors.rst index 3f9e7c55eab7..9adeb7c6db26 100644 --- a/user_guide_src/source/general/errors.rst +++ b/user_guide_src/source/general/errors.rst @@ -57,8 +57,12 @@ Configuration Error Reporting --------------- -By default, CodeIgniter will display a detailed error report with all errors in the ``development`` and ``testing`` environments, and will not -display any errors in the ``production`` environment. +When ``display_errors`` in PHP ini setting is enabled, CodeIgniter will display +a detailed error report with all errors + +So by default, CodeIgniter will display a detailed error report in the ``development`` +and ``testing`` environments, and will not display any errors in the ``production`` +environment. .. image:: ../images/error.png @@ -251,7 +255,7 @@ the **error_404.php** in the **app/Views/errors/cli** folder. If there is no view file corresponding to the HTTP status code, **production.php** or **error_exception.php** will be displayed. -.. note:: If ``display_errors`` is on in the PHP INI configuration, +.. note:: If ``display_errors`` is on in the PHP ini setting, **error_exception.php** is selected and a detailed error report is displayed. You should customize all of the error views in the **app/Views/errors/html** folder From e79028a92e5230b585410a87ef20026eee15198d Mon Sep 17 00:00:00 2001 From: Denny Septian Panggabean Date: Mon, 26 Aug 2024 14:22:57 +0700 Subject: [PATCH 213/277] feat: multiple hostname routing --- system/Router/RouteCollection.php | 7 ++++ tests/system/Router/RouteCollectionTest.php | 41 +++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 6116697cb4f4..1374edc803ce 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -1571,6 +1571,13 @@ private function checkHostname($hostname): bool return false; } + // Has multiple hostname + if (is_array($hostname)) { + $hostnameLower = array_map('strtolower', $hostname); + + return in_array(strtolower($this->httpHost), $hostnameLower, true); + } + return strtolower($this->httpHost) === strtolower($hostname); } diff --git a/tests/system/Router/RouteCollectionTest.php b/tests/system/Router/RouteCollectionTest.php index 00cb087bb945..aef0387e92cd 100644 --- a/tests/system/Router/RouteCollectionTest.php +++ b/tests/system/Router/RouteCollectionTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Router; +use App\Controllers\Home; use App\Controllers\Product; use CodeIgniter\Config\Services; use CodeIgniter\controller; @@ -1744,6 +1745,46 @@ public function testRouteOverwritingMatchingHost(): void $this->assertSame($expects, $router->handle('/')); } + public function testRouteMatchingHostMultipleCorrect(): void + { + service('superglobals')->setServer('HTTP_HOST', 'two.domain.com'); + service('request')->setMethod(Method::GET); + + $routes = $this->getCollector(); + $router = new Router($routes, Services::request()); + + $routes->setDefaultNamespace('App\Controllers'); + $routes->setDefaultController('Home'); + $routes->setDefaultMethod('index'); + + $routes->get('/', 'Home::index', ['as' => 'ddd']); + $routes->get('/', '\App\Controllers\Site\CDoc::index', ['hostname' => ['one.domain.com', 'two.domain.com', 'three.domain.com']]); + + $expects = '\App\Controllers\Site\CDoc'; + + $this->assertSame($expects, $router->handle('/')); + } + + public function testRouteMatchingHostMultipleFail(): void + { + service('superglobals')->setServer('HTTP_HOST', 'doc.domain.com'); + service('request')->setMethod(Method::GET); + + $routes = $this->getCollector(); + $router = new Router($routes, Services::request()); + + $routes->setDefaultNamespace('App\Controllers'); + $routes->setDefaultController('Home'); + $routes->setDefaultMethod('index'); + + $routes->get('/', 'Home::index', ['as' => 'ddd']); + $routes->get('/', '\App\Controllers\Site\CDoc::index', ['hostname' => ['one.domain.com', 'two.domain.com', 'three.domain.com']]); + + $expects = '\\' . Home::class; + + $this->assertSame($expects, $router->handle('/')); + } + /** * Tests for router DefaultNameSpace issue * From fb22d2dd04b0c51d21ef0e675b94f7c393b04eb1 Mon Sep 17 00:00:00 2001 From: Denny Septian Panggabean Date: Tue, 27 Aug 2024 08:53:46 +0700 Subject: [PATCH 214/277] fix: docblock error --- system/Router/RouteCollection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 1374edc803ce..122f97a10b89 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -1562,7 +1562,7 @@ protected function create(string $verb, string $from, $to, ?array $options = nul * Compares the hostname passed in against the current hostname * on this page request. * - * @param string $hostname Hostname in route options + * @param list|string $hostname Hostname in route options */ private function checkHostname($hostname): bool { From d7c7db81e148a00057ea33a28687b710791da409 Mon Sep 17 00:00:00 2001 From: Denny Septian Panggabean Date: Tue, 27 Aug 2024 09:09:13 +0700 Subject: [PATCH 215/277] docs: user guide --- user_guide_src/source/changelogs/v4.6.0.rst | 5 +++++ user_guide_src/source/incoming/routing.rst | 6 ++++++ user_guide_src/source/incoming/routing/073.php | 3 +++ 3 files changed, 14 insertions(+) create mode 100644 user_guide_src/source/incoming/routing/073.php diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index 7814caa04669..676d22a8f14b 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -165,6 +165,11 @@ Commands arguments. - The ``spark filter:check`` command now displays filter classnames. +Routing +======= + +- Restrict limit hostname can multiple. + Testing ======= diff --git a/user_guide_src/source/incoming/routing.rst b/user_guide_src/source/incoming/routing.rst index 340dde63e4c9..aa388ecde70f 100644 --- a/user_guide_src/source/incoming/routing.rst +++ b/user_guide_src/source/incoming/routing.rst @@ -476,6 +476,12 @@ by passing the "hostname" option along with the desired domain to allow it on as This example would only allow the specified hosts to work if the domain exactly matched **accounts.example.com**. It would not work under the main site at **example.com**. +.. versionadded:: 4.6.0 + +Also you can restrict with multiple hostname, e.g: + +.. literalinclude:: routing/073.php + Limit to Subdomains =================== diff --git a/user_guide_src/source/incoming/routing/073.php b/user_guide_src/source/incoming/routing/073.php new file mode 100644 index 000000000000..b8c42b0ad7d5 --- /dev/null +++ b/user_guide_src/source/incoming/routing/073.php @@ -0,0 +1,3 @@ +get('from', 'to', ['hostname' => ['s1.example.com', 's2.example.com']]); From ab1d845be847e9f55cf3da7b28242999211e0159 Mon Sep 17 00:00:00 2001 From: Denny Septian Panggabean <97607754+ddevsr@users.noreply.github.com> Date: Tue, 27 Aug 2024 09:50:48 +0700 Subject: [PATCH 216/277] Update user_guide_src/source/incoming/routing.rst Co-authored-by: kenjis --- user_guide_src/source/incoming/routing.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/incoming/routing.rst b/user_guide_src/source/incoming/routing.rst index aa388ecde70f..c2f5977384a2 100644 --- a/user_guide_src/source/incoming/routing.rst +++ b/user_guide_src/source/incoming/routing.rst @@ -476,9 +476,12 @@ by passing the "hostname" option along with the desired domain to allow it on as This example would only allow the specified hosts to work if the domain exactly matched **accounts.example.com**. It would not work under the main site at **example.com**. +Restrict by Multiple Hostnames +------------------------------ + .. versionadded:: 4.6.0 -Also you can restrict with multiple hostname, e.g: +Also you can restrict by multiple hostnames, e.g: .. literalinclude:: routing/073.php From 94e12145266f38757e2111071aa3bfb90ff21de2 Mon Sep 17 00:00:00 2001 From: Denny Septian Panggabean <97607754+ddevsr@users.noreply.github.com> Date: Tue, 27 Aug 2024 09:51:02 +0700 Subject: [PATCH 217/277] Update user_guide_src/source/changelogs/v4.6.0.rst Co-authored-by: kenjis --- user_guide_src/source/changelogs/v4.6.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index 676d22a8f14b..3cf69c0b2690 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -168,7 +168,7 @@ Commands Routing ======= -- Restrict limit hostname can multiple. +- Now you can specify multiple hostnames when restricting routes. Testing ======= From a99755320a772d219265128a1e43f80ff79c8577 Mon Sep 17 00:00:00 2001 From: Denny Septian Panggabean <97607754+ddevsr@users.noreply.github.com> Date: Tue, 27 Aug 2024 09:51:16 +0700 Subject: [PATCH 218/277] Update system/Router/RouteCollection.php Co-authored-by: kenjis --- system/Router/RouteCollection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 122f97a10b89..c5e13fc3e4dc 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -1571,7 +1571,7 @@ private function checkHostname($hostname): bool return false; } - // Has multiple hostname + // Has multiple hostnames if (is_array($hostname)) { $hostnameLower = array_map('strtolower', $hostname); From ab5dd6cacf879a38e96290c256e9fb9d794e0a89 Mon Sep 17 00:00:00 2001 From: Denny Septian Panggabean <97607754+ddevsr@users.noreply.github.com> Date: Tue, 27 Aug 2024 09:51:30 +0700 Subject: [PATCH 219/277] Update tests/system/Router/RouteCollectionTest.php Co-authored-by: kenjis --- tests/system/Router/RouteCollectionTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/system/Router/RouteCollectionTest.php b/tests/system/Router/RouteCollectionTest.php index aef0387e92cd..16a5bee9c284 100644 --- a/tests/system/Router/RouteCollectionTest.php +++ b/tests/system/Router/RouteCollectionTest.php @@ -1757,7 +1757,7 @@ public function testRouteMatchingHostMultipleCorrect(): void $routes->setDefaultController('Home'); $routes->setDefaultMethod('index'); - $routes->get('/', 'Home::index', ['as' => 'ddd']); + $routes->get('/', 'Home::index', ['as' => 'home']); $routes->get('/', '\App\Controllers\Site\CDoc::index', ['hostname' => ['one.domain.com', 'two.domain.com', 'three.domain.com']]); $expects = '\App\Controllers\Site\CDoc'; From dea605b6f86d795d7b171bd50f1dc9d9c5ffdb5e Mon Sep 17 00:00:00 2001 From: Denny Septian Panggabean <97607754+ddevsr@users.noreply.github.com> Date: Tue, 27 Aug 2024 09:51:35 +0700 Subject: [PATCH 220/277] Update tests/system/Router/RouteCollectionTest.php Co-authored-by: kenjis --- tests/system/Router/RouteCollectionTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/system/Router/RouteCollectionTest.php b/tests/system/Router/RouteCollectionTest.php index 16a5bee9c284..5931f4de131b 100644 --- a/tests/system/Router/RouteCollectionTest.php +++ b/tests/system/Router/RouteCollectionTest.php @@ -1777,7 +1777,7 @@ public function testRouteMatchingHostMultipleFail(): void $routes->setDefaultController('Home'); $routes->setDefaultMethod('index'); - $routes->get('/', 'Home::index', ['as' => 'ddd']); + $routes->get('/', 'Home::index', ['as' => 'home']); $routes->get('/', '\App\Controllers\Site\CDoc::index', ['hostname' => ['one.domain.com', 'two.domain.com', 'three.domain.com']]); $expects = '\\' . Home::class; From 105c1f8259caf9e72b9b6d008e6a2c512536427d Mon Sep 17 00:00:00 2001 From: Denny Septian Panggabean Date: Tue, 27 Aug 2024 10:09:45 +0700 Subject: [PATCH 221/277] refactor: rename variable --- tests/system/Router/RouteCollectionTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/system/Router/RouteCollectionTest.php b/tests/system/Router/RouteCollectionTest.php index 5931f4de131b..1a5c72a0b68e 100644 --- a/tests/system/Router/RouteCollectionTest.php +++ b/tests/system/Router/RouteCollectionTest.php @@ -1760,9 +1760,9 @@ public function testRouteMatchingHostMultipleCorrect(): void $routes->get('/', 'Home::index', ['as' => 'home']); $routes->get('/', '\App\Controllers\Site\CDoc::index', ['hostname' => ['one.domain.com', 'two.domain.com', 'three.domain.com']]); - $expects = '\App\Controllers\Site\CDoc'; + $expect = '\App\Controllers\Site\CDoc'; - $this->assertSame($expects, $router->handle('/')); + $this->assertSame($expect, $router->handle('/')); } public function testRouteMatchingHostMultipleFail(): void @@ -1780,9 +1780,9 @@ public function testRouteMatchingHostMultipleFail(): void $routes->get('/', 'Home::index', ['as' => 'home']); $routes->get('/', '\App\Controllers\Site\CDoc::index', ['hostname' => ['one.domain.com', 'two.domain.com', 'three.domain.com']]); - $expects = '\\' . Home::class; + $expect = '\\' . Home::class; - $this->assertSame($expects, $router->handle('/')); + $this->assertSame($expect, $router->handle('/')); } /** From 973e1487563b67955e2697d1bf26ec6e8c27824a Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 21 Aug 2024 13:02:18 +0900 Subject: [PATCH 222/277] feat: add deprecation error handling for session.sid_length and session.sid_bits_per_character --- system/Debug/Exceptions.php | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php index e5f7ecb04881..fac10b32114d 100644 --- a/system/Debug/Exceptions.php +++ b/system/Debug/Exceptions.php @@ -209,6 +209,10 @@ public function exceptionHandler(Throwable $exception) public function errorHandler(int $severity, string $message, ?string $file = null, ?int $line = null) { if ($this->isDeprecationError($severity)) { + if ($this->isSessionSidDeprecationError($message, $file, $line)) { + return true; + } + if (! $this->config->logDeprecations || (bool) env('CODEIGNITER_SCREAM_DEPRECATIONS')) { throw new ErrorException($message, 0, $severity, $file, $line); } @@ -223,6 +227,32 @@ public function errorHandler(int $severity, string $message, ?string $file = nul return false; // return false to propagate the error to PHP standard error handler } + /** + * Handles session.sid_length and session.sid_bits_per_character deprecations + * in PHP 8.4. + */ + private function isSessionSidDeprecationError(string $message, ?string $file = null, ?int $line = null): bool + { + if ( + PHP_VERSION_ID >= 80400 + && str_contains($message, 'session.sid_') + ) { + log_message( + LogLevel::WARNING, + '[DEPRECATED] {message} in {errFile} on line {errLine}.', + [ + 'message' => $message, + 'errFile' => clean_path($file ?? ''), + 'errLine' => $line ?? 0, + ] + ); + + return true; + } + + return false; + } + /** * Checks to see if any errors have happened during shutdown that * need to be caught and handle them. From 12385ed3b70bfc751675c507f705d764cc4b6a5f Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 22 Aug 2024 10:25:57 +0900 Subject: [PATCH 223/277] fix!: we force PHP defaults of session.sid_bits_per_character/session.sid_length They are deprecated in PHP 8.4. --- system/Session/Handlers/FileHandler.php | 33 +++++++--------- system/Session/Session.php | 50 +++++++------------------ 2 files changed, 26 insertions(+), 57 deletions(-) diff --git a/system/Session/Handlers/FileHandler.php b/system/Session/Handlers/FileHandler.php index 4dbec779526a..2b2425eb0458 100644 --- a/system/Session/Handlers/FileHandler.php +++ b/system/Session/Handlers/FileHandler.php @@ -309,32 +309,25 @@ public function gc($max_lifetime) /** * Configure Session ID regular expression + * + * To make life easier, we force the PHP defaults. Because PHP9 forces them. + * See https://wiki.php.net/rfc/deprecations_php_8_4#sessionsid_length_and_sessionsid_bits_per_character */ protected function configureSessionIDRegex() { $bitsPerCharacter = (int) ini_get('session.sid_bits_per_character'); - $SIDLength = (int) ini_get('session.sid_length'); - - if (($bits = $SIDLength * $bitsPerCharacter) < 160) { - // Add as many more characters as necessary to reach at least 160 bits - $SIDLength += (int) ceil((160 % $bits) / $bitsPerCharacter); - ini_set('session.sid_length', (string) $SIDLength); - } - - switch ($bitsPerCharacter) { - case 4: - $this->sessionIDRegex = '[0-9a-f]'; - break; + $sidLength = (int) ini_get('session.sid_length'); - case 5: - $this->sessionIDRegex = '[0-9a-v]'; - break; - - case 6: - $this->sessionIDRegex = '[0-9a-zA-Z,-]'; - break; + // We force the PHP defaults. + if (PHP_VERSION_ID < 90000) { + if ($bitsPerCharacter !== 4) { + ini_set('session.sid_bits_per_character', '4'); + } + if ($sidLength !== 32) { + ini_set('session.sid_length', '32'); + } } - $this->sessionIDRegex .= '{' . $SIDLength . '}'; + $this->sessionIDRegex = '[0-9a-f]{32}'; } } diff --git a/system/Session/Session.php b/system/Session/Session.php index 0aabcbe31d53..cf11e92f6274 100644 --- a/system/Session/Session.php +++ b/system/Session/Session.php @@ -316,49 +316,25 @@ protected function configure() /** * Configure session ID length * - * To make life easier, we used to force SHA-1 and 4 bits per - * character on everyone. And of course, someone was unhappy. - * - * Then PHP 7.1 broke backwards-compatibility because ext/session - * is such a mess that nobody wants to touch it with a pole stick, - * and the one guy who does, nobody has the energy to argue with. - * - * So we were forced to make changes, and OF COURSE something was - * going to break and now we have this pile of shit. -- Narf + * To make life easier, we force the PHP defaults. Because PHP9 forces them. + * See https://wiki.php.net/rfc/deprecations_php_8_4#sessionsid_length_and_sessionsid_bits_per_character */ protected function configureSidLength() { - $bitsPerCharacter = (int) (ini_get('session.sid_bits_per_character') !== false - ? ini_get('session.sid_bits_per_character') - : 4); - - $sidLength = (int) (ini_get('session.sid_length') !== false - ? ini_get('session.sid_length') - : 40); - - if (($sidLength * $bitsPerCharacter) < 160) { - $bits = ($sidLength * $bitsPerCharacter); - // Add as many more characters as necessary to reach at least 160 bits - $sidLength += (int) ceil((160 % $bits) / $bitsPerCharacter); - ini_set('session.sid_length', (string) $sidLength); - } + $bitsPerCharacter = (int) ini_get('session.sid_bits_per_character'); + $sidLength = (int) ini_get('session.sid_length'); - // Yes, 4,5,6 are the only known possible values as of 2016-10-27 - switch ($bitsPerCharacter) { - case 4: - $this->sidRegexp = '[0-9a-f]'; - break; - - case 5: - $this->sidRegexp = '[0-9a-v]'; - break; - - case 6: - $this->sidRegexp = '[0-9a-zA-Z,-]'; - break; + // We force the PHP defaults. + if (PHP_VERSION_ID < 90000) { + if ($bitsPerCharacter !== 4) { + ini_set('session.sid_bits_per_character', '4'); + } + if ($sidLength !== 32) { + ini_set('session.sid_length', '32'); + } } - $this->sidRegexp .= '{' . $sidLength . '}'; + $this->sidRegexp = '[0-9a-f]{32}'; } /** From 703c2730fe9c5931aa5a7d1dc2c253a46473b3bc Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 27 Aug 2024 14:14:07 +0900 Subject: [PATCH 224/277] docs: add changelog and upgrade --- user_guide_src/source/changelogs/v4.6.0.rst | 7 +++++++ .../source/installation/upgrade_460.rst | 21 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index 4c34f05be882..d3dc4529d512 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -90,6 +90,13 @@ environment, this behavior has been fixed so that error details are displayed if With this fix, the error details are now displayed under the same conditions for both HTML requests and non-HTML requests. +Session ID (SID) +---------------- + +Now ``Session`` library forces to use the PHP default 32 character SIDs, with 4 +bits of entropy per character. +See :ref:`Upgrading Guide ` for details. + .. _v460-interface-changes: Interface Changes diff --git a/user_guide_src/source/installation/upgrade_460.rst b/user_guide_src/source/installation/upgrade_460.rst index da0875849156..a06729a30f29 100644 --- a/user_guide_src/source/installation/upgrade_460.rst +++ b/user_guide_src/source/installation/upgrade_460.rst @@ -126,6 +126,27 @@ The following is an example of code that will no longer work: .. literalinclude:: upgrade_460/001.php +.. _upgrade-460-sid-change: + +Session ID (SID) Change +======================= + +Now :doc:`../libraries/sessions` forces to use the PHP default 32 character SIDs, +with 4 bits of entropy per character. This change is to match the behavior of +PHP 9. + +In other words, the following settings are always used: + +.. code-block:: ini + + session.sid_bits_per_character = 4 + session.sid_length = 32 + +In previous versions, the PHP ini settings was respected. So this change may +change your SID length. + +If you cannot accept this change, customize the Session library. + Interface Changes ================= From ba1702f147a86f2b2694ae5fa7981a0854234e1c Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 22 Aug 2024 10:55:32 +0900 Subject: [PATCH 225/277] feat: add workround for implicit nullable deprecation errors in PHP 8.4 --- system/Debug/Exceptions.php | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php index fac10b32114d..a88ccc550ae0 100644 --- a/system/Debug/Exceptions.php +++ b/system/Debug/Exceptions.php @@ -213,6 +213,10 @@ public function errorHandler(int $severity, string $message, ?string $file = nul return true; } + if ($this->isImplicitNullableDeprecationError($message, $file, $line)) { + return true; + } + if (! $this->config->logDeprecations || (bool) env('CODEIGNITER_SCREAM_DEPRECATIONS')) { throw new ErrorException($message, 0, $severity, $file, $line); } @@ -253,6 +257,34 @@ private function isSessionSidDeprecationError(string $message, ?string $file = n return false; } + /** + * Workaround to implicit nullable deprecation errors in PHP 8.4. + * + * "Implicitly marking parameter $xxx as nullable is deprecated, + * the explicit nullable type must be used instead" + */ + private function isImplicitNullableDeprecationError(string $message, ?string $file = null, ?int $line = null): bool + { + if ( + PHP_VERSION_ID >= 80400 + && str_contains($message, 'the explicit nullable type must be used instead') + ) { + log_message( + LogLevel::WARNING, + '[DEPRECATED] {message} in {errFile} on line {errLine}.', + [ + 'message' => $message, + 'errFile' => clean_path($file ?? ''), + 'errLine' => $line ?? 0, + ] + ); + + return true; + } + + return false; + } + /** * Checks to see if any errors have happened during shutdown that * need to be caught and handle them. From 2eeb9717700a9df07f4446c140d1d63628be5eb0 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 6 Sep 2024 08:57:38 +0900 Subject: [PATCH 226/277] fix: only Faker/Kint errors are skipped This is a workaround for the dependent packages that are not compatible with PHP 8.4. So we should not skip all implicit nullable deprecations. --- system/Debug/Exceptions.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php index a88ccc550ae0..2c88bf695985 100644 --- a/system/Debug/Exceptions.php +++ b/system/Debug/Exceptions.php @@ -262,12 +262,16 @@ private function isSessionSidDeprecationError(string $message, ?string $file = n * * "Implicitly marking parameter $xxx as nullable is deprecated, * the explicit nullable type must be used instead" + * + * @TODO remove this before v4.6.0 release */ private function isImplicitNullableDeprecationError(string $message, ?string $file = null, ?int $line = null): bool { if ( PHP_VERSION_ID >= 80400 && str_contains($message, 'the explicit nullable type must be used instead') + // Only Kint and Faker, which cause this error, are logged. + && (str_starts_with($message, 'Kint\\') || str_starts_with($message, 'Faker\\')) ) { log_message( LogLevel::WARNING, From 052520f1dca1951f065ebebadab4a7497c6df1d0 Mon Sep 17 00:00:00 2001 From: Denny Septian Panggabean Date: Fri, 23 Aug 2024 15:37:46 +0700 Subject: [PATCH 227/277] feat: support CURL HTTP3 --- system/HTTP/CURLRequest.php | 6 ++++++ tests/system/HTTP/CURLRequestTest.php | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php index dadc1eff34ea..92ab3946b90c 100644 --- a/system/HTTP/CURLRequest.php +++ b/system/HTTP/CURLRequest.php @@ -658,6 +658,12 @@ protected function setCURLOptions(array $curlOptions = [], array $config = []) $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1; } elseif ($version === '2.0') { $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0; + } elseif ($version === '3.0') { + if (! defined('CURL_HTTP_VERSION_3')) { + define('CURL_HTTP_VERSION_3', 30); + } + + $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_3; } } diff --git a/tests/system/HTTP/CURLRequestTest.php b/tests/system/HTTP/CURLRequestTest.php index f14198be2264..d824d82831ec 100644 --- a/tests/system/HTTP/CURLRequestTest.php +++ b/tests/system/HTTP/CURLRequestTest.php @@ -1156,6 +1156,18 @@ public function testHTTPv2(): void $this->assertSame(CURL_HTTP_VERSION_2_0, $options[CURLOPT_HTTP_VERSION]); } + public function testHTTPv3(): void + { + $this->request->request('POST', '/post', [ + 'version' => 3.0, + ]); + + $options = $this->request->curl_options; + + $this->assertArrayHasKey(CURLOPT_HTTP_VERSION, $options); + $this->assertSame(CURL_HTTP_VERSION_3, $options[CURLOPT_HTTP_VERSION]); + } + public function testCookieOption(): void { $holder = SUPPORTPATH . 'HTTP/Files/CookiesHolder.txt'; From 484c3115ecbf9d4d480487b930ad0f573a88fd29 Mon Sep 17 00:00:00 2001 From: Denny Septian Panggabean Date: Tue, 10 Sep 2024 11:34:29 +0700 Subject: [PATCH 228/277] test: dynamiclly constant --- tests/system/HTTP/CURLRequestTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/system/HTTP/CURLRequestTest.php b/tests/system/HTTP/CURLRequestTest.php index d824d82831ec..32415c86d3ee 100644 --- a/tests/system/HTTP/CURLRequestTest.php +++ b/tests/system/HTTP/CURLRequestTest.php @@ -1164,6 +1164,10 @@ public function testHTTPv3(): void $options = $this->request->curl_options; + if (! defined('CURL_HTTP_VERSION_3')) { + define('CURL_HTTP_VERSION_3', 30); + } + $this->assertArrayHasKey(CURLOPT_HTTP_VERSION, $options); $this->assertSame(CURL_HTTP_VERSION_3, $options[CURLOPT_HTTP_VERSION]); } From 255a8678a58e7793fbf9f4ae1fde321d6b4cbe03 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Sat, 14 Sep 2024 10:06:39 +0300 Subject: [PATCH 229/277] fix: rector notify --- system/Commands/Utilities/FilterCheck.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Commands/Utilities/FilterCheck.php b/system/Commands/Utilities/FilterCheck.php index bb3811fc0ce8..1536cef0602e 100644 --- a/system/Commands/Utilities/FilterCheck.php +++ b/system/Commands/Utilities/FilterCheck.php @@ -167,7 +167,7 @@ private function showTable( */ private function colorItems(array $array): array { - return array_map(function ($item) { + return array_map(function ($item): array|string { if (is_array($item)) { return $this->colorItems($item); } From 8ab8332fb4137405f5962356762e2e345297d838 Mon Sep 17 00:00:00 2001 From: Denny Septian Panggabean Date: Mon, 28 Oct 2024 12:01:51 +0700 Subject: [PATCH 230/277] feat: design info environment top in error_exception --- app/Views/errors/html/debug.css | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/Views/errors/html/debug.css b/app/Views/errors/html/debug.css index 6a050c8bb462..ca2dadbafde4 100644 --- a/app/Views/errors/html/debug.css +++ b/app/Views/errors/html/debug.css @@ -41,6 +41,7 @@ p.lead { .header { background: var(--light-bg-color); color: var(--dark-text-color); + margin-top: 2.17rem; } .header .container { padding: 1rem; @@ -65,10 +66,13 @@ p.lead { } .environment { - background: var(--dark-bg-color); - color: var(--light-text-color); + background: var(--brand-primary-color); + color: var(--main-bg-color); text-align: center; - padding: 0.2rem; + padding: calc(4px + 0.2083vw); + margin-top: -2.14rem; + width: 100%; + /* position: fixed; */ } .source { From 8ee36a541d550eb06c39538aed155bfe500a33b8 Mon Sep 17 00:00:00 2001 From: Denny Septian Panggabean Date: Mon, 28 Oct 2024 12:14:30 +0700 Subject: [PATCH 231/277] refactor: set position fixed --- app/Views/errors/html/debug.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Views/errors/html/debug.css b/app/Views/errors/html/debug.css index ca2dadbafde4..b20f36a53ce2 100644 --- a/app/Views/errors/html/debug.css +++ b/app/Views/errors/html/debug.css @@ -70,9 +70,9 @@ p.lead { color: var(--main-bg-color); text-align: center; padding: calc(4px + 0.2083vw); - margin-top: -2.14rem; width: 100%; - /* position: fixed; */ + margin-top: -2.14rem; + position: fixed; } .source { From fff0c87664d0947014c6db9a698e97cd906f1046 Mon Sep 17 00:00:00 2001 From: Denny Septian Panggabean <97607754+ddevsr@users.noreply.github.com> Date: Tue, 5 Nov 2024 14:14:10 +0700 Subject: [PATCH 232/277] refactor: bump `laminas/laminas-escaper` to `v2.14` minimum required (#9254) * refactor: dump laminas/laminas-escaper to v2.14 minimum required * refactor: dump laminas/laminas-escaper in framework --- admin/framework/composer.json | 2 +- composer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/admin/framework/composer.json b/admin/framework/composer.json index 79fc5c282e50..b507710c04b0 100644 --- a/admin/framework/composer.json +++ b/admin/framework/composer.json @@ -13,7 +13,7 @@ "php": "^8.1", "ext-intl": "*", "ext-mbstring": "*", - "laminas/laminas-escaper": "^2.13", + "laminas/laminas-escaper": "^2.14", "psr/log": "^3.0" }, "require-dev": { diff --git a/composer.json b/composer.json index f4609fb73d6e..2410b37e84e2 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "php": "^8.1", "ext-intl": "*", "ext-mbstring": "*", - "laminas/laminas-escaper": "^2.13", + "laminas/laminas-escaper": "^2.14", "psr/log": "^3.0" }, "require-dev": { From 857673b3c308b7d8d1b2f25a76946cf5135a77e3 Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Sun, 17 Nov 2024 13:12:13 +0100 Subject: [PATCH 233/277] fix: code issues after merging develop (#9284) * cs-fix * fix services call * apply rector rules --- system/Commands/Utilities/Routes/FilterCollector.php | 4 ++-- system/Validation/StrictRules/FileRules.php | 2 +- tests/system/Database/Live/MySQLi/FoundRowsTest.php | 12 ++++++------ tests/system/Router/RouteCollectionTest.php | 4 ++-- tests/system/View/ViewTest.php | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/system/Commands/Utilities/Routes/FilterCollector.php b/system/Commands/Utilities/Routes/FilterCollector.php index 5a4e3d186940..17395d2f096a 100644 --- a/system/Commands/Utilities/Routes/FilterCollector.php +++ b/system/Commands/Utilities/Routes/FilterCollector.php @@ -109,7 +109,7 @@ public function getClasses(string $method, string $uri): array ]; } - $request = Services::incomingrequest(null, false); + $request = service('incomingrequest', null, false); $request->setMethod($method); $router = $this->createRouter($request); @@ -145,7 +145,7 @@ public function getRequiredFilters(): array */ public function getRequiredFilterClasses(): array { - $request = Services::incomingrequest(null, false); + $request = service('incomingrequest', null, false); $request->setMethod(Method::GET); $router = $this->createRouter($request); diff --git a/system/Validation/StrictRules/FileRules.php b/system/Validation/StrictRules/FileRules.php index e808d4b2a382..9176716ef561 100644 --- a/system/Validation/StrictRules/FileRules.php +++ b/system/Validation/StrictRules/FileRules.php @@ -38,7 +38,7 @@ class FileRules */ public function __construct(?RequestInterface $request = null) { - if ($request === null) { + if (! $request instanceof RequestInterface) { $request = service('request'); } diff --git a/tests/system/Database/Live/MySQLi/FoundRowsTest.php b/tests/system/Database/Live/MySQLi/FoundRowsTest.php index d5b88b49af28..b39f8999085d 100644 --- a/tests/system/Database/Live/MySQLi/FoundRowsTest.php +++ b/tests/system/Database/Live/MySQLi/FoundRowsTest.php @@ -81,7 +81,7 @@ public function testAffectedRowsAfterEnableFoundRowsWithNoChange(): void $affectedRows = $db1->affectedRows(); - $this->assertSame($affectedRows, 2); + $this->assertSame(2, $affectedRows); } public function testAffectedRowsAfterDisableFoundRowsWithNoChange(): void @@ -97,7 +97,7 @@ public function testAffectedRowsAfterDisableFoundRowsWithNoChange(): void $affectedRows = $db1->affectedRows(); - $this->assertSame($affectedRows, 0); + $this->assertSame(0, $affectedRows); } public function testAffectedRowsAfterEnableFoundRowsWithChange(): void @@ -113,7 +113,7 @@ public function testAffectedRowsAfterEnableFoundRowsWithChange(): void $affectedRows = $db1->affectedRows(); - $this->assertSame($affectedRows, 2); + $this->assertSame(2, $affectedRows); } public function testAffectedRowsAfterDisableFoundRowsWithChange(): void @@ -129,7 +129,7 @@ public function testAffectedRowsAfterDisableFoundRowsWithChange(): void $affectedRows = $db1->affectedRows(); - $this->assertSame($affectedRows, 2); + $this->assertSame(2, $affectedRows); } public function testAffectedRowsAfterEnableFoundRowsWithPartialChange(): void @@ -145,7 +145,7 @@ public function testAffectedRowsAfterEnableFoundRowsWithPartialChange(): void $affectedRows = $db1->affectedRows(); - $this->assertSame($affectedRows, 2); + $this->assertSame(2, $affectedRows); } public function testAffectedRowsAfterDisableFoundRowsWithPartialChange(): void @@ -161,6 +161,6 @@ public function testAffectedRowsAfterDisableFoundRowsWithPartialChange(): void $affectedRows = $db1->affectedRows(); - $this->assertSame($affectedRows, 1); + $this->assertSame(1, $affectedRows); } } diff --git a/tests/system/Router/RouteCollectionTest.php b/tests/system/Router/RouteCollectionTest.php index cde22e98b3b0..a8d74984d17c 100644 --- a/tests/system/Router/RouteCollectionTest.php +++ b/tests/system/Router/RouteCollectionTest.php @@ -1750,7 +1750,7 @@ public function testRouteMatchingHostMultipleCorrect(): void service('request')->setMethod(Method::GET); $routes = $this->getCollector(); - $router = new Router($routes, Services::request()); + $router = new Router($routes, service('request')); $routes->setDefaultNamespace('App\Controllers'); $routes->setDefaultController('Home'); @@ -1770,7 +1770,7 @@ public function testRouteMatchingHostMultipleFail(): void service('request')->setMethod(Method::GET); $routes = $this->getCollector(); - $router = new Router($routes, Services::request()); + $router = new Router($routes, service('request')); $routes->setDefaultNamespace('App\Controllers'); $routes->setDefaultController('Home'); diff --git a/tests/system/View/ViewTest.php b/tests/system/View/ViewTest.php index 7ec1adcbe80f..206cdc222d27 100644 --- a/tests/system/View/ViewTest.php +++ b/tests/system/View/ViewTest.php @@ -13,8 +13,8 @@ namespace CodeIgniter\View; -use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\Autoloader\FileLocatorInterface; +use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\View\Exceptions\ViewException; use Config; From 86278d9ab2acc5ce73ebfcf97e921bbbfb052cd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amel=20Junuzovi=C4=87?= Date: Sun, 6 Oct 2024 21:03:45 +0200 Subject: [PATCH 234/277] feat: Add optional dbGroup support in validation rules for multiple database connections cs fixer docs format fix rules add test testIsNotUniqueWithDBConnectionAsParameter docs: update v4.5.6.rst docs: fix tabs docs update for v4.6.0 Update validation.rst Update user_guide_src/source/changelogs/v4.6.0.rst Co-authored-by: Michal Sniatala Add a test case for `InvalidArgumentException` and remove the duplicate comment. refactor: code for is_unique and is_not_unique methods; extracted common code into prepareUniqueQuery method rector and phpstan fix --- system/Validation/Rules.php | 90 ++++++++++--------- system/Validation/StrictRules/Rules.php | 53 +---------- .../StrictRules/DatabaseRelatedRulesTest.php | 50 +++++++++++ user_guide_src/source/changelogs/v4.6.0.rst | 2 + .../source/libraries/validation.rst | 11 ++- 5 files changed, 113 insertions(+), 93 deletions(-) diff --git a/system/Validation/Rules.php b/system/Validation/Rules.php index 06586a64190d..30fab3539ecd 100644 --- a/system/Validation/Rules.php +++ b/system/Validation/Rules.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Validation; +use CodeIgniter\Database\BaseBuilder; use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Helpers\Array\ArrayHelper; use Config\Database; @@ -110,6 +111,7 @@ public function greater_than_equal_to($str, string $min): bool * accept only one filter). * * Example: + * is_not_unique[dbGroup.table.field,where_field,where_value] * is_not_unique[table.field,where_field,where_value] * is_not_unique[menu.id,active,1] * @@ -117,35 +119,17 @@ public function greater_than_equal_to($str, string $min): bool */ public function is_not_unique($str, string $field, array $data): bool { - if (! is_string($str) && $str !== null) { - $str = (string) $str; - } - - // Grab any data for exclusion of a single row. - [$field, $whereField, $whereValue] = array_pad( - explode(',', $field), - 3, - null - ); - - // Break the table and field apart - sscanf($field, '%[^.].%[^.]', $table, $field); - - $row = Database::connect($data['DBGroup'] ?? null) - ->table($table) - ->select('1') - ->where($field, $str) - ->limit(1); + [$builder, $whereField, $whereValue] = $this->prepareUniqueQuery($str, $field, $data); if ( $whereField !== null && $whereField !== '' && $whereValue !== null && $whereValue !== '' && ! preg_match('/^\{(\w+)\}$/', $whereValue) ) { - $row = $row->where($whereField, $whereValue); + $builder = $builder->where($whereField, $whereValue); } - return $row->get()->getRow() !== null; + return $builder->get()->getRow() !== null; } /** @@ -170,6 +154,7 @@ public function in_list($value, string $list): bool * record updates. * * Example: + * is_unique[dbGroup.table.field,ignore_field,ignore_value] * is_unique[table.field,ignore_field,ignore_value] * is_unique[users.email,id,5] * @@ -177,33 +162,58 @@ public function in_list($value, string $list): bool */ public function is_unique($str, string $field, array $data): bool { - if (! is_string($str) && $str !== null) { - $str = (string) $str; - } - - [$field, $ignoreField, $ignoreValue] = array_pad( - explode(',', $field), - 3, - null - ); - - sscanf($field, '%[^.].%[^.]', $table, $field); - - $row = Database::connect($data['DBGroup'] ?? null) - ->table($table) - ->select('1') - ->where($field, $str) - ->limit(1); + [$builder, $ignoreField, $ignoreValue] = $this->prepareUniqueQuery($str, $field, $data); if ( $ignoreField !== null && $ignoreField !== '' && $ignoreValue !== null && $ignoreValue !== '' && ! preg_match('/^\{(\w+)\}$/', $ignoreValue) ) { - $row = $row->where("{$ignoreField} !=", $ignoreValue); + $builder = $builder->where("{$ignoreField} !=", $ignoreValue); } - return $row->get()->getRow() === null; + return $builder->get()->getRow() === null; + } + + /** + * Prepares the database query for uniqueness checks. + * + * @param mixed $value The value to check. + * @param string $field The field parameters. + * @param array $data Additional data. + * + * @return array{0: BaseBuilder, 1: string|null, 2: string|null} + */ + private function prepareUniqueQuery($value, string $field, array $data): array + { + if (! is_string($value) && $value !== null) { + $value = (string) $value; + } + + // Split the field parameters and pad the array to ensure three elements. + [$field, $extraField, $extraValue] = array_pad(explode(',', $field), 3, null); + + // Parse the field string to extract dbGroup, table, and field. + $parts = explode('.', $field, 3); + $numParts = count($parts); + + if ($numParts === 3) { + [$dbGroup, $table, $field] = $parts; + } elseif ($numParts === 2) { + [$table, $field] = $parts; + } else { + throw new InvalidArgumentException('The field must be in the format "table.field" or "dbGroup.table.field".'); + } + + // Connect to the database. + $dbGroup ??= $data['DBGroup'] ?? null; + $builder = Database::connect($dbGroup) + ->table($table) + ->select('1') + ->where($field, $value) + ->limit(1); + + return [$builder, $extraField, $extraValue]; } /** diff --git a/system/Validation/StrictRules/Rules.php b/system/Validation/StrictRules/Rules.php index ec02a4e0a0ac..e836f4e4c127 100644 --- a/system/Validation/StrictRules/Rules.php +++ b/system/Validation/StrictRules/Rules.php @@ -15,7 +15,6 @@ use CodeIgniter\Helpers\Array\ArrayHelper; use CodeIgniter\Validation\Rules as NonStrictRules; -use Config\Database; /** * Validation Rules. @@ -134,6 +133,7 @@ public function greater_than_equal_to($str, string $min): bool * accept only one filter). * * Example: + * is_not_unique[dbGroup.table.field,where_field,where_value] * is_not_unique[table.field,where_field,where_value] * is_not_unique[menu.id,active,1] * @@ -145,31 +145,7 @@ public function is_not_unique($str, string $field, array $data): bool return false; } - // Grab any data for exclusion of a single row. - [$field, $whereField, $whereValue] = array_pad( - explode(',', $field), - 3, - null - ); - - // Break the table and field apart - sscanf($field, '%[^.].%[^.]', $table, $field); - - $row = Database::connect($data['DBGroup'] ?? null) - ->table($table) - ->select('1') - ->where($field, $str) - ->limit(1); - - if ( - $whereField !== null && $whereField !== '' - && $whereValue !== null && $whereValue !== '' - && ! preg_match('/^\{(\w+)\}$/', $whereValue) - ) { - $row = $row->where($whereField, $whereValue); - } - - return $row->get()->getRow() !== null; + return $this->nonStrictRules->is_not_unique($str, $field, $data); } /** @@ -196,6 +172,7 @@ public function in_list($value, string $list): bool * record updates. * * Example: + * is_unique[dbGroup.table.field,ignore_field,ignore_value] * is_unique[table.field,ignore_field,ignore_value] * is_unique[users.email,id,5] * @@ -207,29 +184,7 @@ public function is_unique($str, string $field, array $data): bool return false; } - [$field, $ignoreField, $ignoreValue] = array_pad( - explode(',', $field), - 3, - null - ); - - sscanf($field, '%[^.].%[^.]', $table, $field); - - $row = Database::connect($data['DBGroup'] ?? null) - ->table($table) - ->select('1') - ->where($field, $str) - ->limit(1); - - if ( - $ignoreField !== null && $ignoreField !== '' - && $ignoreValue !== null && $ignoreValue !== '' - && ! preg_match('/^\{(\w+)\}$/', $ignoreValue) - ) { - $row = $row->where("{$ignoreField} !=", $ignoreValue); - } - - return $row->get()->getRow() === null; + return $this->nonStrictRules->is_unique($str, $field, $data); } /** diff --git a/tests/system/Validation/StrictRules/DatabaseRelatedRulesTest.php b/tests/system/Validation/StrictRules/DatabaseRelatedRulesTest.php index d6e87a16f45d..83f45514682c 100644 --- a/tests/system/Validation/StrictRules/DatabaseRelatedRulesTest.php +++ b/tests/system/Validation/StrictRules/DatabaseRelatedRulesTest.php @@ -96,6 +96,29 @@ public function testIsUniqueWithDBConnection(): void $this->assertTrue($result); } + public function testIsUniqueWithDBConnectionAsParameter(): void + { + $this->validation->setRules(['email' => 'is_unique[tests.user.email]']); + + $data = ['email' => 'derek@world.co.uk']; + + $result = $this->validation->run($data); + + $this->assertTrue($result); + } + + public function testIsUniqueWrongParametersThrowInvalidArgumentException(): void + { + $this->validation->setRules(['email' => 'is_unique[invalid_parameters]']); + + $data = ['email' => 'derek@world.co.uk']; + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The field must be in the format "table.field" or "dbGroup.table.field".'); + + $this->validation->run($data); + } + public function testIsUniqueWithInvalidDBGroup(): void { $this->expectException(InvalidArgumentException::class); @@ -295,4 +318,31 @@ public function testIsNotUniqueByManualRun(): void $this->assertTrue($this->createRules()->is_not_unique('deva@example.com', 'user.email,id,{id}', [])); } + + public function testIsNotUniqueWithDBConnectionAsParameter(): void + { + Database::connect() + ->table('user') + ->insert([ + 'name' => 'Derek Travis', + 'email' => 'derek@world.com', + 'country' => 'Elbonia', + ]); + + $data = ['email' => 'derek@world.com']; + $this->validation->setRules(['email' => 'is_not_unique[tests.user.email]']); + $this->assertTrue($this->validation->run($data)); + } + + public function testIsNotUniqueWrongParametersThrowInvalidArgumentException(): void + { + $this->validation->setRules(['email' => 'is_not_unique[invalid_parameters]']); + + $data = ['email' => 'derek@world.co.uk']; + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The field must be in the format "table.field" or "dbGroup.table.field".'); + + $this->validation->run($data); + } } diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index d3dc4529d512..0a3ed22fa37d 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -219,6 +219,8 @@ Libraries See :ref:`FileCollection::retainMultiplePatterns() `. - **Validation:** Added ``min_dims`` validation rule to ``FileRules`` class. See :ref:`Validation `. +- **Validation:** Rules: ``is_unique`` and ``is_not_unique`` now accept the optional + ``dbGroup`` as part of the first parameter. See :ref:`Validation `. Helpers and Functions ===================== diff --git a/user_guide_src/source/libraries/validation.rst b/user_guide_src/source/libraries/validation.rst index 25111c2cdcef..96416355bb22 100644 --- a/user_guide_src/source/libraries/validation.rst +++ b/user_guide_src/source/libraries/validation.rst @@ -949,13 +949,16 @@ is_natural No Fails if field contains anything other than is_natural_no_zero No Fails if field contains anything other than a natural number, except zero: ``1``, ``2``, ``3``, etc. -is_not_unique Yes Checks the database to see if the given value ``is_not_unique[table.field,where_field,where_value]`` +is_not_unique Yes Checks the database to see if the given value ``is_not_unique[table.field,where_field,where_value]`` or ``is_not_unique[dbGroup.table.field,where_field,where_value]`` exists. Can ignore records by field/value to filter (currently accept only one filter). -is_unique Yes Checks if this field value exists in the ``is_unique[table.field,ignore_field,ignore_value]`` + (Since v4.6.0, you can optionally pass + the dbGroup as a parameter) +is_unique Yes Checks if this field value exists in the ``is_unique[table.field,ignore_field,ignore_value]`` or ``is_unique[dbGroup.table.field,ignore_field,ignore_value]`` database. Optionally set a column and value to ignore, useful when updating records to - ignore itself. + ignore itself. (Since v4.6.0, you can + optionally pass the dbGroup as a parameter) less_than Yes Fails if field is greater than or equal to ``less_than[8]`` the parameter value or not numeric. less_than_equal_to Yes Fails if field is greater than the parameter ``less_than_equal_to[8]`` @@ -1094,7 +1097,7 @@ min_dims Yes Fails if the minimum width and height of an parameter is the field name. The second is the width, and the third is the height. Will also fail if the file cannot be determined - to be an image. (This rule was added in + to be an image. (This rule was added in v4.6.0.) mime_in Yes Fails if the file's mime type is not one ``mime_in[field_name,image/png,image/jpeg]`` listed in the parameters. From bcedf1ccfc4e96d689f10dcec584ec1600295373 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu <37987162+dimtrovich@users.noreply.github.com> Date: Sun, 1 Dec 2024 20:31:46 +0100 Subject: [PATCH 235/277] feat: added the `namespace` option to the `publish` command (#9278) --- system/Commands/Utilities/Publish.php | 15 ++++++++--- system/Language/en/Publisher.php | 7 +++--- system/Publisher/Publisher.php | 22 ++++++++++------ .../system/Publisher/PublisherSupportTest.php | 14 +++++++++++ user_guide_src/source/changelogs/v4.6.0.rst | 5 ++++ user_guide_src/source/libraries/publisher.rst | 25 +++++++++++++++++++ .../source/libraries/publisher/016.php | 5 ++++ 7 files changed, 78 insertions(+), 15 deletions(-) create mode 100644 user_guide_src/source/libraries/publisher/016.php diff --git a/system/Commands/Utilities/Publish.php b/system/Commands/Utilities/Publish.php index 3e19685cba81..13ffa96eafc6 100644 --- a/system/Commands/Utilities/Publish.php +++ b/system/Commands/Utilities/Publish.php @@ -67,17 +67,24 @@ class Publish extends BaseCommand * * @var array */ - protected $options = []; + protected $options = [ + '--namespace' => 'The namespace from which to search for files to publish. By default, all namespaces are analysed.', + ]; /** * Displays the help for the spark cli script itself. */ public function run(array $params) { - $directory = array_shift($params) ?? 'Publishers'; + $directory = $params[0] ?? 'Publishers'; + $namespace = $params['namespace'] ?? ''; - if ([] === $publishers = Publisher::discover($directory)) { - CLI::write(lang('Publisher.publishMissing', [$directory])); + if ([] === $publishers = Publisher::discover($directory, $namespace)) { + if ($namespace === '') { + CLI::write(lang('Publisher.publishMissing', [$directory])); + } else { + CLI::write(lang('Publisher.publishMissingNamespace', [$directory, $namespace])); + } return; } diff --git a/system/Language/en/Publisher.php b/system/Language/en/Publisher.php index eb18dfebd555..c2d6d51ec900 100644 --- a/system/Language/en/Publisher.php +++ b/system/Language/en/Publisher.php @@ -18,7 +18,8 @@ '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}".', + 'publishMissing' => 'No Publisher classes detected in {0} across all namespaces.', + 'publishMissingNamespace' => 'No Publisher classes detected in {0} in the {1} namespace.', + 'publishSuccess' => '"{0}" published {1} file(s) to "{2}".', + 'publishFailure' => '"{0}" failed to publish to "{1}".', ]; diff --git a/system/Publisher/Publisher.php b/system/Publisher/Publisher.php index bf81f6079b60..d65765947db1 100644 --- a/system/Publisher/Publisher.php +++ b/system/Publisher/Publisher.php @@ -99,18 +99,24 @@ class Publisher extends FileCollection * * @return list */ - final public static function discover(string $directory = 'Publishers'): array + final public static function discover(string $directory = 'Publishers', string $namespace = ''): array { - if (isset(self::$discovered[$directory])) { - return self::$discovered[$directory]; + $key = implode('.', [$namespace, $directory]); + + if (isset(self::$discovered[$key])) { + return self::$discovered[$key]; } - self::$discovered[$directory] = []; + self::$discovered[$key] = []; /** @var FileLocatorInterface $locator */ $locator = service('locator'); - if ([] === $files = $locator->listFiles($directory)) { + $files = $namespace === '' + ? $locator->listFiles($directory) + : $locator->listNamespaceFiles($namespace, $directory); + + if ([] === $files) { return []; } @@ -119,13 +125,13 @@ final public static function discover(string $directory = 'Publishers'): array $className = $locator->findQualifiedNameFromPath($file); if ($className !== false && class_exists($className) && is_a($className, self::class, true)) { - self::$discovered[$directory][] = new $className(); + self::$discovered[$key][] = new $className(); } } - sort(self::$discovered[$directory]); + sort(self::$discovered[$key]); - return self::$discovered[$directory]; + return self::$discovered[$key]; } /** diff --git a/tests/system/Publisher/PublisherSupportTest.php b/tests/system/Publisher/PublisherSupportTest.php index 5854e1c516d9..e92bdd0c69c3 100644 --- a/tests/system/Publisher/PublisherSupportTest.php +++ b/tests/system/Publisher/PublisherSupportTest.php @@ -61,6 +61,20 @@ public function testDiscoverNothing(): void $this->assertSame([], $result); } + public function testDiscoverInNamespace(): void + { + $result = Publisher::discover('Publishers', 'Tests\Support'); + $this->assertCount(1, $result); + $this->assertInstanceOf(TestPublisher::class, $result[0]); + } + + public function testDiscoverInUnknowNamespace(): void + { + $result = Publisher::discover('Publishers', 'Nothing\App'); + + $this->assertSame([], $result); + } + public function testDiscoverStores(): void { $publisher = Publisher::discover()[0]; diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index 0a3ed22fa37d..f742398aac74 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -160,6 +160,11 @@ Removed Deprecated Items Enhancements ************ +Publisher +========= + +- ``Publisher::discover()`` now accepts a second parameter (``namespace``) specifying the namespace in which publishers should be searched. See :ref:`discovery-in-a-specific-namespace` for the details. + Exceptions ========== diff --git a/user_guide_src/source/libraries/publisher.rst b/user_guide_src/source/libraries/publisher.rst index 99531eacca5b..69b5df6c9f0c 100644 --- a/user_guide_src/source/libraries/publisher.rst +++ b/user_guide_src/source/libraries/publisher.rst @@ -75,6 +75,31 @@ Most of the time you will not need to handle your own discovery, just use the pr By default on your class extension ``publish()`` will add all files from your ``$source`` and merge them out to your destination, overwriting on collision. +.. _discovery-in-a-specific-namespace: + +Discovery in a specific namespace +--------------------------------- + +.. versionadded:: 4.6.0 + +Since v4.6.0, you can also scan a specific namespace. This not only reduces the number of files to be scanned, +but also avoids the need to rerun a Publisher. All you need to do is specify the desired root namespace in the +second parameter of the ``discover()`` method. + +.. literalinclude:: publisher/016.php + +The specified namespace must be known to CodeIgniter. You can check the list of all namespaces using the "spark namespaces" command: + +.. code-block:: console + + php spark namespaces + +The "publish" command also offers the ``--namespace`` option to define the namespace when searching for Publishers that might come from a library. + +.. code-block:: console + + php spark publish --namespace Namespace\Vendor\Package + Security ======== diff --git a/user_guide_src/source/libraries/publisher/016.php b/user_guide_src/source/libraries/publisher/016.php new file mode 100644 index 000000000000..54f2beca5701 --- /dev/null +++ b/user_guide_src/source/libraries/publisher/016.php @@ -0,0 +1,5 @@ + Date: Mon, 2 Dec 2024 13:17:23 +0700 Subject: [PATCH 236/277] chore: update `Kint` to v6.0 (#9289) * feat: update kint to v6.0 * fix: run cs --- admin/framework/composer.json | 2 +- app/Config/Kint.php | 2 - composer.json | 2 +- system/Autoloader/Autoloader.php | 2 +- system/CodeIgniter.php | 2 +- system/ThirdParty/Kint/CallFinder.php | 211 ++++-- system/ThirdParty/Kint/FacadeInterface.php | 6 +- system/ThirdParty/Kint/Kint.php | 289 +++----- .../ThirdParty/Kint/Parser/AbstractPlugin.php | 13 +- .../Kint/Parser/ArrayLimitPlugin.php | 137 ++-- .../Kint/Parser/ArrayObjectPlugin.php | 17 +- .../ThirdParty/Kint/Parser/Base64Plugin.php | 53 +- .../ThirdParty/Kint/Parser/BinaryPlugin.php | 15 +- .../Kint/Parser/BlacklistPlugin.php | 51 +- .../Kint/Parser/ClassHooksPlugin.php | 122 ++++ .../Kint/Parser/ClassMethodsPlugin.php | 204 ++++-- .../Kint/Parser/ClassStaticsPlugin.php | 218 ++++-- .../Kint/Parser/ClassStringsPlugin.php | 102 +++ .../ThirdParty/Kint/Parser/ClosurePlugin.php | 61 +- system/ThirdParty/Kint/Parser/ColorPlugin.php | 37 +- .../Parser/ConstructablePluginInterface.php | 2 +- .../Kint/Parser/DOMDocumentPlugin.php | 356 --------- .../ThirdParty/Kint/Parser/DateTimePlugin.php | 30 +- system/ThirdParty/Kint/Parser/DomPlugin.php | 528 ++++++++++++++ system/ThirdParty/Kint/Parser/EnumPlugin.php | 48 +- .../ThirdParty/Kint/Parser/FsPathPlugin.php | 28 +- system/ThirdParty/Kint/Parser/HtmlPlugin.php | 86 +++ .../ThirdParty/Kint/Parser/IteratorPlugin.php | 109 ++- system/ThirdParty/Kint/Parser/JsonPlugin.php | 47 +- .../Kint/Parser/MicrotimePlugin.php | 62 +- .../ThirdParty/Kint/Parser/MysqliPlugin.php | 138 ++-- system/ThirdParty/Kint/Parser/Parser.php | 682 ++++++++---------- .../Kint/Parser/PluginBeginInterface.php | 39 + .../Kint/Parser/PluginCompleteInterface.php | 42 ++ .../Kint/Parser/PluginInterface.php | 11 +- .../ThirdParty/Kint/Parser/ProfilePlugin.php | 174 +++++ system/ThirdParty/Kint/Parser/ProxyPlugin.php | 45 +- .../Kint/Parser/SerializePlugin.php | 64 +- .../Kint/Parser/SimpleXMLElementPlugin.php | 318 ++++---- .../Kint/Parser/SplFileInfoPlugin.php | 29 +- .../ThirdParty/Kint/Parser/StreamPlugin.php | 57 +- system/ThirdParty/Kint/Parser/TablePlugin.php | 56 +- .../Kint/Parser/ThrowablePlugin.php | 32 +- .../Kint/Parser/TimestampPlugin.php | 40 +- .../ThirdParty/Kint/Parser/ToStringPlugin.php | 43 +- system/ThirdParty/Kint/Parser/TracePlugin.php | 102 ++- system/ThirdParty/Kint/Parser/XmlPlugin.php | 141 ++-- .../Kint/Renderer/AbstractRenderer.php | 130 +--- .../AssetRendererTrait.php} | 54 +- .../ThirdParty/Kint/Renderer/CliRenderer.php | 55 +- ...php => ConstructableRendererInterface.php} | 11 +- .../Kint/Renderer/PlainRenderer.php | 82 +-- .../Kint/Renderer/RendererInterface.php | 16 +- .../Kint/Renderer/Rich/AbstractPlugin.php | 61 +- .../Kint/Renderer/Rich/BinaryPlugin.php | 16 +- ...lugin.php => CallableDefinitionPlugin.php} | 40 +- .../Kint/Renderer/Rich/CallablePlugin.php | 103 +-- .../Kint/Renderer/Rich/ColorPlugin.php | 18 +- .../Kint/Renderer/Rich/LockPlugin.php | 49 ++ .../Kint/Renderer/Rich/MicrotimePlugin.php | 42 +- ...XMLElementPlugin.php => ProfilePlugin.php} | 32 +- .../Kint/Renderer/Rich/SourcePlugin.php | 25 +- .../Kint/Renderer/Rich/TabPluginInterface.php | 5 +- .../Kint/Renderer/Rich/TablePlugin.php | 133 ++-- .../Kint/Renderer/Rich/TraceFramePlugin.php | 34 +- .../Renderer/Rich/ValuePluginInterface.php | 4 +- .../ThirdParty/Kint/Renderer/RichRenderer.php | 430 +++++------ .../Kint/Renderer/Text/AbstractPlugin.php | 15 +- .../Kint/Renderer/Text/LockPlugin.php | 49 ++ .../Kint/Renderer/Text/MicrotimePlugin.php | 54 +- .../Kint/Renderer/Text/PluginInterface.php | 4 +- .../Kint/Renderer/Text/SplFileInfoPlugin.php | 59 ++ .../Kint/Renderer/Text/TracePlugin.php | 64 +- .../ThirdParty/Kint/Renderer/TextRenderer.php | 271 +++---- system/ThirdParty/Kint/Utils.php | 483 ++++++++++--- .../ThirdParty/Kint/Value/AbstractValue.php | 190 +++++ system/ThirdParty/Kint/Value/ArrayValue.php | 71 ++ .../ClosedResourceValue.php} | 15 +- system/ThirdParty/Kint/Value/ClosureValue.php | 120 +++ .../EnumPlugin.php => Value/ColorValue.php} | 10 +- .../Context/ArrayContext.php} | 10 +- .../Kint/Value/Context/BaseContext.php | 83 +++ .../Context/ClassConstContext.php} | 19 +- .../Value/Context/ClassDeclaredContext.php | 94 +++ .../Context/ClassOwnedContext.php} | 33 +- .../Context/ContextInterface.php} | 39 +- .../Context/DoubleAccessMemberContext.php} | 54 +- .../Kint/Value/Context/MethodContext.php | 140 ++++ .../Context/PropertyContext.php} | 70 +- .../Context/StaticPropertyContext.php} | 19 +- .../Kint/{Zval => Value}/DateTimeValue.php | 28 +- .../Kint/Value/DeclaredCallableBag.php | 81 +++ .../DomNodeListValue.php} | 35 +- .../DomNodeValue.php} | 22 +- .../Kint/{Zval => Value}/EnumValue.php | 42 +- .../ThirdParty/Kint/Value/FixedWidthValue.php | 85 +++ .../ThirdParty/Kint/Value/FunctionValue.php | 98 +++ .../ThirdParty/Kint/Value/InstanceValue.php | 111 +++ system/ThirdParty/Kint/Value/MethodValue.php | 130 ++++ .../ThirdParty/Kint/Value/MicrotimeValue.php | 63 ++ .../ParameterBag.php} | 62 +- .../ParameterHoldingTrait.php} | 23 +- .../AbstractRepresentation.php} | 38 +- .../Representation/BinaryRepresentation.php | 50 ++ .../CallableDefinitionRepresentation.php | 157 ++++ .../Representation/ColorRepresentation.php | 147 ++-- .../ContainerRepresentation.php | 63 ++ .../MicrotimeRepresentation.php | 125 ++++ .../Representation/ProfileRepresentation.php | 47 ++ .../RepresentationInterface.php | 39 + .../Representation/SourceRepresentation.php | 83 ++- .../SplFileInfoRepresentation.php | 180 +++++ .../Representation/StringRepresentation.php} | 41 +- .../Representation/TableRepresentation.php | 44 ++ .../Representation/ValueRepresentation.php | 47 ++ .../Kint/{Zval => Value}/ResourceValue.php | 26 +- .../Kint/Value/SimpleXMLElementValue.php | 81 +++ .../Kint/Value/SplFileInfoValue.php | 92 +++ system/ThirdParty/Kint/Value/StreamValue.php | 79 ++ system/ThirdParty/Kint/Value/StringValue.php | 112 +++ .../Kint/{Zval => Value}/ThrowableValue.php | 19 +- .../ThirdParty/Kint/Value/TraceFrameValue.php | 184 +++++ .../Kint/{Zval => Value}/TraceValue.php | 19 +- .../Kint/Value/UninitializedValue.php | 38 + .../UnknownValue.php} | 10 +- .../VirtualValue.php} | 10 +- system/ThirdParty/Kint/Zval/BlobValue.php | 201 ------ system/ThirdParty/Kint/Zval/MethodValue.php | 228 ------ .../MethodDefinitionRepresentation.php | 76 -- .../MicrotimeRepresentation.php | 73 -- .../SplFileInfoRepresentation.php | 196 ----- .../ThirdParty/Kint/Zval/TraceFrameValue.php | 107 --- system/ThirdParty/Kint/Zval/Value.php | 266 ------- system/ThirdParty/Kint/init.php | 18 +- .../Kint/resources/compiled/aante-dark.css | 2 +- .../Kint/resources/compiled/aante-light.css | 2 +- .../Kint/resources/compiled/main.js | 1 + .../Kint/resources/compiled/microtime.js | 1 - .../Kint/resources/compiled/original.css | 2 +- .../Kint/resources/compiled/plain.css | 2 +- .../Kint/resources/compiled/plain.js | 1 - .../Kint/resources/compiled/rich.js | 1 - .../Kint/resources/compiled/shared.js | 1 - .../resources/compiled/solarized-dark.css | 2 +- .../Kint/resources/compiled/solarized.css | 2 +- 145 files changed, 7412 insertions(+), 4442 deletions(-) create mode 100644 system/ThirdParty/Kint/Parser/ClassHooksPlugin.php create mode 100644 system/ThirdParty/Kint/Parser/ClassStringsPlugin.php delete mode 100644 system/ThirdParty/Kint/Parser/DOMDocumentPlugin.php create mode 100644 system/ThirdParty/Kint/Parser/DomPlugin.php create mode 100644 system/ThirdParty/Kint/Parser/HtmlPlugin.php create mode 100644 system/ThirdParty/Kint/Parser/PluginBeginInterface.php create mode 100644 system/ThirdParty/Kint/Parser/PluginCompleteInterface.php create mode 100644 system/ThirdParty/Kint/Parser/ProfilePlugin.php rename system/ThirdParty/Kint/{Zval/InstanceValue.php => Renderer/AssetRendererTrait.php} (55%) rename system/ThirdParty/Kint/Renderer/{Text/DepthLimitPlugin.php => ConstructableRendererInterface.php} (85%) rename system/ThirdParty/Kint/Renderer/Rich/{ClosurePlugin.php => CallableDefinitionPlugin.php} (56%) create mode 100644 system/ThirdParty/Kint/Renderer/Rich/LockPlugin.php rename system/ThirdParty/Kint/Renderer/Rich/{SimpleXMLElementPlugin.php => ProfilePlugin.php} (63%) create mode 100644 system/ThirdParty/Kint/Renderer/Text/LockPlugin.php create mode 100644 system/ThirdParty/Kint/Renderer/Text/SplFileInfoPlugin.php create mode 100644 system/ThirdParty/Kint/Value/AbstractValue.php create mode 100644 system/ThirdParty/Kint/Value/ArrayValue.php rename system/ThirdParty/Kint/{Renderer/Rich/DepthLimitPlugin.php => Value/ClosedResourceValue.php} (79%) create mode 100644 system/ThirdParty/Kint/Value/ClosureValue.php rename system/ThirdParty/Kint/{Renderer/Text/EnumPlugin.php => Value/ColorValue.php} (87%) rename system/ThirdParty/Kint/{Renderer/Text/RecursionPlugin.php => Value/Context/ArrayContext.php} (86%) create mode 100644 system/ThirdParty/Kint/Value/Context/BaseContext.php rename system/ThirdParty/Kint/{Renderer/Rich/RecursionPlugin.php => Value/Context/ClassConstContext.php} (79%) create mode 100644 system/ThirdParty/Kint/Value/Context/ClassDeclaredContext.php rename system/ThirdParty/Kint/{Zval/SimpleXMLElementValue.php => Value/Context/ClassOwnedContext.php} (65%) rename system/ThirdParty/Kint/{Zval/StreamValue.php => Value/Context/ContextInterface.php} (63%) rename system/ThirdParty/Kint/{Zval/ParameterHoldingTrait.php => Value/Context/DoubleAccessMemberContext.php} (59%) create mode 100644 system/ThirdParty/Kint/Value/Context/MethodContext.php rename system/ThirdParty/Kint/{Renderer/Rich/MethodDefinitionPlugin.php => Value/Context/PropertyContext.php} (52%) rename system/ThirdParty/Kint/{Renderer/Rich/ArrayLimitPlugin.php => Value/Context/StaticPropertyContext.php} (78%) rename system/ThirdParty/Kint/{Zval => Value}/DateTimeValue.php (69%) create mode 100644 system/ThirdParty/Kint/Value/DeclaredCallableBag.php rename system/ThirdParty/Kint/{Parser/SplObjectStoragePlugin.php => Value/DomNodeListValue.php} (67%) rename system/ThirdParty/Kint/{Renderer/Rich/TimestampPlugin.php => Value/DomNodeValue.php} (74%) rename system/ThirdParty/Kint/{Zval => Value}/EnumValue.php (75%) create mode 100644 system/ThirdParty/Kint/Value/FixedWidthValue.php create mode 100644 system/ThirdParty/Kint/Value/FunctionValue.php create mode 100644 system/ThirdParty/Kint/Value/InstanceValue.php create mode 100644 system/ThirdParty/Kint/Value/MethodValue.php create mode 100644 system/ThirdParty/Kint/Value/MicrotimeValue.php rename system/ThirdParty/Kint/{Zval/ParameterValue.php => Value/ParameterBag.php} (64%) rename system/ThirdParty/Kint/{Renderer/Rich/BlacklistPlugin.php => Value/ParameterHoldingTrait.php} (78%) rename system/ThirdParty/Kint/{Zval/Representation/Representation.php => Value/Representation/AbstractRepresentation.php} (74%) create mode 100644 system/ThirdParty/Kint/Value/Representation/BinaryRepresentation.php create mode 100644 system/ThirdParty/Kint/Value/Representation/CallableDefinitionRepresentation.php rename system/ThirdParty/Kint/{Zval => Value}/Representation/ColorRepresentation.php (81%) create mode 100644 system/ThirdParty/Kint/Value/Representation/ContainerRepresentation.php create mode 100644 system/ThirdParty/Kint/Value/Representation/MicrotimeRepresentation.php create mode 100644 system/ThirdParty/Kint/Value/Representation/ProfileRepresentation.php create mode 100644 system/ThirdParty/Kint/Value/Representation/RepresentationInterface.php rename system/ThirdParty/Kint/{Zval => Value}/Representation/SourceRepresentation.php (52%) create mode 100644 system/ThirdParty/Kint/Value/Representation/SplFileInfoRepresentation.php rename system/ThirdParty/Kint/{Zval/ClosureValue.php => Value/Representation/StringRepresentation.php} (64%) create mode 100644 system/ThirdParty/Kint/Value/Representation/TableRepresentation.php create mode 100644 system/ThirdParty/Kint/Value/Representation/ValueRepresentation.php rename system/ThirdParty/Kint/{Zval => Value}/ResourceValue.php (74%) create mode 100644 system/ThirdParty/Kint/Value/SimpleXMLElementValue.php create mode 100644 system/ThirdParty/Kint/Value/SplFileInfoValue.php create mode 100644 system/ThirdParty/Kint/Value/StreamValue.php create mode 100644 system/ThirdParty/Kint/Value/StringValue.php rename system/ThirdParty/Kint/{Zval => Value}/ThrowableValue.php (77%) create mode 100644 system/ThirdParty/Kint/Value/TraceFrameValue.php rename system/ThirdParty/Kint/{Zval => Value}/TraceValue.php (79%) create mode 100644 system/ThirdParty/Kint/Value/UninitializedValue.php rename system/ThirdParty/Kint/{Renderer/Text/ArrayLimitPlugin.php => Value/UnknownValue.php} (85%) rename system/ThirdParty/Kint/{Renderer/Text/BlacklistPlugin.php => Value/VirtualValue.php} (85%) delete mode 100644 system/ThirdParty/Kint/Zval/BlobValue.php delete mode 100644 system/ThirdParty/Kint/Zval/MethodValue.php delete mode 100644 system/ThirdParty/Kint/Zval/Representation/MethodDefinitionRepresentation.php delete mode 100644 system/ThirdParty/Kint/Zval/Representation/MicrotimeRepresentation.php delete mode 100644 system/ThirdParty/Kint/Zval/Representation/SplFileInfoRepresentation.php delete mode 100644 system/ThirdParty/Kint/Zval/TraceFrameValue.php delete mode 100644 system/ThirdParty/Kint/Zval/Value.php create mode 100644 system/ThirdParty/Kint/resources/compiled/main.js delete mode 100644 system/ThirdParty/Kint/resources/compiled/microtime.js delete mode 100644 system/ThirdParty/Kint/resources/compiled/plain.js delete mode 100644 system/ThirdParty/Kint/resources/compiled/rich.js delete mode 100644 system/ThirdParty/Kint/resources/compiled/shared.js diff --git a/admin/framework/composer.json b/admin/framework/composer.json index b507710c04b0..d03e43e18915 100644 --- a/admin/framework/composer.json +++ b/admin/framework/composer.json @@ -20,7 +20,7 @@ "codeigniter/coding-standard": "^1.7", "fakerphp/faker": "^1.9", "friendsofphp/php-cs-fixer": "^3.47.1", - "kint-php/kint": "^5.0.4", + "kint-php/kint": "^6.0", "mikey179/vfsstream": "^1.6", "nexusphp/cs-config": "^3.6", "phpunit/phpunit": "^10.5.16 || ^11.2", diff --git a/app/Config/Kint.php b/app/Config/Kint.php index d07078270145..931ad47f5fe4 100644 --- a/app/Config/Kint.php +++ b/app/Config/Kint.php @@ -3,7 +3,6 @@ namespace Config; use Kint\Parser\ConstructablePluginInterface; -use Kint\Renderer\AbstractRenderer; use Kint\Renderer\Rich\TabPluginInterface; use Kint\Renderer\Rich\ValuePluginInterface; @@ -41,7 +40,6 @@ class Kint */ public string $richTheme = 'aante-light.css'; public bool $richFolder = false; - public int $richSort = AbstractRenderer::SORT_FULL; /** * @var array>|null diff --git a/composer.json b/composer.json index 1704c70baa06..a46df741d51a 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "require-dev": { "codeigniter/phpstan-codeigniter": "^1.4", "fakerphp/faker": "^1.9", - "kint-php/kint": "^5.0.4", + "kint-php/kint": "^6.0", "mikey179/vfsstream": "^1.6", "nexusphp/tachycardia": "^2.0", "phpstan/extension-installer": "^1.4", diff --git a/system/Autoloader/Autoloader.php b/system/Autoloader/Autoloader.php index 3c2a250f6600..0c5c7fab50fa 100644 --- a/system/Autoloader/Autoloader.php +++ b/system/Autoloader/Autoloader.php @@ -547,7 +547,7 @@ private function configureKint(): void RichRenderer::$theme = $config->richTheme; RichRenderer::$folder = $config->richFolder; - RichRenderer::$sort = $config->richSort; + if (isset($config->richObjectPlugins) && is_array($config->richObjectPlugins)) { RichRenderer::$value_plugins = $config->richObjectPlugins; } diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index bb68c33f0760..8ff5d2ae5b9b 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -294,7 +294,7 @@ private function configureKint(): void RichRenderer::$theme = $config->richTheme; RichRenderer::$folder = $config->richFolder; - RichRenderer::$sort = $config->richSort; + if (isset($config->richObjectPlugins) && is_array($config->richObjectPlugins)) { RichRenderer::$value_plugins = $config->richObjectPlugins; } diff --git a/system/ThirdParty/Kint/CallFinder.php b/system/ThirdParty/Kint/CallFinder.php index f85b1ae017a9..11c40ad85526 100644 --- a/system/ThirdParty/Kint/CallFinder.php +++ b/system/ThirdParty/Kint/CallFinder.php @@ -30,10 +30,17 @@ /** * @psalm-type PhpTokenArray = array{int, string, int} * @psalm-type PhpToken = string|PhpTokenArray + * @psalm-type CallParameter = array{ + * name: string, + * path: string, + * expression: bool, + * literal: bool, + * new_without_parens: bool, + * } */ class CallFinder { - private static $ignore = [ + private static array $ignore = [ T_CLOSE_TAG => true, T_COMMENT => true, T_DOC_COMMENT => true, @@ -49,13 +56,12 @@ class CallFinder * - Wrap the access path in parentheses if there * are any of these in the final short parameter. */ - private static $operator = [ + private static array $operator = [ T_AND_EQUAL => true, T_BOOLEAN_AND => true, T_BOOLEAN_OR => true, T_ARRAY_CAST => true, T_BOOL_CAST => true, - T_CLASS => true, T_CLONE => true, T_CONCAT_EQUAL => true, T_DEC => true, @@ -79,7 +85,6 @@ class CallFinder T_MINUS_EQUAL => true, T_MOD_EQUAL => true, T_MUL_EQUAL => true, - T_NEW => true, T_OBJECT_CAST => true, T_OR_EQUAL => true, T_PLUS_EQUAL => true, @@ -96,6 +101,8 @@ class CallFinder T_POW_EQUAL => true, T_SPACESHIP => true, T_DOUBLE_ARROW => true, + T_FN => true, + T_COALESCE_EQUAL => true, '!' => true, '%' => true, '&' => true, @@ -114,7 +121,12 @@ class CallFinder '~' => true, ]; - private static $strip = [ + private static array $preserve_spaces = [ + T_CLASS => true, + T_NEW => true, + ]; + + private static array $strip = [ '(' => true, ')' => true, '[' => true, @@ -126,19 +138,19 @@ class CallFinder T_NS_SEPARATOR => true, ]; - private static $classcalls = [ + private static array $classcalls = [ T_DOUBLE_COLON => true, T_OBJECT_OPERATOR => true, ]; - private static $namespace = [ + private static array $namespace = [ T_STRING => true, ]; /** * @psalm-param callable-array|callable-string $function * - * @psalm-return list}> + * @psalm-return list, modifiers: list}> * * @return array List of matching calls on the relevant line */ @@ -169,11 +181,6 @@ public static function getFunctionCalls(string $source, int $line, $function): a T_NS_SEPARATOR => true, ]; - if (KINT_PHP74) { - self::$operator[T_FN] = true; - self::$operator[T_COALESCE_EQUAL] = true; - } - if (KINT_PHP80) { $up[T_ATTRIBUTE] = true; self::$operator[T_MATCH] = true; @@ -187,9 +194,12 @@ public static function getFunctionCalls(string $source, int $line, $function): a $identifier[T_NAME_RELATIVE] = true; } + if (!KINT_PHP84) { + self::$operator[T_NEW] = true; // @codeCoverageIgnore + } + /** @psalm-var list */ $tokens = \token_get_all($source); - $cursor = 1; $function_calls = []; // Performance optimization preventing backwards loops @@ -204,6 +214,7 @@ public static function getFunctionCalls(string $source, int $line, $function): a $class = null; /** * @psalm-suppress RedundantFunctionCallGivenDocblockType + * Psalm bug #11075 */ $function = \strtolower($function); } @@ -214,11 +225,7 @@ public static function getFunctionCalls(string $source, int $line, $function): a continue; } - // Count newlines for line number instead of using $token[2] - // since certain situations (String tokens after whitespace) may - // not have the correct line number unless you do this manually - $cursor += \substr_count($token[1], "\n"); - if ($cursor > $line) { + if ($token[2] > $line) { break; } @@ -229,6 +236,12 @@ public static function getFunctionCalls(string $source, int $line, $function): a $prev_tokens = [$prev_tokens[1], $prev_tokens[2], $token]; + // The logic for 7.3 through 8.1 is far more complicated. + // This should speed things up without making a lot more work for us + if (KINT_PHP82 && $line !== $token[2]) { + continue; + } + // Check if it's the right type to be the function we're looking for if (!isset(self::$namespace[$token[0]])) { continue; @@ -242,26 +255,29 @@ public static function getFunctionCalls(string $source, int $line, $function): a // Check if it's a function call $nextReal = self::realTokenIndex($tokens, $index); - if (!isset($nextReal, $tokens[$nextReal]) || '(' !== $tokens[$nextReal]) { + if ('(' !== ($tokens[$nextReal] ?? null)) { continue; } // Check if it matches the signature if (null === $class) { - if ($prev_tokens[1] && isset(self::$classcalls[$prev_tokens[1][0]])) { + if (null !== $prev_tokens[1] && isset(self::$classcalls[$prev_tokens[1][0]])) { continue; } } else { - if (!$prev_tokens[1] || T_DOUBLE_COLON !== $prev_tokens[1][0]) { + if (null === $prev_tokens[1] || T_DOUBLE_COLON !== $prev_tokens[1][0]) { continue; } - if (!$prev_tokens[0] || !isset(self::$namespace[$prev_tokens[0][0]])) { + if (null === $prev_tokens[0] || !isset(self::$namespace[$prev_tokens[0][0]])) { continue; } // All self::$namespace tokens are T_ constants - /** @psalm-var PhpTokenArray $prev_tokens[0] */ + /** + * @psalm-var PhpTokenArray $prev_tokens[0] + * Psalm bug #746 (wontfix) + */ $ns = \explode('\\', \strtolower($prev_tokens[0][1])); if (\end($ns) !== $class) { @@ -269,7 +285,7 @@ public static function getFunctionCalls(string $source, int $line, $function): a } } - $inner_cursor = $cursor; + $last_line = $token[2]; $depth = 1; // The depth respective to the function call $offset = $nextReal + 1; // The start of the function call $instring = false; // Whether we're in a string or not @@ -283,10 +299,8 @@ public static function getFunctionCalls(string $source, int $line, $function): a while (isset($tokens[$offset])) { $token = $tokens[$offset]; - // Ensure that the $inner_cursor is correct and - // that $token is either a T_ constant or a string if (\is_array($token)) { - $inner_cursor += \substr_count($token[1], "\n"); + $last_line = $token[2]; } if (!isset(self::$ignore[$token[0]]) && !isset($down[$token[0]])) { @@ -312,7 +326,7 @@ public static function getFunctionCalls(string $source, int $line, $function): a } $shortparam[] = $token; } - } elseif ('"' === $token[0]) { + } elseif ('"' === $token || 'b"' === $token) { // Strings use the same symbol for up and down, but we can // only ever be inside one string, so just use a bool for that if ($instring) { @@ -326,7 +340,7 @@ public static function getFunctionCalls(string $source, int $line, $function): a $instring = !$instring; - $shortparam[] = '"'; + $shortparam[] = $token; } elseif (1 === $depth) { if (',' === $token[0]) { $params[] = [ @@ -336,8 +350,19 @@ public static function getFunctionCalls(string $source, int $line, $function): a $shortparam = []; $paramrealtokens = false; $param_start = $offset + 1; - } elseif (T_CONSTANT_ENCAPSED_STRING === $token[0] && \strlen($token[1]) > 2) { - $shortparam[] = $token[1][0].'...'.$token[1][0]; + } elseif (T_CONSTANT_ENCAPSED_STRING === $token[0]) { + $quote = $token[1][0]; + if ('b' === $quote) { + $quote = $token[1][1]; + if (\strlen($token[1]) > 3) { + $token[1] = 'b'.$quote.'...'.$quote; + } + } else { + if (\strlen($token[1]) > 2) { + $token[1] = $quote.'...'.$quote; + } + } + $shortparam[] = $token; } else { $shortparam[] = $token; } @@ -360,15 +385,21 @@ public static function getFunctionCalls(string $source, int $line, $function): a // If we're not passed (or at) the line at the end // of the function call, we're too early so skip it - if ($inner_cursor < $line) { - continue; + // Only applies to < 8.2 since we check line explicitly above that + if (!KINT_PHP82 && $last_line < $line) { + continue; // @codeCoverageIgnore } + $formatted_parameters = []; + // Format the final output parameters - foreach ($params as &$param) { + foreach ($params as $param) { $name = self::tokensFormatted($param['short']); - + $path = self::tokensToString(self::tokensTrim($param['full'])); $expression = false; + $literal = false; + $new_without_parens = false; + foreach ($name as $token) { if (self::tokenIsOperator($token)) { $expression = true; @@ -376,16 +407,79 @@ public static function getFunctionCalls(string $source, int $line, $function): a } } - $param = [ - 'name' => self::tokensToString($name), - 'path' => self::tokensToString(self::tokensTrim($param['full'])), + // As of 8.4 new is only an expression when parentheses are + // omitted. In that case we can cheat and add them ourselves. + // + // > PHP interprets the first expression after new as a class name + // per https://wiki.php.net/rfc/new_without_parentheses + if (KINT_PHP84 && !$expression && T_NEW === $name[0][0]) { + $had_name_token = false; + $new_without_parens = true; + + foreach ($name as $token) { + if (T_NEW === $token[0]) { + continue; + } + + if (isset(self::$ignore[$token[0]])) { + continue; + } + + if (T_CLASS === $token[0]) { + $new_without_parens = false; + break; + } + + if ('(' === $token && $had_name_token) { + $new_without_parens = false; + break; + } + + $had_name_token = true; + } + } + + if (!$expression && 1 === \count($name)) { + switch ($name[0][0]) { + case T_CONSTANT_ENCAPSED_STRING: + case T_LNUMBER: + case T_DNUMBER: + $literal = true; + break; + case T_STRING: + switch (\strtolower($name[0][1])) { + case 'null': + case 'true': + case 'false': + $literal = true; + } + } + + $name = self::tokensToString($name); + } else { + $name = self::tokensToString($name); + + if (!$expression) { + switch (\strtolower($name)) { + case 'array()': + case '[]': + $literal = true; + break; + } + } + } + + $formatted_parameters[] = [ + 'name' => $name, + 'path' => $path, 'expression' => $expression, + 'literal' => $literal, + 'new_without_parens' => $new_without_parens, ]; } // Skip first-class callables - /** @psalm-var list $params */ - if (KINT_PHP81 && 1 === \count($params) && '...' === \reset($params)['path']) { + if (KINT_PHP81 && 1 === \count($formatted_parameters) && '...' === \reset($formatted_parameters)['path']) { continue; } @@ -418,7 +512,7 @@ public static function getFunctionCalls(string $source, int $line, $function): a } $function_calls[] = [ - 'parameters' => $params, + 'parameters' => $formatted_parameters, 'modifiers' => $mods, ]; } @@ -426,9 +520,6 @@ public static function getFunctionCalls(string $source, int $line, $function): a return $function_calls; } - /** - * @psalm-param PhpToken[] $tokens - */ private static function realTokenIndex(array $tokens, int $index): ?int { ++$index; @@ -457,8 +548,13 @@ private static function tokenIsOperator($token): bool } /** - * @psalm-param PhpToken[] $tokens + * @psalm-param PhpToken $token The token to check */ + private static function tokenPreserveWhitespace($token): bool + { + return self::tokenIsOperator($token) || isset(self::$preserve_spaces[$token[0]]); + } + private static function tokensToString(array $tokens): string { $out = ''; @@ -474,9 +570,6 @@ private static function tokensToString(array $tokens): string return $out; } - /** - * @psalm-param PhpToken[] $tokens - */ private static function tokensTrim(array $tokens): array { foreach ($tokens as $index => $token) { @@ -500,11 +593,6 @@ private static function tokensTrim(array $tokens): array return \array_reverse($tokens); } - /** - * @psalm-param PhpToken[] $tokens - * - * @psalm-return PhpToken[] - */ private static function tokensFormatted(array $tokens): array { $tokens = self::tokensTrim($tokens); @@ -519,7 +607,7 @@ private static function tokensFormatted(array $tokens): array $last = null; if (T_FUNCTION === $tokens[0][0] || - (KINT_PHP74 && T_FN === $tokens[0][0]) || + T_FN === $tokens[0][0] || (KINT_PHP80 && T_MATCH === $tokens[0][0]) ) { $ignorestrip = true; @@ -538,21 +626,24 @@ private static function tokensFormatted(array $tokens): array } $next = $tokens[$next]; - /** @psalm-var PhpToken $last */ + /** + * @psalm-var PhpToken $last + * Since we call tokensTrim we know we can't be here without a $last + */ if ($attribute && ']' === $last[0]) { $attribute = false; - } elseif (!$ignorestrip && isset(self::$strip[$last[0]]) && !self::tokenIsOperator($next)) { + } elseif (!$ignorestrip && isset(self::$strip[$last[0]]) && !self::tokenPreserveWhitespace($next)) { continue; } - if (!$ignorestrip && isset(self::$strip[$next[0]]) && $last && !self::tokenIsOperator($last)) { + if (!$ignorestrip && isset(self::$strip[$next[0]]) && !self::tokenPreserveWhitespace($last)) { continue; } - $token = ' '; + $token[1] = ' '; $space = true; } else { - if (KINT_PHP80 && $last && T_ATTRIBUTE == $last[0]) { + if (KINT_PHP80 && null !== $last && T_ATTRIBUTE === $last[0]) { $attribute = true; } diff --git a/system/ThirdParty/Kint/FacadeInterface.php b/system/ThirdParty/Kint/FacadeInterface.php index da4badcd869a..d61f1bdb6e10 100644 --- a/system/ThirdParty/Kint/FacadeInterface.php +++ b/system/ThirdParty/Kint/FacadeInterface.php @@ -29,7 +29,7 @@ use Kint\Parser\Parser; use Kint\Renderer\RendererInterface; -use Kint\Zval\Value; +use Kint\Value\Context\ContextInterface; interface FacadeInterface { @@ -42,8 +42,8 @@ public function setStatesFromCallInfo(array $info): void; /** * Renders a list of vars including the pre and post renders. * - * @param array $vars Data to dump - * @param Value[] $base The base value objects + * @param array $vars Data to dump + * @param ContextInterface[] $base The base contexts */ public function dumpAll(array $vars, array $base): string; } diff --git a/system/ThirdParty/Kint/Kint.php b/system/ThirdParty/Kint/Kint.php index 418c1f3f8789..6c55593d4ab2 100644 --- a/system/ThirdParty/Kint/Kint.php +++ b/system/ThirdParty/Kint/Kint.php @@ -31,12 +31,22 @@ use Kint\Parser\ConstructablePluginInterface; use Kint\Parser\Parser; use Kint\Parser\PluginInterface; +use Kint\Renderer\ConstructableRendererInterface; use Kint\Renderer\RendererInterface; use Kint\Renderer\TextRenderer; -use Kint\Zval\Value; +use Kint\Value\Context\BaseContext; +use Kint\Value\Context\ContextInterface; +use Kint\Value\UninitializedValue; /** * @psalm-consistent-constructor + * Psalm bug #8523 + * + * @psalm-import-type CallParameter from CallFinder + * + * @psalm-type KintMode = Kint::MODE_*|bool + * + * @psalm-api */ class Kint implements FacadeInterface { @@ -51,134 +61,111 @@ class Kint implements FacadeInterface * false: Disabled * true: Enabled, default mode selection * other: Manual mode selection + * + * @psalm-var KintMode */ public static $enabled_mode = true; /** * Default mode. * - * @var string + * @psalm-var KintMode */ public static $mode_default = self::MODE_RICH; /** * Default mode in CLI with cli_detection on. * - * @var string + * @psalm-var KintMode */ public static $mode_default_cli = self::MODE_CLI; /** - * @var bool Return output instead of echoing - */ - public static $return; - - /** - * @var string format of the link to the source file in trace entries. - * - * Use %f for file path, %l for line number. - * - * [!] EXAMPLE (works with for phpStorm and RemoteCall Plugin): + * @var bool enable detection when Kint is command line. * - * Kint::$file_link_format = 'http://localhost:8091/?message=%f:%l'; - */ - public static $file_link_format = ''; - - /** - * @var bool whether to display where kint was called from + * Formats output with whitespace only; does not HTML-escape it */ - public static $display_called_from = true; + public static bool $cli_detection = true; /** - * @var array base directories of your application that will be displayed instead of the full path. - * - * Keys are paths, values are replacement strings - * - * [!] EXAMPLE (for Laravel 5): - * - * Kint::$app_root_dirs = [ - * base_path() => '', - * app_path() => '', - * config_path() => '', - * database_path() => '', - * public_path() => '', - * resource_path() => '', - * storage_path() => '', - * ]; - * - * Defaults to [$_SERVER['DOCUMENT_ROOT'] => ''] + * @var bool Return output instead of echoing */ - public static $app_root_dirs = []; + public static bool $return = false; /** * @var int depth limit for array/object traversal. 0 for no limit */ - public static $depth_limit = 7; + public static int $depth_limit = 7; /** * @var bool expand all trees by default for rich view */ - public static $expanded = false; + public static bool $expanded = false; /** - * @var bool enable detection when Kint is command line. - * - * Formats output with whitespace only; does not HTML-escape it + * @var bool whether to display where kint was called from */ - public static $cli_detection = true; + public static bool $display_called_from = true; /** * @var array Kint aliases. Add debug functions in Kint wrappers here to fix modifiers and backtraces */ - public static $aliases = [ - ['Kint\\Kint', 'dump'], - ['Kint\\Kint', 'trace'], - ['Kint\\Kint', 'dumpAll'], + public static array $aliases = [ + [self::class, 'dump'], + [self::class, 'trace'], + [self::class, 'dumpAll'], ]; /** - * @psalm-var class-string[] Array of modes to renderer class names + * @psalm-var array> + * + * Array of modes to renderer class names */ - public static $renderers = [ - self::MODE_RICH => \Kint\Renderer\RichRenderer::class, - self::MODE_PLAIN => \Kint\Renderer\PlainRenderer::class, - self::MODE_TEXT => \Kint\Renderer\TextRenderer::class, - self::MODE_CLI => \Kint\Renderer\CliRenderer::class, + public static array $renderers = [ + self::MODE_RICH => Renderer\RichRenderer::class, + self::MODE_PLAIN => Renderer\PlainRenderer::class, + self::MODE_TEXT => TextRenderer::class, + self::MODE_CLI => Renderer\CliRenderer::class, ]; /** - * @psalm-var class-string[] + * @psalm-var array> */ - public static $plugins = [ + public static array $plugins = [ \Kint\Parser\ArrayLimitPlugin::class, \Kint\Parser\ArrayObjectPlugin::class, \Kint\Parser\Base64Plugin::class, + \Kint\Parser\BinaryPlugin::class, \Kint\Parser\BlacklistPlugin::class, + \Kint\Parser\ClassHooksPlugin::class, \Kint\Parser\ClassMethodsPlugin::class, \Kint\Parser\ClassStaticsPlugin::class, + \Kint\Parser\ClassStringsPlugin::class, \Kint\Parser\ClosurePlugin::class, \Kint\Parser\ColorPlugin::class, \Kint\Parser\DateTimePlugin::class, + \Kint\Parser\DomPlugin::class, \Kint\Parser\EnumPlugin::class, \Kint\Parser\FsPathPlugin::class, + \Kint\Parser\HtmlPlugin::class, \Kint\Parser\IteratorPlugin::class, \Kint\Parser\JsonPlugin::class, \Kint\Parser\MicrotimePlugin::class, + \Kint\Parser\MysqliPlugin::class, + // \Kint\Parser\SerializePlugin::class, \Kint\Parser\SimpleXMLElementPlugin::class, \Kint\Parser\SplFileInfoPlugin::class, - \Kint\Parser\SplObjectStoragePlugin::class, \Kint\Parser\StreamPlugin::class, \Kint\Parser\TablePlugin::class, \Kint\Parser\ThrowablePlugin::class, \Kint\Parser\TimestampPlugin::class, + \Kint\Parser\ToStringPlugin::class, \Kint\Parser\TracePlugin::class, \Kint\Parser\XmlPlugin::class, ]; - protected static $plugin_pool = []; - - protected $parser; - protected $renderer; + protected Parser $parser; + protected RendererInterface $renderer; public function __construct(Parser $p, RendererInterface $r) { @@ -210,7 +197,7 @@ public function setStatesFromStatics(array $statics): void { $this->renderer->setStatics($statics); - $this->parser->setDepthLimit(isset($statics['depth_limit']) ? $statics['depth_limit'] : 0); + $this->parser->setDepthLimit($statics['depth_limit'] ?? 0); $this->parser->clearPlugins(); if (!isset($statics['plugins'])) { @@ -222,19 +209,22 @@ public function setStatesFromStatics(array $statics): void foreach ($statics['plugins'] as $plugin) { if ($plugin instanceof PluginInterface) { $plugins[] = $plugin; - } elseif (\is_string($plugin) && \is_subclass_of($plugin, ConstructablePluginInterface::class)) { - if (!isset(static::$plugin_pool[$plugin])) { - $p = new $plugin(); - static::$plugin_pool[$plugin] = $p; - } - $plugins[] = static::$plugin_pool[$plugin]; + } elseif (\is_string($plugin) && \is_a($plugin, ConstructablePluginInterface::class, true)) { + $plugins[] = new $plugin($this->parser); } } $plugins = $this->renderer->filterParserPlugins($plugins); foreach ($plugins as $plugin) { - $this->parser->addPlugin($plugin); + try { + $this->parser->addPlugin($plugin); + } catch (InvalidArgumentException $e) { + \trigger_error( + 'Plugin '.Utils::errorSanitizeString(\get_class($plugin)).' could not be added to a Kint parser: '.Utils::errorSanitizeString($e->getMessage()), + E_USER_WARNING + ); + } } } @@ -246,7 +236,7 @@ public function setStatesFromCallInfo(array $info): void $this->parser->setDepthLimit(0); } - $this->parser->setCallerClass(isset($info['caller']['class']) ? $info['caller']['class'] : null); + $this->parser->setCallerClass($info['caller']['class'] ?? null); } public function dumpAll(array $vars, array $base): string @@ -255,17 +245,17 @@ public function dumpAll(array $vars, array $base): string throw new InvalidArgumentException('Kint::dumpAll requires arrays of identical size and keys as arguments'); } - $output = $this->renderer->preRender(); - if ([] === $vars) { - $output .= $this->renderer->renderNothing(); + return $this->dumpNothing(); } - foreach ($vars as $key => $arg) { - if (!$base[$key] instanceof Value) { - throw new InvalidArgumentException('Kint::dumpAll requires all elements of the second argument to be Value instances'); + $output = $this->renderer->preRender(); + + foreach ($vars as $key => $_) { + if (!$base[$key] instanceof ContextInterface) { + throw new InvalidArgumentException('Kint::dumpAll requires all elements of the second argument to be ContextInterface instances'); } - $output .= $this->dumpVar($arg, $base[$key]); + $output .= $this->dumpVar($vars[$key], $base[$key]); } $output .= $this->renderer->postRender(); @@ -273,16 +263,24 @@ public function dumpAll(array $vars, array $base): string return $output; } + protected function dumpNothing(): string + { + $output = $this->renderer->preRender(); + $output .= $this->renderer->render(new UninitializedValue(new BaseContext('No argument'))); + $output .= $this->renderer->postRender(); + + return $output; + } + /** * Dumps and renders a var. * * @param mixed &$var Data to dump - * @param Value $base Base object */ - protected function dumpVar(&$var, Value $base): string + protected function dumpVar(&$var, ContextInterface $c): string { return $this->renderer->render( - $this->parser->parse($var, $base) + $this->parser->parse($var, $c) ); } @@ -295,13 +293,11 @@ public static function getStatics(): array { return [ 'aliases' => static::$aliases, - 'app_root_dirs' => static::$app_root_dirs, 'cli_detection' => static::$cli_detection, 'depth_limit' => static::$depth_limit, 'display_called_from' => static::$display_called_from, 'enabled_mode' => static::$enabled_mode, 'expanded' => static::$expanded, - 'file_link_format' => static::$file_link_format, 'mode_default' => static::$mode_default, 'mode_default_cli' => static::$mode_default_cli, 'plugins' => static::$plugins, @@ -335,67 +331,57 @@ public static function createFromStatics(array $statics): ?FacadeInterface return null; } - /** @psalm-var class-string[] $statics['renderers'] */ - if (isset($statics['renderers'][$mode]) && \is_subclass_of($statics['renderers'][$mode], RendererInterface::class)) { - $renderer = new $statics['renderers'][$mode](); - } else { - $renderer = new TextRenderer(); + $renderer = null; + if (isset($statics['renderers'][$mode])) { + if ($statics['renderers'][$mode] instanceof RendererInterface) { + $renderer = $statics['renderers'][$mode]; + } + + if (\is_a($statics['renderers'][$mode], ConstructableRendererInterface::class, true)) { + $renderer = new $statics['renderers'][$mode](); + } } + $renderer ??= new TextRenderer(); + return new static(new Parser(), $renderer); } /** - * Creates base objects given parameter info. + * Creates base contexts given parameter info. * - * @param array $params Parameters as returned from getCallInfo - * @param int $argc Number of arguments the helper was called with + * @psalm-param list $params * - * @return Value[] Base objects for the arguments + * @return BaseContext[] Base contexts for the arguments */ public static function getBasesFromParamInfo(array $params, int $argc): array { - static $blacklist = [ - 'null', - 'true', - 'false', - 'array(...)', - 'array()', - '[...]', - '[]', - '(...)', - '()', - '"..."', - 'b"..."', - "'...'", - "b'...'", - ]; - - $params = \array_values($params); $bases = []; for ($i = 0; $i < $argc; ++$i) { $param = $params[$i] ?? null; - if (!isset($param['name']) || \is_numeric($param['name'])) { - $name = null; - } elseif (\in_array(\strtolower($param['name']), $blacklist, true)) { - $name = null; + if (!empty($param['literal'])) { + $name = 'literal'; } else { - $name = $param['name']; + $name = $param['name'] ?? '$'.$i; } if (isset($param['path'])) { $access_path = $param['path']; - if (!empty($param['expression'])) { + if ($param['expression']) { $access_path = '('.$access_path.')'; + } elseif ($param['new_without_parens']) { + $access_path .= '()'; } } else { $access_path = '$'.$i; } - $bases[] = Value::blank($name, $access_path); + $base = new BaseContext($name); + $base->access_path = $access_path; + $bases[] = $base; } return $bases; @@ -411,6 +397,8 @@ public static function getBasesFromParamInfo(array $params, int $argc): array * @param array $args Arguments * * @return array Call info + * + * @psalm-param list $trace */ public static function getCallInfo(array $aliases, array $trace, array $args): array { @@ -419,7 +407,7 @@ public static function getCallInfo(array $aliases, array $trace, array $args): a $caller = null; $miniTrace = []; - foreach ($trace as $index => $frame) { + foreach ($trace as $frame) { if (Utils::traceFrameIsListed($frame, $aliases)) { $found = true; $miniTrace = []; @@ -456,7 +444,7 @@ public static function getCallInfo(array $aliases, array $trace, array $args): a 'trace' => $miniTrace, ]; - if ($call) { + if (null !== $call) { $ret['params'] = $call['parameters']; $ret['modifiers'] = $call['modifiers']; } @@ -477,7 +465,7 @@ public static function trace() return 0; } - Utils::normalizeAliases(static::$aliases); + static::$aliases = Utils::normalizeAliases(static::$aliases); $call_info = static::getCallInfo(static::$aliases, \debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), []); @@ -514,10 +502,9 @@ public static function trace() \array_shift($trimmed_trace); - $output = $kintstance->dumpAll( - [$trimmed_trace], - [Value::blank('Kint\\Kint::trace()', 'debug_backtrace()')] - ); + $base = new BaseContext('Kint\\Kint::trace()'); + $base->access_path = 'debug_backtrace()'; + $output = $kintstance->dumpAll([$trimmed_trace], [$base]); if (static::$return || \in_array('@', $call_info['modifiers'], true)) { return $output; @@ -537,7 +524,7 @@ public static function trace() * * Functionally equivalent to Kint::dump(1) or Kint::dump(debug_backtrace()) * - * @psalm-param array ...$args + * @psalm-param mixed ...$args * * @return int|string */ @@ -547,7 +534,7 @@ public static function dump(...$args) return 0; } - Utils::normalizeAliases(static::$aliases); + static::$aliases = Utils::normalizeAliases(static::$aliases); $call_info = static::getCallInfo(static::$aliases, \debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), $args); @@ -587,54 +574,6 @@ public static function dump(...$args) return 0; } - /** - * generic path display callback, can be configured in app_root_dirs; purpose is - * to show relevant path info and hide as much of the path as possible. - */ - public static function shortenPath(string $file): string - { - $file = \array_values(\array_filter(\explode('/', \str_replace('\\', '/', $file)), 'strlen')); - - $longest_match = 0; - $match = '/'; - - foreach (static::$app_root_dirs as $path => $alias) { - /** @psalm-var string $path */ - if (empty($path)) { - continue; - } - - $path = \array_values(\array_filter(\explode('/', \str_replace('\\', '/', $path)), 'strlen')); - - if (\array_slice($file, 0, \count($path)) === $path && \count($path) > $longest_match) { - $longest_match = \count($path); - $match = $alias; - } - } - - if ($longest_match) { - $file = \array_merge([$match], \array_slice($file, $longest_match)); - - return \implode('/', $file); - } - - // fallback to find common path with Kint dir - $kint = \array_values(\array_filter(\explode('/', \str_replace('\\', '/', KINT_DIR)), 'strlen')); - - foreach ($file as $i => $part) { - if (!isset($kint[$i]) || $kint[$i] !== $part) { - return ($i ? '.../' : '/').\implode('/', \array_slice($file, $i)); - } - } - - return '/'.\implode('/', $file); - } - - public static function getIdeLink(string $file, int $line): string - { - return \str_replace(['%f', '%l'], [$file, $line], static::$file_link_format); - } - /** * Returns specific function call info from a stack trace frame, or null if no match could be found. * @@ -648,7 +587,7 @@ protected static function getSingleCall(array $frame, array $args): ?array if ( !isset($frame['file'], $frame['line'], $frame['function']) || !\is_readable($frame['file']) || - !$source = \file_get_contents($frame['file']) + false === ($source = \file_get_contents($frame['file'])) ) { return null; } @@ -686,6 +625,8 @@ protected static function getSingleCall(array $frame, array $args): ?array 'name' => \substr($param['name'], 3).'['.\var_export($key, true).']', 'path' => \substr($param['path'], 3).'['.\var_export($key, true).']', 'expression' => false, + 'literal' => false, + 'new_without_parens' => false, ]; } } else { @@ -696,6 +637,8 @@ protected static function getSingleCall(array $frame, array $args): ?array 'name' => 'array_values('.\substr($param['name'], 3).')['.$j.']', 'path' => 'array_values('.\substr($param['path'], 3).')['.$j.']', 'expression' => false, + 'literal' => false, + 'new_without_parens' => false, ]; } } diff --git a/system/ThirdParty/Kint/Parser/AbstractPlugin.php b/system/ThirdParty/Kint/Parser/AbstractPlugin.php index a3f896843a3a..6ab031d0b586 100644 --- a/system/ThirdParty/Kint/Parser/AbstractPlugin.php +++ b/system/ThirdParty/Kint/Parser/AbstractPlugin.php @@ -27,19 +27,22 @@ namespace Kint\Parser; -/** - * @psalm-consistent-constructor - */ abstract class AbstractPlugin implements ConstructablePluginInterface { - protected $parser; + private Parser $parser; - public function __construct() + public function __construct(Parser $parser) { + $this->parser = $parser; } public function setParser(Parser $p): void { $this->parser = $p; } + + protected function getParser(): Parser + { + return $this->parser; + } } diff --git a/system/ThirdParty/Kint/Parser/ArrayLimitPlugin.php b/system/ThirdParty/Kint/Parser/ArrayLimitPlugin.php index 2e7fca93842d..bd54f5fc235d 100644 --- a/system/ThirdParty/Kint/Parser/ArrayLimitPlugin.php +++ b/system/ThirdParty/Kint/Parser/ArrayLimitPlugin.php @@ -29,30 +29,43 @@ use InvalidArgumentException; use Kint\Utils; -use Kint\Zval\Value; - -class ArrayLimitPlugin extends AbstractPlugin +use Kint\Value\AbstractValue; +use Kint\Value\ArrayValue; +use Kint\Value\Context\BaseContext; +use Kint\Value\Context\ContextInterface; +use Kint\Value\Representation\ContainerRepresentation; +use Kint\Value\Representation\ProfileRepresentation; +use Kint\Value\Representation\ValueRepresentation; + +class ArrayLimitPlugin extends AbstractPlugin implements PluginBeginInterface { /** * Maximum size of arrays before limiting. - * - * @var int */ - public static $trigger = 1000; + public static int $trigger = 1000; /** * Maximum amount of items to show in a limited array. - * - * @var int */ - public static $limit = 50; + public static int $limit = 50; /** * Don't limit arrays with string keys. - * - * @var bool */ - public static $numeric_only = true; + public static bool $numeric_only = true; + + public function __construct(Parser $p) + { + if (self::$limit < 0) { + throw new InvalidArgumentException('ArrayLimitPlugin::$limit can not be lower than 0'); + } + + if (self::$limit >= self::$trigger) { + throw new InvalidArgumentException('ArrayLimitPlugin::$limit can not be lower than ArrayLimitPlugin::$trigger'); + } + + parent::__construct($p); + } public function getTypes(): array { @@ -64,80 +77,92 @@ public function getTriggers(): int return Parser::TRIGGER_BEGIN; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseBegin(&$var, ContextInterface $c): ?AbstractValue { - if (self::$limit >= self::$trigger) { - throw new InvalidArgumentException('ArrayLimitPlugin::$limit can not be lower than ArrayLimitPlugin::$trigger'); - } - - $depth = $this->parser->getDepthLimit(); + $parser = $this->getParser(); + $pdepth = $parser->getDepthLimit(); - if (!$depth) { - return; + if (!$pdepth) { + return null; } - if ($o->depth >= $depth - 1) { - return; + $cdepth = $c->getDepth(); + + if ($cdepth >= $pdepth - 1) { + return null; } if (\count($var) < self::$trigger) { - return; + return null; } if (self::$numeric_only && Utils::isAssoc($var)) { - return; + return null; } - $base = clone $o; - $base->depth = $depth - 1; - $obj = $this->parser->parse($var, $base); + $slice = \array_slice($var, 0, self::$limit, true); + $array = $parser->parse($slice, $c); - if ('array' != $obj->type) { - return; // @codeCoverageIgnore + if (!$array instanceof ArrayValue) { + return null; } - $obj->depth = $o->depth; - $i = 0; + $base = new BaseContext($c->getName()); + $base->depth = $pdepth - 1; + $base->access_path = $c->getAccessPath(); - foreach ($obj->value->contents as $child) { - // We only bother setting the correct depth for the first child, - // any deeper children should be cancelled by the depth limit - $child->depth = $o->depth + 1; - $this->recalcDepthLimit($child); + $slice = \array_slice($var, self::$limit, null, true); + $slice = $parser->parse($slice, $base); + + if (!$slice instanceof ArrayValue) { + return null; } - $var2 = \array_slice($var, 0, self::$limit, true); - $base = clone $o; - $slice = $this->parser->parse($var2, $base); + foreach ($slice->getContents() as $child) { + $this->replaceDepthLimit($child, $cdepth + 1); + } - \array_splice($obj->value->contents, 0, self::$limit, $slice->value->contents); + $out = new ArrayValue($c, \count($var), \array_merge($array->getContents(), $slice->getContents())); + $out->flags = $array->flags; - $o = $obj; + // Explicitly copy over profile plugin + $arrayp = $array->getRepresentation('profiling'); + $slicep = $slice->getRepresentation('profiling'); + if ($arrayp instanceof ProfileRepresentation && $slicep instanceof ProfileRepresentation) { + $out->addRepresentation(new ProfileRepresentation($arrayp->complexity + $slicep->complexity)); + } - $this->parser->haltParse(); + // Add contents. Check is in case some bad plugin empties both $slice and $array + if ($contents = $out->getContents()) { + $out->addRepresentation(new ContainerRepresentation('Contents', $contents, null, true)); + } + + return $out; } - protected function recalcDepthLimit(Value $o): void + protected function replaceDepthLimit(AbstractValue $v, int $depth): void { - $hintkey = \array_search('depth_limit', $o->hints, true); - if (false !== $hintkey) { - $o->hints[$hintkey] = 'array_limit'; + $c = $v->getContext(); + + if ($c instanceof BaseContext) { + $c->depth = $depth; } - $reps = $o->getRepresentations(); - if ($o->value) { - $reps[] = $o->value; + $pdepth = $this->getParser()->getDepthLimit(); + + if (($v->flags & AbstractValue::FLAG_DEPTH_LIMIT) && $pdepth && $depth < $pdepth) { + $v->flags = $v->flags & ~AbstractValue::FLAG_DEPTH_LIMIT | AbstractValue::FLAG_ARRAY_LIMIT; } + $reps = $v->getRepresentations(); + foreach ($reps as $rep) { - if ($rep->contents instanceof Value) { - $this->recalcDepthLimit($rep->contents); - } elseif (\is_array($rep->contents)) { - foreach ($rep->contents as $child) { - if ($child instanceof Value) { - $this->recalcDepthLimit($child); - } + if ($rep instanceof ContainerRepresentation) { + foreach ($rep->getContents() as $child) { + $this->replaceDepthLimit($child, $depth + 1); } + } elseif ($rep instanceof ValueRepresentation) { + $this->replaceDepthLimit($rep->getValue(), $depth + 1); } } } diff --git a/system/ThirdParty/Kint/Parser/ArrayObjectPlugin.php b/system/ThirdParty/Kint/Parser/ArrayObjectPlugin.php index 82ff7593e3b7..605d695a887b 100644 --- a/system/ThirdParty/Kint/Parser/ArrayObjectPlugin.php +++ b/system/ThirdParty/Kint/Parser/ArrayObjectPlugin.php @@ -28,9 +28,10 @@ namespace Kint\Parser; use ArrayObject; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\Context\ContextInterface; -class ArrayObjectPlugin extends AbstractPlugin +class ArrayObjectPlugin extends AbstractPlugin implements PluginBeginInterface { public function getTypes(): array { @@ -42,24 +43,26 @@ public function getTriggers(): int return Parser::TRIGGER_BEGIN; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseBegin(&$var, ContextInterface $c): ?AbstractValue { if (!$var instanceof ArrayObject) { - return; + return null; } $flags = $var->getFlags(); if (ArrayObject::STD_PROP_LIST === $flags) { - return; + return null; } + $parser = $this->getParser(); + $var->setFlags(ArrayObject::STD_PROP_LIST); - $o = $this->parser->parse($var, $o); + $v = $parser->parse($var, $c); $var->setFlags($flags); - $this->parser->haltParse(); + return $v; } } diff --git a/system/ThirdParty/Kint/Parser/Base64Plugin.php b/system/ThirdParty/Kint/Parser/Base64Plugin.php index a25a7343ac1f..9cc6eee94c93 100644 --- a/system/ThirdParty/Kint/Parser/Base64Plugin.php +++ b/system/ThirdParty/Kint/Parser/Base64Plugin.php @@ -27,24 +27,22 @@ namespace Kint\Parser; -use Kint\Zval\Representation\Representation; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\Context\BaseContext; +use Kint\Value\Representation\ValueRepresentation; +use Kint\Value\StringValue; -class Base64Plugin extends AbstractPlugin +class Base64Plugin extends AbstractPlugin implements PluginCompleteInterface { /** * The minimum length before a string will be considered for base64 decoding. - * - * @var int */ - public static $min_length_hard = 16; + public static int $min_length_hard = 16; /** * The minimum length before the base64 decoding will take precedence. - * - * @var int */ - public static $min_length_soft = 50; + public static int $min_length_soft = 50; public function getTypes(): array { @@ -56,41 +54,50 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { if (\strlen($var) < self::$min_length_hard || \strlen($var) % 4) { - return; + return $v; } if (\preg_match('/^[A-Fa-f0-9]+$/', $var)) { - return; + return $v; } if (!\preg_match('/^[A-Za-z0-9+\\/=]+$/', $var)) { - return; + return $v; } $data = \base64_decode($var, true); if (false === $data) { - return; + return $v; } - $base_obj = new Value(); - $base_obj->depth = $o->depth + 1; - $base_obj->name = 'base64_decode('.$o->name.')'; + $c = $v->getContext(); - if ($o->access_path) { - $base_obj->access_path = 'base64_decode('.$o->access_path.')'; + $base = new BaseContext('base64_decode('.$c->getName().')'); + $base->depth = $c->getDepth() + 1; + + if (null !== ($ap = $c->getAccessPath())) { + $base->access_path = 'base64_decode('.$ap.')'; } - $r = new Representation('Base64'); - $r->contents = $this->parser->parse($data, $base_obj); + $data = $this->getParser()->parse($data, $base); + $data->flags |= AbstractValue::FLAG_GENERATED; + + if (!$data instanceof StringValue || false === $data->getEncoding()) { + return $v; + } + + $r = new ValueRepresentation('Base64', $data); if (\strlen($var) > self::$min_length_soft) { - $o->addRepresentation($r, 0); + $v->addRepresentation($r, 0); } else { - $o->addRepresentation($r); + $v->addRepresentation($r); } + + return $v; } } diff --git a/system/ThirdParty/Kint/Parser/BinaryPlugin.php b/system/ThirdParty/Kint/Parser/BinaryPlugin.php index 56cf68a3359a..0ee1f639ab6e 100644 --- a/system/ThirdParty/Kint/Parser/BinaryPlugin.php +++ b/system/ThirdParty/Kint/Parser/BinaryPlugin.php @@ -27,10 +27,11 @@ namespace Kint\Parser; -use Kint\Zval\BlobValue; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\Representation\BinaryRepresentation; +use Kint\Value\StringValue; -class BinaryPlugin extends AbstractPlugin +class BinaryPlugin extends AbstractPlugin implements PluginCompleteInterface { public function getTypes(): array { @@ -42,10 +43,12 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { - if (!$o instanceof BlobValue || !\in_array($o->encoding, ['ASCII', 'UTF-8'], true)) { - $o->value->hints[] = 'binary'; + if ($v instanceof StringValue && false === $v->getEncoding()) { + $v->addRepresentation(new BinaryRepresentation($v->getValue(), true), 0); } + + return $v; } } diff --git a/system/ThirdParty/Kint/Parser/BlacklistPlugin.php b/system/ThirdParty/Kint/Parser/BlacklistPlugin.php index fa54a3a6743e..2fa4fed56137 100644 --- a/system/ThirdParty/Kint/Parser/BlacklistPlugin.php +++ b/system/ThirdParty/Kint/Parser/BlacklistPlugin.php @@ -27,25 +27,30 @@ namespace Kint\Parser; -use Kint\Zval\InstanceValue; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\Context\ContextInterface; +use Kint\Value\InstanceValue; use Psr\Container\ContainerInterface; +use Psr\EventDispatcher\EventDispatcherInterface; -class BlacklistPlugin extends AbstractPlugin +class BlacklistPlugin extends AbstractPlugin implements PluginBeginInterface { /** * List of classes and interfaces to blacklist. * - * @var array + * @var class-string[] */ - public static $blacklist = []; + public static array $blacklist = []; /** * List of classes and interfaces to blacklist except when dumped directly. * - * @var array + * @var class-string[] */ - public static $shallow_blacklist = [ContainerInterface::class]; + public static array $shallow_blacklist = [ + ContainerInterface::class, + EventDispatcherInterface::class, + ]; public function getTypes(): array { @@ -57,45 +62,35 @@ public function getTriggers(): int return Parser::TRIGGER_BEGIN; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseBegin(&$var, ContextInterface $c): ?AbstractValue { foreach (self::$blacklist as $class) { if ($var instanceof $class) { - $this->blacklistValue($var, $o); - - return; + return $this->blacklistValue($var, $c); } } - if ($o->depth <= 0) { - return; + if ($c->getDepth() <= 0) { + return null; } foreach (self::$shallow_blacklist as $class) { if ($var instanceof $class) { - $this->blacklistValue($var, $o); - - return; + return $this->blacklistValue($var, $c); } } + + return null; } /** * @param object &$var */ - protected function blacklistValue(&$var, Value &$o): void + protected function blacklistValue(&$var, ContextInterface $c): InstanceValue { - $object = new InstanceValue(); - $object->transplant($o); - $object->classname = \get_class($var); - $object->spl_object_hash = \spl_object_hash($var); - $object->clearRepresentations(); - $object->value = null; - $object->size = null; - $object->hints[] = 'blacklist'; - - $o = $object; + $object = new InstanceValue($c, \get_class($var), \spl_object_hash($var), \spl_object_id($var)); + $object->flags |= AbstractValue::FLAG_BLACKLIST; - $this->parser->haltParse(); + return $object; } } diff --git a/system/ThirdParty/Kint/Parser/ClassHooksPlugin.php b/system/ThirdParty/Kint/Parser/ClassHooksPlugin.php new file mode 100644 index 000000000000..1d658bfa909d --- /dev/null +++ b/system/ThirdParty/Kint/Parser/ClassHooksPlugin.php @@ -0,0 +1,122 @@ +> */ + private array $cache = []; + /** @psalm-var array> */ + private array $cache_verbose = []; + + public function getTypes(): array + { + return ['object']; + } + + public function getTriggers(): int + { + if (!KINT_PHP84) { + return Parser::TRIGGER_NONE; // @codeCoverageIgnore + } + + return Parser::TRIGGER_SUCCESS; + } + + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue + { + if (!$v instanceof InstanceValue) { + return $v; + } + + $props = $v->getRepresentation('properties'); + + if (!$props instanceof ContainerRepresentation) { + return $v; + } + + foreach ($props->getContents() as $prop) { + $c = $prop->getContext(); + + if (!$c instanceof PropertyContext || PropertyContext::HOOK_NONE === $c->hooks) { + continue; + } + + $cname = $c->getName(); + $cowner = $c->owner_class; + + if (!isset($this->cache_verbose[$cowner][$cname])) { + $ref = new ReflectionProperty($cowner, $cname); + $hooks = $ref->getHooks(); + + foreach ($hooks as $hook) { + if (!self::$verbose && false === $hook->getDocComment()) { + continue; + } + + $m = new MethodValue( + new MethodContext($hook), + new DeclaredCallableBag($hook) + ); + + $this->cache_verbose[$cowner][$cname][] = $m; + + if (false !== $hook->getDocComment()) { + $this->cache[$cowner][$cname][] = $m; + } + } + + $this->cache[$cowner][$cname] ??= []; + + if (self::$verbose) { + $this->cache_verbose[$cowner][$cname] ??= []; + } + } + + $cache = self::$verbose ? $this->cache_verbose : $this->cache; + $cache = $cache[$cowner][$cname] ?? []; + + if (\count($cache)) { + $prop->addRepresentation(new ContainerRepresentation('Hooks', $cache, 'propertyhooks')); + } + } + + return $v; + } +} diff --git a/system/ThirdParty/Kint/Parser/ClassMethodsPlugin.php b/system/ThirdParty/Kint/Parser/ClassMethodsPlugin.php index e71d537aa838..e06df2b61042 100644 --- a/system/ThirdParty/Kint/Parser/ClassMethodsPlugin.php +++ b/system/ThirdParty/Kint/Parser/ClassMethodsPlugin.php @@ -27,15 +27,32 @@ namespace Kint\Parser; -use Kint\Zval\InstanceValue; -use Kint\Zval\MethodValue; -use Kint\Zval\Representation\Representation; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\Context\MethodContext; +use Kint\Value\DeclaredCallableBag; +use Kint\Value\InstanceValue; +use Kint\Value\MethodValue; +use Kint\Value\Representation\ContainerRepresentation; use ReflectionClass; +use ReflectionMethod; -class ClassMethodsPlugin extends AbstractPlugin +class ClassMethodsPlugin extends AbstractPlugin implements PluginCompleteInterface { - private static $cache = []; + public static bool $show_access_path = true; + + /** + * Whether to go out of the way to show constructor paths + * when the instance isn't accessible. + * + * Disabling this improves performance. + */ + public static bool $show_constructor_path = false; + + /** @psalm-var array */ + private array $instance_cache = []; + + /** @psalm-var array */ + private array $static_cache = []; public function getTypes(): array { @@ -47,69 +64,162 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + /** + * @psalm-template T of AbstractValue + * + * @psalm-param mixed $var + * @psalm-param T $v + * + * @psalm-return T + */ + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { - $class = \get_class($var); - - // assuming class definition will not change inside one request - if (!isset(self::$cache[$class])) { - $methods = []; - - $reflection = new ReflectionClass($class); + if (!$v instanceof InstanceValue) { + return $v; + } - foreach ($reflection->getMethods() as $method) { - $methods[] = new MethodValue($method); + $class = $v->getClassName(); + $scope = $this->getParser()->getCallerClass(); + + if ($contents = $this->getCachedMethods($class)) { + if (self::$show_access_path) { + if (null !== $v->getContext()->getAccessPath()) { + // If we have an access path we can generate them for the children + foreach ($contents as $key => $val) { + if ($val->getContext()->isAccessible($scope)) { + $val = clone $val; + $val->getContext()->setAccessPathFromParent($v); + $contents[$key] = $val; + } + } + } elseif (self::$show_constructor_path && isset($contents['__construct'])) { + // __construct is the only exception: The only non-static method + // that can be called without access to the parent instance. + // Technically I guess it really is a static method but so long + // as PHP continues to refer to it as a normal one so will we. + $val = $contents['__construct']; + if ($val->getContext()->isAccessible($scope)) { + $val = clone $val; + $val->getContext()->setAccessPathFromParent($v); + $contents['__construct'] = $val; + } + } } - \usort($methods, ['Kint\\Parser\\ClassMethodsPlugin', 'sort']); + $v->addRepresentation(new ContainerRepresentation('Methods', $contents)); + } - self::$cache[$class] = $methods; + if ($contents = $this->getCachedStaticMethods($class)) { + $v->addRepresentation(new ContainerRepresentation('Static methods', $contents)); } - if (!empty(self::$cache[$class])) { - $rep = new Representation('Available methods', 'methods'); + return $v; + } - // Can't cache access paths - foreach (self::$cache[$class] as $m) { - $method = clone $m; - $method->depth = $o->depth + 1; + /** + * @psalm-param class-string $class + * + * @psalm-return MethodValue[] + */ + private function getCachedMethods(string $class): array + { + if (!isset($this->instance_cache[$class])) { + $methods = []; - if (!$this->parser->childHasPath($o, $method)) { - $method->access_path = null; - } else { - $method->setAccessPathFrom($o); + $r = new ReflectionClass($class); + + $parent_methods = []; + if ($parent = \get_parent_class($class)) { + $parent_methods = $this->getCachedMethods($parent); + } + + foreach ($r->getMethods() as $mr) { + if ($mr->isStatic()) { + continue; } - if ($method->owner_class !== $class && $d = $method->getRepresentation('method_definition')) { - $d = clone $d; - $d->inherited = true; - $method->replaceRepresentation($d); + $canon_name = \strtolower($mr->name); + if ($mr->isPrivate() && '__construct' !== $canon_name) { + $canon_name = \strtolower($mr->getDeclaringClass()->name).'::'.$canon_name; } - $rep->contents[] = $method; + if ($mr->getDeclaringClass()->name === $class) { + $method = new MethodValue(new MethodContext($mr), new DeclaredCallableBag($mr)); + $methods[$canon_name] = $method; + unset($parent_methods[$canon_name]); + } elseif (isset($parent_methods[$canon_name])) { + $method = $parent_methods[$canon_name]; + unset($parent_methods[$canon_name]); + + if (!$method->getContext()->inherited) { + $method = clone $method; + $method->getContext()->inherited = true; + } + + $methods[$canon_name] = $method; + } elseif ($mr->getDeclaringClass()->isInterface()) { + $c = new MethodContext($mr); + $c->inherited = true; + $methods[$canon_name] = new MethodValue($c, new DeclaredCallableBag($mr)); + } } - $o->addRepresentation($rep); + foreach ($parent_methods as $name => $method) { + if (!$method->getContext()->inherited) { + $method = clone $method; + $method->getContext()->inherited = true; + } + + if ('__construct' === $name) { + $methods['__construct'] = $method; + } else { + $methods[] = $method; + } + } + + $this->instance_cache[$class] = $methods; } + + return $this->instance_cache[$class]; } - private static function sort(MethodValue $a, MethodValue $b): int + /** + * @psalm-param class-string $class + * + * @psalm-return MethodValue[] + */ + private function getCachedStaticMethods(string $class): array { - $sort = ((int) $a->static) - ((int) $b->static); - if ($sort) { - return $sort; - } + if (!isset($this->static_cache[$class])) { + $methods = []; - $sort = Value::sortByAccess($a, $b); - if ($sort) { - return $sort; - } + $r = new ReflectionClass($class); + + $parent_methods = []; + if ($parent = \get_parent_class($class)) { + $parent_methods = $this->getCachedStaticMethods($parent); + } + + foreach ($r->getMethods(ReflectionMethod::IS_STATIC) as $mr) { + $canon_name = \strtolower($mr->getDeclaringClass()->name.'::'.$mr->name); + + if ($mr->getDeclaringClass()->name === $class) { + $method = new MethodValue(new MethodContext($mr), new DeclaredCallableBag($mr)); + $methods[$canon_name] = $method; + } elseif (isset($parent_methods[$canon_name])) { + $methods[$canon_name] = $parent_methods[$canon_name]; + } elseif ($mr->getDeclaringClass()->isInterface()) { + $c = new MethodContext($mr); + $c->inherited = true; + $methods[$canon_name] = new MethodValue($c, new DeclaredCallableBag($mr)); + } + + unset($parent_methods[$canon_name]); + } - $sort = InstanceValue::sortByHierarchy($a->owner_class, $b->owner_class); - if ($sort) { - return $sort; + $this->static_cache[$class] = $methods + $parent_methods; } - return $a->startline - $b->startline; + return $this->static_cache[$class]; } } diff --git a/system/ThirdParty/Kint/Parser/ClassStaticsPlugin.php b/system/ThirdParty/Kint/Parser/ClassStaticsPlugin.php index 5435da932bfc..17fa84ff3719 100644 --- a/system/ThirdParty/Kint/Parser/ClassStaticsPlugin.php +++ b/system/ThirdParty/Kint/Parser/ClassStaticsPlugin.php @@ -27,17 +27,23 @@ namespace Kint\Parser; -use Kint\Zval\InstanceValue; -use Kint\Zval\Representation\Representation; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\Context\ClassConstContext; +use Kint\Value\Context\ClassDeclaredContext; +use Kint\Value\Context\ClassOwnedContext; +use Kint\Value\Context\StaticPropertyContext; +use Kint\Value\InstanceValue; +use Kint\Value\Representation\ContainerRepresentation; +use Kint\Value\UninitializedValue; use ReflectionClass; use ReflectionClassConstant; use ReflectionProperty; use UnitEnum; -class ClassStaticsPlugin extends AbstractPlugin +class ClassStaticsPlugin extends AbstractPlugin implements PluginCompleteInterface { - private static $cache = []; + /** @psalm-var array>> */ + private array $cache = []; public function getTypes(): array { @@ -49,106 +55,176 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + /** + * @psalm-template T of AbstractValue + * + * @psalm-param mixed $var + * @psalm-param T $v + * + * @psalm-return T + */ + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { - if (!$o instanceof InstanceValue) { - return; + if (!$v instanceof InstanceValue) { + return $v; } - $class = \get_class($var); - $reflection = new ReflectionClass($class); + $class = $v->getClassName(); + $parser = $this->getParser(); + $r = new ReflectionClass($class); - // Constants - if (!isset(self::$cache[$class])) { - $consts = []; + $statics_full_name = false; + $statics = []; + $props = $r->getProperties(ReflectionProperty::IS_STATIC); + foreach ($props as $prop) { + $statics[$prop->name] = $prop; + } - foreach ($reflection->getConstants() as $name => $val) { - // Skip enum constants - if ($var instanceof UnitEnum && $val instanceof UnitEnum && $o->classname == \get_class($val)) { + $parent = $r; + while ($parent = $parent->getParentClass()) { + foreach ($parent->getProperties(ReflectionProperty::IS_STATIC) as $static) { + if (isset($statics[$static->name]) && $statics[$static->name]->getDeclaringClass()->name === $static->getDeclaringClass()->name) { continue; } - - $const = Value::blank($name); - $const->const = true; - $const->depth = $o->depth + 1; - $const->owner_class = $class; - $const->operator = Value::OPERATOR_STATIC; - - $creflection = new ReflectionClassConstant($class, $name); - - $const->access = Value::ACCESS_PUBLIC; - if ($creflection->isProtected()) { - $const->access = Value::ACCESS_PROTECTED; - } elseif ($creflection->isPrivate()) { - $const->access = Value::ACCESS_PRIVATE; - } - - if ($this->parser->childHasPath($o, $const)) { - $const->access_path = '\\'.$class.'::'.$name; - } - - $const = $this->parser->parse($val, $const); - - $consts[] = $const; + $statics[] = $static; } - - self::$cache[$class] = $consts; } - $statics = new Representation('Static class properties', 'statics'); - $statics->contents = self::$cache[$class]; + $statics_parsed = []; + $found_statics = []; + + $cdepth = $v->getContext()->getDepth(); - foreach ($reflection->getProperties(ReflectionProperty::IS_STATIC) as $static) { - $prop = new Value(); - $prop->name = '$'.$static->getName(); - $prop->depth = $o->depth + 1; - $prop->static = true; - $prop->operator = Value::OPERATOR_STATIC; - $prop->owner_class = $static->getDeclaringClass()->name; + foreach ($statics as $static) { + $prop = new StaticPropertyContext( + '$'.$static->getName(), + $static->getDeclaringClass()->name, + ClassDeclaredContext::ACCESS_PUBLIC + ); + $prop->depth = $cdepth + 1; + $prop->final = KINT_PHP84 && $static->isFinal(); - $prop->access = Value::ACCESS_PUBLIC; if ($static->isProtected()) { - $prop->access = Value::ACCESS_PROTECTED; + $prop->access = ClassDeclaredContext::ACCESS_PROTECTED; } elseif ($static->isPrivate()) { - $prop->access = Value::ACCESS_PRIVATE; + $prop->access = ClassDeclaredContext::ACCESS_PRIVATE; } - if ($this->parser->childHasPath($o, $prop)) { + if ($prop->isAccessible($parser->getCallerClass())) { $prop->access_path = '\\'.$prop->owner_class.'::'.$prop->name; } + if (isset($found_statics[$prop->name])) { + $statics_full_name = true; + } else { + $found_statics[$prop->name] = true; + + if ($prop->owner_class !== $class && ClassDeclaredContext::ACCESS_PRIVATE === $prop->access) { + $statics_full_name = true; + } + } + + if ($statics_full_name) { + $prop->name = $prop->owner_class.'::'.$prop->name; + } + $static->setAccessible(true); - if (KINT_PHP74 && !$static->isInitialized()) { - $prop->type = 'uninitialized'; - $statics->contents[] = $prop; + /** + * @psalm-suppress TooFewArguments + * Appears to have been fixed in master + */ + if (!$static->isInitialized()) { + $statics_parsed[] = new UninitializedValue($prop); } else { $static = $static->getValue(); - $statics->contents[] = $this->parser->parse($static, $prop); + $statics_parsed[] = $parser->parse($static, $prop); } } - if (empty($statics->contents)) { - return; + if ($statics_parsed) { + $v->addRepresentation(new ContainerRepresentation('Static properties', $statics_parsed, 'statics')); } - \usort($statics->contents, ['Kint\\Parser\\ClassStaticsPlugin', 'sort']); + if ($consts = $this->getCachedConstants($r)) { + $v->addRepresentation(new ContainerRepresentation('Class constants', $consts, 'constants')); + } - $o->addRepresentation($statics); + return $v; } - private static function sort(Value $a, Value $b): int + /** @psalm-return list */ + private function getCachedConstants(ReflectionClass $r): array { - $sort = ((int) $a->const) - ((int) $b->const); - if ($sort) { - return $sort; - } + $parser = $this->getParser(); + $pdepth = $parser->getDepthLimit(); + $pdepth_enabled = (int) ($pdepth > 0); + $class = $r->getName(); + + // Separate cache for dumping with/without depth limit + // This means we can do immediate depth limit on normal dumps + if (!isset($this->cache[$class][$pdepth_enabled])) { + $consts = []; + $reflectors = []; + + foreach ($r->getConstants() as $name => $val) { + $cr = new ReflectionClassConstant($class, $name); + + // Skip enum constants + if (\is_a($cr->class, UnitEnum::class, true) && $val instanceof UnitEnum && $cr->class === \get_class($val)) { + continue; + } + + $reflectors[$cr->name] = [$cr, $val]; + $consts[$cr->name] = null; + } + + if ($r = $r->getParentClass()) { + $parents = $this->getCachedConstants($r); + + foreach ($parents as $value) { + $c = $value->getContext(); + $cname = $c->getName(); + + if (isset($reflectors[$cname]) && $c instanceof ClassOwnedContext && $reflectors[$cname][0]->getDeclaringClass()->name === $c->owner_class) { + $consts[$cname] = $value; + unset($reflectors[$cname]); + } else { + $value = clone $value; + $c = $value->getContext(); + if ($c instanceof ClassOwnedContext) { + $c->name = $c->owner_class.'::'.$cname; + } + $consts[] = $value; + } + } + } + + foreach ($reflectors as [$cr, $val]) { + $context = new ClassConstContext( + $cr->name, + $cr->getDeclaringClass()->name, + ClassDeclaredContext::ACCESS_PUBLIC + ); + $context->depth = $pdepth ?: 1; + $context->final = KINT_PHP81 && $cr->isFinal(); + + if ($cr->isProtected()) { + $context->access = ClassDeclaredContext::ACCESS_PROTECTED; + } elseif ($cr->isPrivate()) { + $context->access = ClassDeclaredContext::ACCESS_PRIVATE; + } else { + // No access path for protected/private. Tough shit the cache is worth it + $context->access_path = '\\'.$context->owner_class.'::'.$context->name; + } + + $consts[$cr->name] = $parser->parse($val, $context); + } - $sort = Value::sortByAccess($a, $b); - if ($sort) { - return $sort; + /** @psalm-var AbstractValue[] $consts */ + $this->cache[$class][$pdepth_enabled] = \array_values($consts); } - return InstanceValue::sortByHierarchy($a->owner_class, $b->owner_class); + return $this->cache[$class][$pdepth_enabled]; } } diff --git a/system/ThirdParty/Kint/Parser/ClassStringsPlugin.php b/system/ThirdParty/Kint/Parser/ClassStringsPlugin.php new file mode 100644 index 000000000000..82927a655ce1 --- /dev/null +++ b/system/ThirdParty/Kint/Parser/ClassStringsPlugin.php @@ -0,0 +1,102 @@ +methods_plugin = new ClassMethodsPlugin($parser); + $this->statics_plugin = new ClassStaticsPlugin($parser); + } + + public function setParser(Parser $p): void + { + parent::setParser($p); + + $this->methods_plugin->setParser($p); + $this->statics_plugin->setParser($p); + } + + public function getTypes(): array + { + return ['string']; + } + + public function getTriggers(): int + { + return Parser::TRIGGER_SUCCESS; + } + + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue + { + $c = $v->getContext(); + + if ($c->getDepth() > 0) { + return $v; + } + + if (!\class_exists($var, true)) { + return $v; + } + + if (\in_array($var, self::$blacklist, true)) { + return $v; + } + + $r = new ReflectionClass($var); + + $fakeC = new BaseContext($c->getName()); + $fakeC->access_path = null; + $fakeV = new InstanceValue($fakeC, $r->getName(), 'badhash', -1); + $fakeVar = null; + + $fakeV = $this->methods_plugin->parseComplete($fakeVar, $fakeV, Parser::TRIGGER_SUCCESS); + $fakeV = $this->statics_plugin->parseComplete($fakeVar, $fakeV, Parser::TRIGGER_SUCCESS); + + foreach (['methods', 'static_methods', 'statics', 'constants'] as $rep) { + if ($rep = $fakeV->getRepresentation($rep)) { + $v->addRepresentation($rep); + } + } + + return $v; + } +} diff --git a/system/ThirdParty/Kint/Parser/ClosurePlugin.php b/system/ThirdParty/Kint/Parser/ClosurePlugin.php index 84ea582646f8..62db52547785 100644 --- a/system/ThirdParty/Kint/Parser/ClosurePlugin.php +++ b/system/ThirdParty/Kint/Parser/ClosurePlugin.php @@ -28,13 +28,14 @@ namespace Kint\Parser; use Closure; -use Kint\Zval\ClosureValue; -use Kint\Zval\ParameterValue; -use Kint\Zval\Representation\Representation; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\ClosureValue; +use Kint\Value\Context\BaseContext; +use Kint\Value\Representation\ContainerRepresentation; use ReflectionFunction; +use ReflectionReference; -class ClosurePlugin extends AbstractPlugin +class ClosurePlugin extends AbstractPlugin implements PluginCompleteInterface { public function getTypes(): array { @@ -46,29 +47,21 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { if (!$var instanceof Closure) { - return; + return $v; } - $object = new ClosureValue(); - $object->transplant($o); - $o = $object; - $object->removeRepresentation('properties'); - - $closure = new ReflectionFunction($var); + $c = $v->getContext(); - $o->filename = $closure->getFileName(); - $o->startline = $closure->getStartLine(); + $object = new ClosureValue($c, $var); + $object->flags = $v->flags; + $object->appendRepresentations($v->getRepresentations()); - foreach ($closure->getParameters() as $param) { - $o->parameters[] = new ParameterValue($param); - } + $object->removeRepresentation('properties'); - $p = new Representation('Parameters'); - $p->contents = $o->parameters; - $o->addRepresentation($p, 0); + $closure = new ReflectionFunction($var); $statics = []; @@ -76,21 +69,25 @@ public function parse(&$var, Value &$o, int $trigger): void $statics = ['this' => $v]; } - if (\count($statics = $statics + $closure->getStaticVariables())) { + $statics = $statics + $closure->getStaticVariables(); + + $cdepth = $c->getDepth(); + + if (\count($statics)) { $statics_parsed = []; - foreach ($statics as $name => &$static) { - $obj = Value::blank('$'.$name); - $obj->depth = $o->depth + 1; - $statics_parsed[$name] = $this->parser->parse($static, $obj); - if (null === $statics_parsed[$name]->value) { - $statics_parsed[$name]->access_path = null; - } + $parser = $this->getParser(); + + foreach ($statics as $name => $_) { + $base = new BaseContext('$'.$name); + $base->depth = $cdepth + 1; + $base->reference = null !== ReflectionReference::fromArrayElement($statics, $name); + $statics_parsed[$name] = $parser->parse($statics[$name], $base); } - $r = new Representation('Uses'); - $r->contents = $statics_parsed; - $o->addRepresentation($r, 0); + $object->addRepresentation(new ContainerRepresentation('Uses', $statics_parsed), 0); } + + return $object; } } diff --git a/system/ThirdParty/Kint/Parser/ColorPlugin.php b/system/ThirdParty/Kint/Parser/ColorPlugin.php index 2a58cb9ad3b9..fc160a4342ee 100644 --- a/system/ThirdParty/Kint/Parser/ColorPlugin.php +++ b/system/ThirdParty/Kint/Parser/ColorPlugin.php @@ -27,10 +27,13 @@ namespace Kint\Parser; -use Kint\Zval\Representation\ColorRepresentation; -use Kint\Zval\Value; +use InvalidArgumentException; +use Kint\Value\AbstractValue; +use Kint\Value\ColorValue; +use Kint\Value\Representation\ColorRepresentation; +use Kint\Value\StringValue; -class ColorPlugin extends AbstractPlugin +class ColorPlugin extends AbstractPlugin implements PluginCompleteInterface { public function getTypes(): array { @@ -42,24 +45,34 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { if (\strlen($var) > 32) { - return; + return $v; + } + + if (!$v instanceof StringValue) { + return $v; } $trimmed = \strtolower(\trim($var)); if (!isset(ColorRepresentation::$color_map[$trimmed]) && !\preg_match('/^(?:(?:rgb|hsl)[^\\)]{6,}\\)|#[0-9a-fA-F]{3,8})$/', $trimmed)) { - return; + return $v; } - $rep = new ColorRepresentation($var); - - if ($rep->variant) { - $o->removeRepresentation($o->value); - $o->addRepresentation($rep, 0); - $o->hints[] = 'color'; + try { + $rep = new ColorRepresentation($var); + } catch (InvalidArgumentException $e) { + return $v; } + + $out = new ColorValue($v->getContext(), $v->getValue(), $v->getEncoding()); + $out->flags = $v->flags; + $out->appendRepresentations($v->getRepresentations()); + $out->removeRepresentation('contents'); + $out->addRepresentation($rep, 0); + + return $out; } } diff --git a/system/ThirdParty/Kint/Parser/ConstructablePluginInterface.php b/system/ThirdParty/Kint/Parser/ConstructablePluginInterface.php index 689b24e61668..880b57efb72f 100644 --- a/system/ThirdParty/Kint/Parser/ConstructablePluginInterface.php +++ b/system/ThirdParty/Kint/Parser/ConstructablePluginInterface.php @@ -29,5 +29,5 @@ interface ConstructablePluginInterface extends PluginInterface { - public function __construct(); + public function __construct(Parser $p); } diff --git a/system/ThirdParty/Kint/Parser/DOMDocumentPlugin.php b/system/ThirdParty/Kint/Parser/DOMDocumentPlugin.php deleted file mode 100644 index 84cfbf835f26..000000000000 --- a/system/ThirdParty/Kint/Parser/DOMDocumentPlugin.php +++ /dev/null @@ -1,356 +0,0 @@ - 'DOMNode', - 'firstChild' => 'DOMNode', - 'lastChild' => 'DOMNode', - 'previousSibling' => 'DOMNode', - 'nextSibling' => 'DOMNode', - 'ownerDocument' => 'DOMDocument', - ]; - - /** - * Show all properties and methods. - * - * @var bool - */ - public static $verbose = false; - - public function getTypes(): array - { - return ['object']; - } - - public function getTriggers(): int - { - return Parser::TRIGGER_SUCCESS; - } - - public function parse(&$var, Value &$o, int $trigger): void - { - if (!$o instanceof InstanceValue) { - return; - } - - if ($var instanceof DOMNamedNodeMap || $var instanceof DOMNodeList) { - $this->parseList($var, $o, $trigger); - - return; - } - - if ($var instanceof DOMNode) { - $this->parseNode($var, $o); - - return; - } - } - - /** - * @param DOMNamedNodeMap|DOMNodeList &$var - */ - protected function parseList($var, InstanceValue &$o, int $trigger): void - { - if (!$var instanceof DOMNamedNodeMap && !$var instanceof DOMNodeList) { - return; - } - - // Recursion should never happen, should always be stopped at the parent - // DOMNode. Depth limit on the other hand we're going to skip since - // that would show an empty iterator and rather useless. Let the depth - // limit hit the children (DOMNodeList only has DOMNode as children) - if ($trigger & Parser::TRIGGER_RECURSION) { - return; - } - - $o->size = $var->length; - if (0 === $o->size) { - $o->replaceRepresentation(new Representation('Iterator')); - $o->size = null; - - return; - } - - // Depth limit - // Make empty iterator representation since we need it in DOMNode to point out depth limits - if ($this->parser->getDepthLimit() && $o->depth + 1 >= $this->parser->getDepthLimit()) { - $b = new Value(); - $b->name = $o->classname.' Iterator Contents'; - $b->access_path = 'iterator_to_array('.$o->access_path.')'; - $b->depth = $o->depth + 1; - $b->hints[] = 'depth_limit'; - - $r = new Representation('Iterator'); - $r->contents = [$b]; - $o->replaceRepresentation($r, 0); - - return; - } - - $r = new Representation('Iterator'); - $o->replaceRepresentation($r, 0); - - foreach ($var as $key => $item) { - $base_obj = new Value(); - $base_obj->depth = $o->depth + 1; - $base_obj->name = $item->nodeName; - - if ($o->access_path) { - if ($var instanceof DOMNamedNodeMap) { - // We can't use getNamedItem() for attributes without a - // namespace because it will pick the first matching - // attribute of *any* namespace. - // - // Contrary to the PHP docs, getNamedItemNS takes null - // as a namespace argument for an unnamespaced item. - $base_obj->access_path = $o->access_path.'->getNamedItemNS('; - $base_obj->access_path .= \var_export($item->namespaceURI, true); - $base_obj->access_path .= ', '; - $base_obj->access_path .= \var_export($item->name, true); - $base_obj->access_path .= ')'; - } else { // DOMNodeList - $base_obj->access_path = $o->access_path.'->item('.\var_export($key, true).')'; - } - } - - $r->contents[] = $this->parser->parse($item, $base_obj); - } - } - - /** - * @psalm-param-out Value &$o - */ - protected function parseNode(DOMNode $var, InstanceValue &$o): void - { - // Fill the properties - // They can't be enumerated through reflection or casting, - // so we have to trust the docs and try them one at a time - $known_properties = [ - 'nodeValue', - 'childNodes', - 'attributes', - ]; - - if (self::$verbose) { - $known_properties = [ - 'nodeName', - 'nodeValue', - 'nodeType', - 'parentNode', - 'childNodes', - 'firstChild', - 'lastChild', - 'previousSibling', - 'nextSibling', - 'attributes', - 'ownerDocument', - 'namespaceURI', - 'prefix', - 'localName', - 'baseURI', - 'textContent', - ]; - } - - $childNodes = null; - $attributes = null; - - $rep = $o->value; - - foreach ($known_properties as $prop) { - $prop_obj = $this->parseProperty($o, $prop, $var); - $rep->contents[] = $prop_obj; - - if ('childNodes' === $prop) { - $childNodes = $prop_obj->getRepresentation('iterator'); - } elseif ('attributes' === $prop) { - $attributes = $prop_obj->getRepresentation('iterator'); - } - } - - if (!self::$verbose) { - $o->removeRepresentation('methods'); - $o->removeRepresentation('properties'); - } - - // Attributes and comments and text nodes don't - // need children or attributes of their own - if (\in_array($o->classname, ['DOMAttr', 'DOMText', 'DOMComment'], true)) { - $o = self::textualNodeToString($o); - - return; - } - - // Set the attributes - if ($attributes) { - $a = new Representation('Attributes'); - foreach ($attributes->contents as $attribute) { - $a->contents[] = $attribute; - } - $o->addRepresentation($a, 0); - } - - // Set the children - if ($childNodes) { - $c = new Representation('Children'); - - if (1 === \count($childNodes->contents) && ($node = \reset($childNodes->contents)) && \in_array('depth_limit', $node->hints, true)) { - $n = new InstanceValue(); - $n->transplant($node); - $n->name = 'childNodes'; - $n->classname = 'DOMNodeList'; - $c->contents = [$n]; - } else { - foreach ($childNodes->contents as $node) { - // Remove text nodes if theyre empty - if ($node instanceof BlobValue && '#text' === $node->name && (\ctype_space($node->value->contents) || '' === $node->value->contents)) { - continue; - } - - $c->contents[] = $node; - } - } - - $o->addRepresentation($c, 0); - } - - if ($childNodes) { - $o->size = \count($childNodes->contents); - } - - if (!$o->size) { - $o->size = null; - } - } - - protected function parseProperty(InstanceValue $o, string $prop, DOMNode &$var): Value - { - // Duplicating (And slightly optimizing) the Parser::parseObject() code here - $base_obj = new Value(); - $base_obj->depth = $o->depth + 1; - $base_obj->owner_class = $o->classname; - $base_obj->name = $prop; - $base_obj->operator = Value::OPERATOR_OBJECT; - $base_obj->access = Value::ACCESS_PUBLIC; - - if (null !== $o->access_path) { - $base_obj->access_path = $o->access_path; - - if (\preg_match('/^[A-Za-z0-9_]+$/', $base_obj->name)) { - $base_obj->access_path .= '->'.$base_obj->name; - } else { - $base_obj->access_path .= '->{'.\var_export($base_obj->name, true).'}'; - } - } - - if (!isset($var->{$prop})) { - $base_obj->type = 'null'; - } elseif (isset(self::$blacklist[$prop])) { - $b = new InstanceValue(); - $b->transplant($base_obj); - $base_obj = $b; - - $base_obj->hints[] = 'blacklist'; - $base_obj->classname = self::$blacklist[$prop]; - } elseif ('attributes' === $prop) { - // Attributes are strings. If we're too deep set the - // depth limit to enable parsing them, but no deeper. - if ($this->parser->getDepthLimit() && $this->parser->getDepthLimit() - 2 < $base_obj->depth) { - $base_obj->depth = $this->parser->getDepthLimit() - 2; - } - $base_obj = $this->parser->parse($var->{$prop}, $base_obj); - } else { - $base_obj = $this->parser->parse($var->{$prop}, $base_obj); - } - - return $base_obj; - } - - protected static function textualNodeToString(InstanceValue $o): Value - { - if (empty($o->value) || empty($o->value->contents) || empty($o->classname)) { - throw new InvalidArgumentException('Invalid DOMNode passed to DOMDocumentPlugin::textualNodeToString'); - } - - if (!\in_array($o->classname, ['DOMText', 'DOMAttr', 'DOMComment'], true)) { - throw new InvalidArgumentException('Invalid DOMNode passed to DOMDocumentPlugin::textualNodeToString'); - } - - foreach ($o->value->contents as $property) { - if ('nodeValue' === $property->name) { - $ret = clone $property; - $ret->name = $o->name; - - return $ret; - } - } - - throw new InvalidArgumentException('Invalid DOMNode passed to DOMDocumentPlugin::textualNodeToString'); - } -} diff --git a/system/ThirdParty/Kint/Parser/DateTimePlugin.php b/system/ThirdParty/Kint/Parser/DateTimePlugin.php index 038acea133ec..4cba25d48dc9 100644 --- a/system/ThirdParty/Kint/Parser/DateTimePlugin.php +++ b/system/ThirdParty/Kint/Parser/DateTimePlugin.php @@ -27,11 +27,13 @@ namespace Kint\Parser; -use DateTime; -use Kint\Zval\DateTimeValue; -use Kint\Zval\Value; +use DateTimeInterface; +use Error; +use Kint\Value\AbstractValue; +use Kint\Value\DateTimeValue; +use Kint\Value\InstanceValue; -class DateTimePlugin extends AbstractPlugin +class DateTimePlugin extends AbstractPlugin implements PluginCompleteInterface { public function getTypes(): array { @@ -43,15 +45,23 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { - if (!$var instanceof DateTime) { - return; + if (!$var instanceof DateTimeInterface || !$v instanceof InstanceValue) { + return $v; } - $object = new DateTimeValue($var); - $object->transplant($o); + try { + $dtv = new DateTimeValue($v->getContext(), $var); + } catch (Error $e) { + // Only happens if someone makes a DateTimeInterface with a private __clone + return $v; + } + + $dtv->setChildren($v->getChildren()); + $dtv->flags = $v->flags; + $dtv->appendRepresentations($v->getRepresentations()); - $o = $object; + return $dtv; } } diff --git a/system/ThirdParty/Kint/Parser/DomPlugin.php b/system/ThirdParty/Kint/Parser/DomPlugin.php new file mode 100644 index 000000000000..d12e2f4cc8d9 --- /dev/null +++ b/system/ThirdParty/Kint/Parser/DomPlugin.php @@ -0,0 +1,528 @@ + Property names to readable status + */ + public const NODE_PROPS = [ + 'nodeType' => true, + 'nodeName' => true, + 'baseURI' => true, + 'isConnected' => true, + 'ownerDocument' => true, + 'parentNode' => true, + 'parentElement' => true, + 'childNodes' => true, + 'firstChild' => true, + 'lastChild' => true, + 'previousSibling' => true, + 'nextSibling' => true, + 'nodeValue' => true, + 'textContent' => false, + ]; + + /** + * @psalm-var non-empty-array Property names to readable status + */ + public const ELEMENT_PROPS = [ + 'namespaceURI' => true, + 'prefix' => true, + 'localName' => true, + 'tagName' => true, + 'id' => false, + 'className' => false, + 'classList' => true, + 'attributes' => true, + 'firstElementChild' => true, + 'lastElementChild' => true, + 'childElementCount' => true, + 'previousElementSibling' => true, + 'nextElementSibling' => true, + 'innerHTML' => false, + 'substitutedNodeValue' => false, + ]; + + /** + * @psalm-var non-empty-array Property names to readable status + */ + public const DOMNODE_PROPS = [ + 'nodeName' => true, + 'nodeValue' => false, + 'nodeType' => true, + 'parentNode' => true, + 'parentElement' => true, + 'childNodes' => true, + 'firstChild' => true, + 'lastChild' => true, + 'previousSibling' => true, + 'nextSibling' => true, + 'attributes' => true, + 'isConnected' => true, + 'ownerDocument' => true, + 'namespaceURI' => true, + 'prefix' => false, + 'localName' => true, + 'baseURI' => true, + 'textContent' => false, + ]; + + /** + * @psalm-var non-empty-array Property names to readable status + */ + public const DOMELEMENT_PROPS = [ + 'tagName' => true, + 'className' => false, + 'id' => false, + 'schemaTypeInfo' => true, + 'firstElementChild' => true, + 'lastElementChild' => true, + 'childElementCount' => true, + 'previousElementSibling' => true, + 'nextElementSibling' => true, + ]; + + public const DOM_VERSIONS = [ + 'parentElement' => KINT_PHP83, + 'isConnected' => KINT_PHP83, + 'className' => KINT_PHP83, + 'id' => KINT_PHP83, + 'firstElementChild' => KINT_PHP80, + 'lastElementChild' => KINT_PHP80, + 'childElementCount' => KINT_PHP80, + 'previousElementSibling' => KINT_PHP80, + 'nextElementSibling' => KINT_PHP80, + ]; + + /** + * List of properties to skip parsing. + * + * The properties of a Dom\Node can do a *lot* of damage to debuggers. The + * Dom\Node contains not one, not two, but 13 different ways to recurse into itself: + * * parentNode + * * firstChild + * * lastChild + * * previousSibling + * * nextSibling + * * parentElement + * * firstElementChild + * * lastElementChild + * * previousElementSibling + * * nextElementSibling + * * childNodes + * * attributes + * * ownerDocument + * + * All of this combined: the tiny SVGs used as the caret in Kint were already + * enough to make parsing and rendering take over a second, and send memory + * usage over 128 megs, back in the old DOM API. So we blacklist every field + * we don't strictly need and hope that that's good enough. + * + * In retrospect -- this is probably why print_r does the same + * + * @psalm-var array + */ + public static array $blacklist = [ + 'parentNode' => true, + 'firstChild' => true, + 'lastChild' => true, + 'previousSibling' => true, + 'nextSibling' => true, + 'firstElementChild' => true, + 'lastElementChild' => true, + 'parentElement' => true, + 'previousElementSibling' => true, + 'nextElementSibling' => true, + 'ownerDocument' => true, + ]; + + /** + * Show all properties and methods. + */ + public static bool $verbose = false; + + protected ClassMethodsPlugin $methods_plugin; + protected ClassStaticsPlugin $statics_plugin; + + public function __construct(Parser $parser) + { + parent::__construct($parser); + + $this->methods_plugin = new ClassMethodsPlugin($parser); + $this->statics_plugin = new ClassStaticsPlugin($parser); + } + + public function setParser(Parser $p): void + { + parent::setParser($p); + + $this->methods_plugin->setParser($p); + $this->statics_plugin->setParser($p); + } + + public function getTypes(): array + { + return ['object']; + } + + public function getTriggers(): int + { + return Parser::TRIGGER_BEGIN; + } + + public function parseBegin(&$var, ContextInterface $c): ?AbstractValue + { + // Attributes and chardata (Which is parent of comments and text + // nodes) don't need children or attributes of their own + if ($var instanceof Attr || $var instanceof CharacterData || $var instanceof DOMAttr || $var instanceof DOMCharacterData) { + return $this->parseText($var, $c); + } + + if ($var instanceof NamedNodeMap || $var instanceof NodeList || $var instanceof DOMNamedNodeMap || $var instanceof DOMNodeList) { + return $this->parseList($var, $c); + } + + if ($var instanceof Node || $var instanceof DOMNode) { + return $this->parseNode($var, $c); + } + + return null; + } + + /** @psalm-param Node|DOMNode $var */ + private function parseProperty(object $var, string $prop, ContextInterface $c): AbstractValue + { + if (!isset($var->{$prop})) { + return new FixedWidthValue($c, null); + } + + $parser = $this->getParser(); + $value = $var->{$prop}; + + if (\is_scalar($value)) { + return $parser->parse($value, $c); + } + + if (isset(self::$blacklist[$prop])) { + $b = new InstanceValue($c, \get_class($value), \spl_object_hash($value), \spl_object_id($value)); + $b->flags |= AbstractValue::FLAG_GENERATED | AbstractValue::FLAG_BLACKLIST; + + return $b; + } + + // Everything we can handle in parseBegin + if ($value instanceof Attr || $value instanceof CharacterData || $value instanceof DOMAttr || $value instanceof DOMCharacterData || $value instanceof NamedNodeMap || $value instanceof NodeList || $value instanceof DOMNamedNodeMap || $value instanceof DOMNodeList || $value instanceof Node || $value instanceof DOMNode) { + $out = $this->parseBegin($value, $c); + } + + if (!isset($out)) { + // Shouldn't ever happen + $out = $parser->parse($value, $c); // @codeCoverageIgnore + } + + $out->flags |= AbstractValue::FLAG_GENERATED; + + return $out; + } + + /** @psalm-param Attr|CharacterData|DOMAttr|DOMCharacterData $var */ + private function parseText(object $var, ContextInterface $c): AbstractValue + { + if ($c instanceof BaseContext && null !== $c->access_path) { + $c->access_path .= '->nodeValue'; + } + + return $this->parseProperty($var, 'nodeValue', $c); + } + + /** @psalm-param NamedNodeMap|NodeList|DOMNamedNodeMap|DOMNodeList $var */ + private function parseList(object $var, ContextInterface $c): InstanceValue + { + if ($var instanceof NodeList || $var instanceof DOMNodeList) { + $v = new DomNodeListValue($c, $var); + } else { + $v = new InstanceValue($c, \get_class($var), \spl_object_hash($var), \spl_object_id($var)); + } + + $parser = $this->getParser(); + $pdepth = $parser->getDepthLimit(); + + // Depth limit + // Use empty iterator representation since we need it to point out depth limits + if (($var instanceof NodeList || $var instanceof DOMNodeList) && $pdepth && $c->getDepth() >= $pdepth) { + $v->flags |= AbstractValue::FLAG_DEPTH_LIMIT; + + return $v; + } + + if (self::$verbose) { + $v = $this->methods_plugin->parseComplete($var, $v, Parser::TRIGGER_SUCCESS); + $v = $this->statics_plugin->parseComplete($var, $v, Parser::TRIGGER_SUCCESS); + } + + if (0 === $var->length) { + $v->setChildren([]); + + return $v; + } + + $cdepth = $c->getDepth(); + $ap = $c->getAccessPath(); + $contents = []; + + foreach ($var as $key => $item) { + $base_obj = new BaseContext($item->nodeName); + $base_obj->depth = $cdepth + 1; + + if ($var instanceof NamedNodeMap || $var instanceof DOMNamedNodeMap) { + if (null !== $ap) { + $base_obj->access_path = $ap.'['.\var_export($item->nodeName, true).']'; + } + } else { // NodeList + if (null !== $ap) { + $base_obj->access_path = $ap.'['.\var_export($key, true).']'; + } + } + + if ($item instanceof HTMLElement) { + $base_obj->name = $item->localName; + } + + $item = $parser->parse($item, $base_obj); + $item->flags |= AbstractValue::FLAG_GENERATED; + + $contents[] = $item; + } + + $v->setChildren($contents); + + if ($contents) { + $v->addRepresentation(new ContainerRepresentation('Iterator', $contents), 0); + } + + return $v; + } + + /** @psalm-param Node|DOMNode $var */ + private function parseNode(object $var, ContextInterface $c): DomNodeValue + { + $class = \get_class($var); + $pdepth = $this->getParser()->getDepthLimit(); + + if ($pdepth && $c->getDepth() >= $pdepth) { + $v = new DomNodeValue($c, $var); + $v->flags |= AbstractValue::FLAG_DEPTH_LIMIT; + + return $v; + } + + if (($var instanceof DocumentType || $var instanceof DOMDocumentType) && $c instanceof BaseContext && $c->name === $var->nodeName) { + $c->name = '!DOCTYPE '.$c->name; + } + + $cdepth = $c->getDepth(); + $ap = $c->getAccessPath(); + + $properties = []; + $children = []; + $attributes = []; + + foreach (self::getKnownProperties($var) as $prop => $readonly) { + $prop_c = new PropertyContext($prop, $class, ClassDeclaredContext::ACCESS_PUBLIC); + $prop_c->depth = $cdepth + 1; + $prop_c->readonly = KINT_PHP81 && $readonly; + + if (null !== $ap) { + $prop_c->access_path = $ap.'->'.$prop; + } + + $properties[] = $prop_obj = $this->parseProperty($var, $prop, $prop_c); + + if ('childNodes' === $prop) { + if (!$prop_obj instanceof DomNodeListValue) { + throw new LogicException('childNodes property parsed incorrectly'); // @codeCoverageIgnore + } + $children = self::getChildren($prop_obj); + } elseif ('attributes' === $prop) { + $attributes = $prop_obj->getRepresentation('iterator'); + $attributes = $attributes instanceof ContainerRepresentation ? $attributes->getContents() : []; + } elseif ('classList' === $prop) { + if ($iter = $prop_obj->getRepresentation('iterator')) { + $prop_obj->removeRepresentation($iter); + $prop_obj->addRepresentation($iter, 0); + } + } + } + + $v = new DomNodeValue($c, $var); + // If we're in text mode, we can see children through the childNodes property + $v->setChildren($properties); + + if ($children) { + $v->addRepresentation(new ContainerRepresentation('Children', $children, null, true)); + } + + if ($attributes) { + $v->addRepresentation(new ContainerRepresentation('Attributes', $attributes)); + } + + if (self::$verbose) { + $v->addRepresentation(new ContainerRepresentation('Properties', $properties)); + + $v = $this->methods_plugin->parseComplete($var, $v, Parser::TRIGGER_SUCCESS); + $v = $this->statics_plugin->parseComplete($var, $v, Parser::TRIGGER_SUCCESS); + } + + return $v; + } + + /** + * @psalm-param Node|DOMNode $var + * + * @psalm-return non-empty-array + */ + public static function getKnownProperties(object $var): array + { + if ($var instanceof Node) { + $known_properties = self::NODE_PROPS; + if ($var instanceof Element) { + $known_properties += self::ELEMENT_PROPS; + } + + if ($var instanceof Document) { + $known_properties['textContent'] = true; + } + + if ($var instanceof Attr || $var instanceof CharacterData) { + $known_properties['nodeValue'] = false; + } + } else { + $known_properties = self::DOMNODE_PROPS; + if ($var instanceof DOMElement) { + $known_properties += self::DOMELEMENT_PROPS; + } + + foreach (self::DOM_VERSIONS as $key => $val) { + /** + * @psalm-var bool $val + * Psalm bug #4509 + */ + if (false === $val) { + unset($known_properties[$key]); // @codeCoverageIgnore + } + } + } + + /** @psalm-var non-empty-array $known_properties */ + if (!self::$verbose) { + $known_properties = \array_intersect_key($known_properties, [ + 'nodeValue' => null, + 'childNodes' => null, + 'attributes' => null, + ]); + } + + return $known_properties; + } + + /** @psalm-return list */ + private static function getChildren(DomNodeListValue $property): array + { + if (0 === $property->getLength()) { + return []; + } + + if ($property->flags & AbstractValue::FLAG_DEPTH_LIMIT) { + return [$property]; + } + + $list_items = $property->getChildren(); + + if (null === $list_items) { + // This is here for psalm but all DomNodeListValue should + // either be depth_limit or have array children + return []; // @codeCoverageIgnore + } + + $children = []; + + foreach ($list_items as $node) { + // Remove text nodes if theyre empty + if ($node instanceof StringValue && '#text' === $node->getContext()->getName()) { + /** + * @psalm-suppress InvalidArgument + * Psalm bug #11055 + */ + if (\ctype_space($node->getValue()) || '' === $node->getValue()) { + continue; + } + } + + $children[] = $node; + } + + return $children; + } +} diff --git a/system/ThirdParty/Kint/Parser/EnumPlugin.php b/system/ThirdParty/Kint/Parser/EnumPlugin.php index d5d348aa5c90..e1fe85b65201 100644 --- a/system/ThirdParty/Kint/Parser/EnumPlugin.php +++ b/system/ThirdParty/Kint/Parser/EnumPlugin.php @@ -27,15 +27,15 @@ namespace Kint\Parser; -use BackedEnum; -use Kint\Zval\EnumValue; -use Kint\Zval\Representation\Representation; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\Context\BaseContext; +use Kint\Value\EnumValue; +use Kint\Value\Representation\ContainerRepresentation; use UnitEnum; -class EnumPlugin extends AbstractPlugin +class EnumPlugin extends AbstractPlugin implements PluginCompleteInterface { - private static $cache = []; + private array $cache = []; public function getTypes(): array { @@ -51,38 +51,34 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { if (!$var instanceof UnitEnum) { - return; + return $v; } + $c = $v->getContext(); $class = \get_class($var); - if (!isset(self::$cache[$class])) { - $cases = new Representation('Enum values', 'enum'); - $cases->contents = []; + if (!isset($this->cache[$class])) { + $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; - } + $base = new BaseContext($case->name); + $base->access_path = '\\'.$class.'::'.$case->name; + $base->depth = $c->getDepth() + 1; + $contents[] = new EnumValue($base, $case); } - self::$cache[$class] = $cases; + /** @psalm-var non-empty-array $contents */ + $this->cache[$class] = new ContainerRepresentation('Enum values', $contents, 'enum'); } - $object = new EnumValue($var); - $object->transplant($o); - - $object->addRepresentation(self::$cache[$class], 0); + $object = new EnumValue($c, $var); + $object->flags = $v->flags; + $object->appendRepresentations($v->getRepresentations()); + $object->addRepresentation($this->cache[$class], 0); - $o = $object; + return $object; } } diff --git a/system/ThirdParty/Kint/Parser/FsPathPlugin.php b/system/ThirdParty/Kint/Parser/FsPathPlugin.php index 1a98c6dcd134..3b17d0e91142 100644 --- a/system/ThirdParty/Kint/Parser/FsPathPlugin.php +++ b/system/ThirdParty/Kint/Parser/FsPathPlugin.php @@ -27,14 +27,14 @@ namespace Kint\Parser; -use Kint\Zval\Representation\SplFileInfoRepresentation; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\Representation\SplFileInfoRepresentation; use SplFileInfo; use TypeError; -class FsPathPlugin extends AbstractPlugin +class FsPathPlugin extends AbstractPlugin implements PluginCompleteInterface { - public static $blacklist = ['/', '.']; + public static array $blacklist = ['/', '.']; public function getTypes(): array { @@ -46,35 +46,35 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { if (\strlen($var) > 2048) { - return; + return $v; } if (!\preg_match('/[\\/\\'.DIRECTORY_SEPARATOR.']/', $var)) { - return; + return $v; } if (\preg_match('/[?<>"*|]/', $var)) { - return; + return $v; } try { if (!@\file_exists($var)) { - return; + return $v; } } catch (TypeError $e) {// @codeCoverageIgnore // Only possible in PHP 7 - return; // @codeCoverageIgnore + return $v; // @codeCoverageIgnore } if (\in_array($var, self::$blacklist, true)) { - return; + return $v; } - $r = new SplFileInfoRepresentation(new SplFileInfo($var)); - $r->hints[] = 'fspath'; - $o->addRepresentation($r, 0); + $v->addRepresentation(new SplFileInfoRepresentation(new SplFileInfo($var)), 0); + + return $v; } } diff --git a/system/ThirdParty/Kint/Parser/HtmlPlugin.php b/system/ThirdParty/Kint/Parser/HtmlPlugin.php new file mode 100644 index 000000000000..4df7ad1993fd --- /dev/null +++ b/system/ThirdParty/Kint/Parser/HtmlPlugin.php @@ -0,0 +1,86 @@ +' !== \strtolower(\substr($var, 0, 15))) { + return $v; + } + + try { + $html = HTMLDocument::createFromString($var, LIBXML_NOERROR); + } catch (DOMException $e) { // @codeCoverageIgnore + return $v; // @codeCoverageIgnore + } + + $c = $v->getContext(); + + $base = new BaseContext('childNodes'); + $base->depth = $c->getDepth(); + + if (null !== ($ap = $c->getAccessPath())) { + $base->access_path = '\\Dom\\HTMLDocument::createFromString('.$ap.')->childNodes'; + } + + $out = $this->getParser()->parse($html->childNodes, $base); + $iter = $out->getRepresentation('iterator'); + + if ($out->flags & AbstractValue::FLAG_DEPTH_LIMIT) { + $out->flags |= AbstractValue::FLAG_GENERATED; + $v->addRepresentation(new ValueRepresentation('HTML', $out), 0); + } elseif ($iter instanceof ContainerRepresentation) { + $v->addRepresentation(new ContainerRepresentation('HTML', $iter->getContents()), 0); + } + + return $v; + } +} diff --git a/system/ThirdParty/Kint/Parser/IteratorPlugin.php b/system/ThirdParty/Kint/Parser/IteratorPlugin.php index 7ebfe73e5cef..fff51ee491d7 100644 --- a/system/ThirdParty/Kint/Parser/IteratorPlugin.php +++ b/system/ThirdParty/Kint/Parser/IteratorPlugin.php @@ -27,11 +27,25 @@ namespace Kint\Parser; -use Kint\Zval\Representation\Representation; -use Kint\Zval\Value; +use Dom\NamedNodeMap; +use Dom\NodeList; +use DOMNamedNodeMap; +use DOMNodeList; +use Kint\Value\AbstractValue; +use Kint\Value\ArrayValue; +use Kint\Value\Context\BaseContext; +use Kint\Value\InstanceValue; +use Kint\Value\Representation\ContainerRepresentation; +use Kint\Value\Representation\ValueRepresentation; +use Kint\Value\UninitializedValue; +use mysqli_result; +use PDOStatement; +use SimpleXMLElement; +use SplFileObject; +use Throwable; use Traversable; -class IteratorPlugin extends AbstractPlugin +class IteratorPlugin extends AbstractPlugin implements PluginCompleteInterface { /** * List of classes and interfaces to blacklist. @@ -40,14 +54,17 @@ class IteratorPlugin extends AbstractPlugin * when traversed. Others are just huge. Either way, put them in here * and you won't have to worry about them being parsed. * - * @var array + * @psalm-var class-string[] */ - public static $blacklist = [ - 'DOMNamedNodeMap', - 'DOMNodeList', - 'mysqli_result', - 'PDOStatement', - 'SplFileObject', + public static array $blacklist = [ + NamedNodeMap::class, + NodeList::class, + DOMNamedNodeMap::class, + DOMNodeList::class, + mysqli_result::class, + PDOStatement::class, + SimpleXMLElement::class, + SplFileObject::class, ]; public function getTypes(): array @@ -60,48 +77,70 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { - if (!$var instanceof Traversable) { - return; + if (!$var instanceof Traversable || !$v instanceof InstanceValue || $v->getRepresentation('iterator')) { + return $v; } + $c = $v->getContext(); + foreach (self::$blacklist as $class) { + /** + * @psalm-suppress RedundantCondition + * Psalm bug #11076 + */ if ($var instanceof $class) { - $b = new Value(); - $b->name = $class.' Iterator Contents'; - $b->access_path = 'iterator_to_array('.$o->access_path.', true)'; - $b->depth = $o->depth + 1; - $b->hints[] = 'blacklist'; + $base = new BaseContext($class.' Iterator Contents'); + $base->depth = $c->getDepth() + 1; + if (null !== ($ap = $c->getAccessPath())) { + $base->access_path = 'iterator_to_array('.$ap.', false)'; + } - $r = new Representation('Iterator'); - $r->contents = [$b]; + $b = new UninitializedValue($base); + $b->flags |= AbstractValue::FLAG_BLACKLIST; - $o->addRepresentation($r); + $v->addRepresentation(new ValueRepresentation('Iterator', $b)); - return; + return $v; } } - $data = \iterator_to_array($var); + try { + $data = \iterator_to_array($var, false); + } catch (Throwable $t) { + return $v; + } - $base_obj = new Value(); - $base_obj->depth = $o->depth; + if (!\count($data)) { + return $v; + } - if ($o->access_path) { - $base_obj->access_path = 'iterator_to_array('.$o->access_path.')'; + $base = new BaseContext('Iterator Contents'); + $base->depth = $c->getDepth(); + if (null !== ($ap = $c->getAccessPath())) { + $base->access_path = 'iterator_to_array('.$ap.', false)'; } - $r = new Representation('Iterator'); - $r->contents = $this->parser->parse($data, $base_obj); - $r->contents = $r->contents->value->contents; + $iter_val = $this->getParser()->parse($data, $base); - $primary = $o->getRepresentations(); - $primary = \reset($primary); - if ($primary && $primary === $o->value && [] === $primary->contents) { - $o->addRepresentation($r, 0); + // Since we didn't get TRIGGER_DEPTH_LIMIT and set the iterator to the + // same depth we can assume at least 1 level deep will exist + if ($iter_val instanceof ArrayValue && $iterator_items = $iter_val->getContents()) { + $r = new ContainerRepresentation('Iterator', $iterator_items); + $iterator_items = \array_values($iterator_items); } else { - $o->addRepresentation($r); + $r = new ValueRepresentation('Iterator', $iter_val); + $iterator_items = [$iter_val]; } + + if ((bool) $v->getChildren()) { + $v->addRepresentation($r); + } else { + $v->setChildren($iterator_items); + $v->addRepresentation($r, 0); + } + + return $v; } } diff --git a/system/ThirdParty/Kint/Parser/JsonPlugin.php b/system/ThirdParty/Kint/Parser/JsonPlugin.php index 6bcf3a6130ce..cefdb11bda8f 100644 --- a/system/ThirdParty/Kint/Parser/JsonPlugin.php +++ b/system/ThirdParty/Kint/Parser/JsonPlugin.php @@ -27,10 +27,14 @@ namespace Kint\Parser; -use Kint\Zval\Representation\Representation; -use Kint\Zval\Value; +use JsonException; +use Kint\Value\AbstractValue; +use Kint\Value\ArrayValue; +use Kint\Value\Context\BaseContext; +use Kint\Value\Representation\ContainerRepresentation; +use Kint\Value\Representation\ValueRepresentation; -class JsonPlugin extends AbstractPlugin +class JsonPlugin extends AbstractPlugin implements PluginCompleteInterface { public function getTypes(): array { @@ -42,34 +46,41 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { if (!isset($var[0]) || ('{' !== $var[0] && '[' !== $var[0])) { - return; + return $v; } - $json = \json_decode($var, true); - - if (!$json) { - return; + try { + $json = \json_decode($var, true, 512, JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + return $v; } $json = (array) $json; - $base_obj = new Value(); - $base_obj->depth = $o->depth; + $c = $v->getContext(); + + $base = new BaseContext('JSON Decode'); + $base->depth = $c->getDepth(); - if ($o->access_path) { - $base_obj->access_path = 'json_decode('.$o->access_path.', true)'; + if (null !== ($ap = $c->getAccessPath())) { + $base->access_path = 'json_decode('.$ap.', true)'; } - $r = new Representation('Json'); - $r->contents = $this->parser->parse($json, $base_obj); + $json = $this->getParser()->parse($json, $base); - if (!\in_array('depth_limit', $r->contents->hints, true)) { - $r->contents = $r->contents->value->contents; + if ($json instanceof ArrayValue && (~$json->flags & AbstractValue::FLAG_DEPTH_LIMIT) && $contents = $json->getContents()) { + foreach ($contents as $value) { + $value->flags |= AbstractValue::FLAG_GENERATED; + } + $v->addRepresentation(new ContainerRepresentation('Json', $contents), 0); + } else { + $json->flags |= AbstractValue::FLAG_GENERATED; + $v->addRepresentation(new ValueRepresentation('Json', $json), 0); } - $o->addRepresentation($r, 0); + return $v; } } diff --git a/system/ThirdParty/Kint/Parser/MicrotimePlugin.php b/system/ThirdParty/Kint/Parser/MicrotimePlugin.php index 9531bbe73121..4ebabae88327 100644 --- a/system/ThirdParty/Kint/Parser/MicrotimePlugin.php +++ b/system/ThirdParty/Kint/Parser/MicrotimePlugin.php @@ -27,15 +27,16 @@ namespace Kint\Parser; -use Kint\Zval\Representation\MicrotimeRepresentation; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\MicrotimeValue; +use Kint\Value\Representation\MicrotimeRepresentation; -class MicrotimePlugin extends AbstractPlugin +class MicrotimePlugin extends AbstractPlugin implements PluginCompleteInterface { - private static $last = null; - private static $start = null; - private static $times = 0; - private static $group = 0; + private static ?array $last = null; + private static ?float $start = null; + private static int $times = 0; + private static ?string $group = null; public function getTypes(): array { @@ -47,22 +48,24 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { - if (0 !== $o->depth) { - return; + $c = $v->getContext(); + + if ($c->getDepth() > 0) { + return $v; } if (\is_string($var)) { - if ('microtime()' !== $o->name || !\preg_match('/^0\\.[0-9]{8} [0-9]{10}$/', $var)) { - return; + if ('microtime()' !== $c->getName() || !\preg_match('/^0\\.[0-9]{8} [0-9]{10}$/', $var)) { + return $v; } $usec = (int) \substr($var, 2, 6); $sec = (int) \substr($var, 11, 10); } else { - if ('microtime(...)' !== $o->name) { - return; + if ('microtime(...)' !== $c->getName()) { + return $v; } $sec = (int) \floor($var); @@ -85,23 +88,38 @@ public function parse(&$var, Value &$o, int $trigger): void if (null !== $lap) { $total = $time - self::$start; - $r = new MicrotimeRepresentation($sec, $usec, self::$group, $lap, $total, self::$times); + $r = new MicrotimeRepresentation($sec, $usec, self::getGroup(), $lap, $total, self::$times); } else { - $r = new MicrotimeRepresentation($sec, $usec, self::$group); + $r = new MicrotimeRepresentation($sec, $usec, self::getGroup()); } - $r->contents = $var; - $r->implicit_label = true; - $o->removeRepresentation($o->value); - $o->addRepresentation($r); - $o->hints[] = 'microtime'; + $out = new MicrotimeValue($v); + $out->removeRepresentation('contents'); + $out->addRepresentation($r); + + return $out; } + /** @psalm-api */ public static function clean(): void { self::$last = null; self::$start = null; self::$times = 0; - ++self::$group; + self::newGroup(); + } + + private static function getGroup(): string + { + if (null === self::$group) { + return self::newGroup(); + } + + return self::$group; + } + + private static function newGroup(): string + { + return self::$group = \bin2hex(\random_bytes(4)); } } diff --git a/system/ThirdParty/Kint/Parser/MysqliPlugin.php b/system/ThirdParty/Kint/Parser/MysqliPlugin.php index 22a23a901ccb..51195bf306cc 100644 --- a/system/ThirdParty/Kint/Parser/MysqliPlugin.php +++ b/system/ThirdParty/Kint/Parser/MysqliPlugin.php @@ -27,9 +27,11 @@ namespace Kint\Parser; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\Context\PropertyContext; +use Kint\Value\InstanceValue; +use Kint\Value\Representation\ContainerRepresentation; use mysqli; -use ReflectionClass; use Throwable; /** @@ -38,24 +40,24 @@ * Due to the way mysqli is implemented in PHP, this will cause * warnings on certain mysqli objects if screaming is enabled. */ -class MysqliPlugin extends AbstractPlugin +class MysqliPlugin extends AbstractPlugin implements PluginCompleteInterface { // These 'properties' are actually globals - protected $always_readable = [ + public const ALWAYS_READABLE = [ 'client_version' => true, 'connect_errno' => true, 'connect_error' => true, ]; // These are readable on empty mysqli objects, but not on failed connections - protected $empty_readable = [ + public const EMPTY_READABLE = [ 'client_info' => true, 'errno' => true, 'error' => true, ]; // These are only readable on connected mysqli objects - protected $connected_readable = [ + public const CONNECTED_READABLE = [ 'affected_rows' => true, 'error_list' => true, 'field_count' => true, @@ -80,20 +82,33 @@ public function getTriggers(): int return Parser::TRIGGER_COMPLETE; } - public function parse(&$var, Value &$o, int $trigger): void + /** + * Before 8.1: Properties were nulls when cast to array + * After 8.1: Properties are readonly and uninitialized when cast to array (Aka missing). + */ + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { - if (!$var instanceof mysqli) { - return; + if (!$var instanceof mysqli || !$v instanceof InstanceValue) { + return $v; } - /** @psalm-var ?string $var->sqlstate */ + $props = $v->getRepresentation('properties'); + + if (!$props instanceof ContainerRepresentation) { + return $v; + } + + /** + * @psalm-var ?string $var->sqlstate + * @psalm-var ?string $var->client_info + * Psalm bug #4502 + */ try { $connected = \is_string(@$var->sqlstate); } catch (Throwable $t) { $connected = false; } - /** @psalm-var ?string $var->client_info */ try { $empty = !$connected && \is_string(@$var->client_info); } catch (Throwable $t) { // @codeCoverageIgnore @@ -102,93 +117,60 @@ public function parse(&$var, Value &$o, int $trigger): void $empty = false; // @codeCoverageIgnore } - foreach ($o->value->contents as $key => $obj) { - if (isset($this->connected_readable[$obj->name])) { + $parser = $this->getParser(); + + $new_contents = []; + + foreach ($props->getContents() as $key => $obj) { + $new_contents[$key] = $obj; + + $c = $obj->getContext(); + + if (!$c instanceof PropertyContext) { + continue; + } + + if (isset(self::CONNECTED_READABLE[$c->getName()])) { + $c->readonly = KINT_PHP81; if (!$connected) { // No failed connections after PHP 8.1 continue; // @codeCoverageIgnore } - } elseif (isset($this->empty_readable[$obj->name])) { + } elseif (isset(self::EMPTY_READABLE[$c->getName()])) { + $c->readonly = KINT_PHP81; // No failed connections after PHP 8.1 if (!$connected && !$empty) { // @codeCoverageIgnore continue; // @codeCoverageIgnore } - } elseif (!isset($this->always_readable[$obj->name])) { - continue; + } elseif (!isset(self::ALWAYS_READABLE[$c->getName()])) { + continue; // @codeCoverageIgnore } - if ('null' !== $obj->type) { - continue; - } - - // @codeCoverageIgnoreStart - // All of this is irellevant after 8.1, - // we have separate logic for that below - - $param = $var->{$obj->name}; + $c->readonly = KINT_PHP81; - if (null === $param) { + // Only handle unparsed properties + if ((KINT_PHP81 ? 'uninitialized' : 'null') !== $obj->getType()) { continue; } - $base = Value::blank($obj->name, $obj->access_path); + $param = $var->{$c->getName()}; - $base->depth = $obj->depth; - $base->owner_class = $obj->owner_class; - $base->operator = $obj->operator; - $base->access = $obj->access; - $base->reference = $obj->reference; - - $o->value->contents[$key] = $this->parser->parse($param, $base); + // If it really was a null + if (!KINT_PHP81 && null === $param) { + continue; // @codeCoverageIgnore + } - // @codeCoverageIgnoreEnd + $new_contents[$key] = $parser->parse($param, $c); } - // PHP81 returns an empty array when casting a mysqli instance - if (KINT_PHP81) { - $r = new ReflectionClass(mysqli::class); - - $basepropvalues = []; - - foreach ($r->getProperties() as $prop) { - if ($prop->isStatic()) { - continue; // @codeCoverageIgnore - } + $new_contents = \array_values($new_contents); - $pname = $prop->getName(); - $param = null; - - if (isset($this->connected_readable[$pname])) { - if ($connected) { - $param = $var->{$pname}; - } - } else { - $param = $var->{$pname}; - } + $v->setChildren($new_contents); - $child = new Value(); - $child->depth = $o->depth + 1; - $child->owner_class = mysqli::class; - $child->operator = Value::OPERATOR_OBJECT; - $child->name = $pname; - - if ($prop->isPublic()) { - $child->access = Value::ACCESS_PUBLIC; - } elseif ($prop->isProtected()) { // @codeCoverageIgnore - $child->access = Value::ACCESS_PROTECTED; // @codeCoverageIgnore - } elseif ($prop->isPrivate()) { // @codeCoverageIgnore - $child->access = Value::ACCESS_PRIVATE; // @codeCoverageIgnore - } - - // We only do base mysqli properties so we don't need to worry about complex names - if ($this->parser->childHasPath($o, $child)) { - $child->access_path .= $o->access_path.'->'.$child->name; - } - - $basepropvalues[] = $this->parser->parse($param, $child); - } - - $o->value->contents = \array_merge($basepropvalues, $o->value->contents); + if ($new_contents) { + $v->replaceRepresentation(new ContainerRepresentation('Properties', $new_contents)); } + + return $v; } } diff --git a/system/ThirdParty/Kint/Parser/Parser.php b/system/ThirdParty/Kint/Parser/Parser.php index f044a9de913f..b609edb08c97 100644 --- a/system/ThirdParty/Kint/Parser/Parser.php +++ b/system/ThirdParty/Kint/Parser/Parser.php @@ -29,16 +29,33 @@ use DomainException; use Exception; -use Kint\Zval\BlobValue; -use Kint\Zval\InstanceValue; -use Kint\Zval\Representation\Representation; -use Kint\Zval\ResourceValue; -use Kint\Zval\Value; +use InvalidArgumentException; +use Kint\Utils; +use Kint\Value\AbstractValue; +use Kint\Value\ArrayValue; +use Kint\Value\ClosedResourceValue; +use Kint\Value\Context\ArrayContext; +use Kint\Value\Context\ClassDeclaredContext; +use Kint\Value\Context\ClassOwnedContext; +use Kint\Value\Context\ContextInterface; +use Kint\Value\Context\PropertyContext; +use Kint\Value\FixedWidthValue; +use Kint\Value\InstanceValue; +use Kint\Value\Representation\ContainerRepresentation; +use Kint\Value\Representation\StringRepresentation; +use Kint\Value\ResourceValue; +use Kint\Value\StringValue; +use Kint\Value\UninitializedValue; +use Kint\Value\UnknownValue; +use Kint\Value\VirtualValue; +use ReflectionClass; use ReflectionObject; use ReflectionProperty; -use stdClass; -use TypeError; +use ReflectionReference; +/** + * @psalm-type ParserTrigger int-mask-of + */ class Parser { /** @@ -52,44 +69,48 @@ class Parser * DEPTH_LIMIT: After parsing cancelled by depth limit * COMPLETE: SUCCESS | RECURSION | DEPTH_LIMIT * - * While a plugin's getTriggers may return any of these + * While a plugin's getTriggers may return any of these only one should + * be given to the plugin when PluginInterface::parse is called */ public const TRIGGER_NONE = 0; - public const TRIGGER_BEGIN = 1; - public const TRIGGER_SUCCESS = 2; - public const TRIGGER_RECURSION = 4; - public const TRIGGER_DEPTH_LIMIT = 8; - public const TRIGGER_COMPLETE = 14; - - protected $caller_class; - protected $depth_limit = 0; - protected $marker; - protected $object_hashes = []; - protected $parse_break = false; - protected $plugins = []; + public const TRIGGER_BEGIN = 1 << 0; + public const TRIGGER_SUCCESS = 1 << 1; + public const TRIGGER_RECURSION = 1 << 2; + public const TRIGGER_DEPTH_LIMIT = 1 << 3; + public const TRIGGER_COMPLETE = self::TRIGGER_SUCCESS | self::TRIGGER_RECURSION | self::TRIGGER_DEPTH_LIMIT; + + /** @psalm-var ?class-string */ + protected ?string $caller_class; + protected int $depth_limit = 0; + protected array $array_ref_stack = []; + protected array $object_hashes = []; + protected array $plugins = []; /** * @param int $depth_limit Maximum depth to parse data * @param ?string $caller Caller class name + * + * @psalm-param ?class-string $caller */ - public function __construct(int $depth_limit = 0, string $caller = null) + public function __construct(int $depth_limit = 0, ?string $caller = null) { - $this->marker = "kint\0".\random_bytes(16); - $this->depth_limit = $depth_limit; $this->caller_class = $caller; } /** * Set the caller class. + * + * @psalm-param ?class-string $caller */ - public function setCallerClass(string $caller = null): void + public function setCallerClass(?string $caller = null): void { $this->noRecurseCall(); $this->caller_class = $caller; } + /** @psalm-return ?class-string */ public function getCallerClass(): ?string { return $this->caller_class; @@ -116,58 +137,67 @@ public function getDepthLimit(): int * Parses a variable into a Kint object structure. * * @param mixed &$var The input variable - * @param Value $o The base object */ - public function parse(&$var, Value $o): Value + public function parse(&$var, ContextInterface $c): AbstractValue { - $o->type = \strtolower(\gettype($var)); + $type = \strtolower(\gettype($var)); - if (!$this->applyPlugins($var, $o, self::TRIGGER_BEGIN)) { - return $o; + if ($v = $this->applyPluginsBegin($var, $c, $type)) { + return $v; } - switch ($o->type) { + switch ($type) { case 'array': - return $this->parseArray($var, $o); + return $this->parseArray($var, $c); case 'boolean': case 'double': case 'integer': case 'null': - return $this->parseGeneric($var, $o); + return $this->parseFixedWidth($var, $c); case 'object': - return $this->parseObject($var, $o); + return $this->parseObject($var, $c); case 'resource': - return $this->parseResource($var, $o); + return $this->parseResource($var, $c); case 'string': - return $this->parseString($var, $o); - case 'unknown type': + return $this->parseString($var, $c); case 'resource (closed)': + return $this->parseResourceClosed($var, $c); + + case 'unknown type': // @codeCoverageIgnore default: - return $this->parseResourceClosed($var, $o); + // These should never happen. Unknown is resource (closed) from old + // PHP versions and there shouldn't be any other types. + return $this->parseUnknown($var, $c); // @codeCoverageIgnore } } - public function addPlugin(PluginInterface $p): bool + public function addPlugin(PluginInterface $p): void { if (!$types = $p->getTypes()) { - return false; + return; } if (!$triggers = $p->getTriggers()) { - return false; + return; + } + + if ($triggers & self::TRIGGER_BEGIN && !$p instanceof PluginBeginInterface) { + throw new InvalidArgumentException('Parsers triggered on begin must implement PluginBeginInterface'); + } + + if ($triggers & self::TRIGGER_COMPLETE && !$p instanceof PluginCompleteInterface) { + throw new InvalidArgumentException('Parsers triggered on completion must implement PluginCompleteInterface'); } $p->setParser($this); foreach ($types as $type) { - if (!isset($this->plugins[$type])) { - $this->plugins[$type] = [ - self::TRIGGER_BEGIN => [], - self::TRIGGER_SUCCESS => [], - self::TRIGGER_RECURSION => [], - self::TRIGGER_DEPTH_LIMIT => [], - ]; - } + $this->plugins[$type] ??= [ + self::TRIGGER_BEGIN => [], + self::TRIGGER_SUCCESS => [], + self::TRIGGER_RECURSION => [], + self::TRIGGER_DEPTH_LIMIT => [], + ]; foreach ($this->plugins[$type] as $trigger => &$pool) { if ($triggers & $trigger) { @@ -175,8 +205,6 @@ public function addPlugin(PluginInterface $p): bool } } } - - return true; } public function clearPlugins(): void @@ -184,61 +212,6 @@ public function clearPlugins(): void $this->plugins = []; } - public function haltParse(): void - { - $this->parse_break = true; - } - - public function childHasPath(InstanceValue $parent, Value $child): bool - { - if ('__PHP_Incomplete_Class' === $parent->classname) { - return false; - } - - if ('object' === $parent->type && (null !== $parent->access_path || $child->static || $child->const)) { - if (Value::ACCESS_PUBLIC === $child->access) { - return true; - } - - if (Value::ACCESS_PRIVATE === $child->access && $this->caller_class) { - if ($this->caller_class === $child->owner_class) { - return true; - } - } elseif (Value::ACCESS_PROTECTED === $child->access && $this->caller_class) { - if ($this->caller_class === $child->owner_class) { - return true; - } - - if (\is_subclass_of($this->caller_class, $child->owner_class)) { - return true; - } - - if (\is_subclass_of($child->owner_class, $this->caller_class)) { - return true; - } - } - } - - return false; - } - - /** - * Returns an array without the recursion marker in it. - * - * DO NOT pass an array that has had it's marker removed back - * into the parser, it will result in an extra recursion - * - * @param array $array Array potentially containing a recursion marker - * - * @return array Array with recursion marker removed - */ - public function getCleanArray(array $array): array - { - unset($array[$this->marker]); - - return $array; - } - protected function noRecurseCall(): void { $bt = \debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS); @@ -259,397 +232,360 @@ protected function noRecurseCall(): void } /** - * @param null|bool|float|int &$var + * @psalm-param null|bool|float|int &$var */ - private function parseGeneric(&$var, Value $o): Value + private function parseFixedWidth(&$var, ContextInterface $c): AbstractValue { - $rep = new Representation('Contents'); - $rep->contents = $var; - $rep->implicit_label = true; - $o->addRepresentation($rep); - $o->value = $rep; - - $this->applyPlugins($var, $o, self::TRIGGER_SUCCESS); + $v = new FixedWidthValue($c, $var); - return $o; + return $this->applyPluginsComplete($var, $v, self::TRIGGER_SUCCESS); } - /** - * Parses a string into a Kint BlobValue structure. - * - * @param string &$var The input variable - * @param Value $o The base object - */ - private function parseString(string &$var, Value $o): Value + private function parseString(string &$var, ContextInterface $c): AbstractValue { - $string = new BlobValue(); - $string->transplant($o); - $string->encoding = BlobValue::detectEncoding($var); - $string->size = \strlen($var); + $string = new StringValue($c, $var, Utils::detectEncoding($var)); - $rep = new Representation('Contents'); - $rep->contents = $var; - $rep->implicit_label = true; - - $string->addRepresentation($rep); - $string->value = $rep; - - $this->applyPlugins($var, $string, self::TRIGGER_SUCCESS); + if (false !== $string->getEncoding() && \strlen($var)) { + $string->addRepresentation(new StringRepresentation('Contents', $var, null, true)); + } - return $string; + return $this->applyPluginsComplete($var, $string, self::TRIGGER_SUCCESS); } - /** - * Parses an array into a Kint object structure. - * - * @param array &$var The input variable - * @param Value $o The base object - */ - private function parseArray(array &$var, Value $o): Value + private function parseArray(array &$var, ContextInterface $c): AbstractValue { - $array = new Value(); - $array->transplant($o); - $array->size = \count($var); + $size = \count($var); + $contents = []; + $parentRef = ReflectionReference::fromArrayElement([&$var], 0)->getId(); - if (isset($var[$this->marker])) { - --$array->size; - $array->hints[] = 'recursion'; + if (isset($this->array_ref_stack[$parentRef])) { + $array = new ArrayValue($c, $size, $contents); + $array->flags |= AbstractValue::FLAG_RECURSION; - $this->applyPlugins($var, $array, self::TRIGGER_RECURSION); - - return $array; + return $this->applyPluginsComplete($var, $array, self::TRIGGER_RECURSION); } - $rep = new Representation('Contents'); - $rep->implicit_label = true; - $array->addRepresentation($rep); - $array->value = $rep; + $this->array_ref_stack[$parentRef] = true; - if (!$array->size) { - $this->applyPlugins($var, $array, self::TRIGGER_SUCCESS); + $cdepth = $c->getDepth(); + $ap = $c->getAccessPath(); - return $array; - } + if ($size > 0 && $this->depth_limit && $cdepth >= $this->depth_limit) { + $array = new ArrayValue($c, $size, $contents); + $array->flags |= AbstractValue::FLAG_DEPTH_LIMIT; - if ($this->depth_limit && $o->depth >= $this->depth_limit) { - $array->hints[] = 'depth_limit'; + $array = $this->applyPluginsComplete($var, $array, self::TRIGGER_DEPTH_LIMIT); - $this->applyPlugins($var, $array, self::TRIGGER_DEPTH_LIMIT); + unset($this->array_ref_stack[$parentRef]); return $array; } - $copy = \array_values($var); - - // It's really really hard to access numeric string keys in arrays, - // and it's really really hard to access integer properties in - // objects, so we just use array_values and index by counter to get - // at it reliably for reference testing. This also affects access - // paths since it's pretty much impossible to access these things - // without complicated stuff you should never need to do. - $i = 0; - - // Set the marker for recursion - $var[$this->marker] = $array->depth; - - $refmarker = new stdClass(); + foreach ($var as $key => $_) { + $child = new ArrayContext($key); + $child->depth = $cdepth + 1; + $child->reference = null !== ReflectionReference::fromArrayElement($var, $key); - foreach ($var as $key => &$val) { - if ($key === $this->marker) { - continue; + if (null !== $ap) { + $child->access_path = $ap.'['.\var_export($key, true).']'; } - $child = new Value(); - $child->name = $key; - $child->depth = $array->depth + 1; - $child->access = Value::ACCESS_NONE; - $child->operator = Value::OPERATOR_ARRAY; - - if (null !== $array->access_path) { - if (\is_string($key) && (string) (int) $key === $key) { - $child->access_path = 'array_values('.$array->access_path.')['.$i.']'; // @codeCoverageIgnore - } else { - $child->access_path = $array->access_path.'['.\var_export($key, true).']'; - } - } + $contents[$key] = $this->parse($var[$key], $child); + } - $stash = $val; - try { - $copy[$i] = $refmarker; - } catch (TypeError $e) { - $child->reference = true; - } - if ($val === $refmarker) { - $child->reference = true; - $val = $stash; - } + $array = new ArrayValue($c, $size, $contents); - $rep->contents[] = $this->parse($val, $child); - ++$i; + if ($contents) { + $array->addRepresentation(new ContainerRepresentation('Contents', $contents, null, true)); } - $this->applyPlugins($var, $array, self::TRIGGER_SUCCESS); - unset($var[$this->marker]); + $array = $this->applyPluginsComplete($var, $array, self::TRIGGER_SUCCESS); + + unset($this->array_ref_stack[$parentRef]); return $array; } /** - * Parses an object into a Kint InstanceValue structure. - * - * @param object &$var The input variable - * @param Value $o The base object + * @psalm-return ReflectionProperty[] */ - private function parseObject(&$var, Value $o): Value + private function getPropsOrdered(ReflectionClass $r): array { - $hash = \spl_object_hash($var); - $values = (array) $var; - - $object = new InstanceValue(); - $object->transplant($o); - $object->classname = \get_class($var); - $object->spl_object_hash = $hash; - $object->size = \count($values); - - if (KINT_PHP72) { - $object->spl_object_id = \spl_object_id($var); + if ($parent = $r->getParentClass()) { + $props = self::getPropsOrdered($parent); + } else { + $props = []; } - if (isset($this->object_hashes[$hash])) { - $object->hints[] = 'recursion'; - - $this->applyPlugins($var, $object, self::TRIGGER_RECURSION); + foreach ($r->getProperties() as $prop) { + if ($prop->isStatic()) { + continue; + } - return $object; + if ($prop->isPrivate()) { + $props[] = $prop; + } else { + $props[$prop->name] = $prop; + } } - $this->object_hashes[$hash] = $object; + return $props; + } - if ($this->depth_limit && $o->depth >= $this->depth_limit) { - $object->hints[] = 'depth_limit'; + /** + * @codeCoverageIgnore + * + * @psalm-return ReflectionProperty[] + */ + private function getPropsOrderedOld(ReflectionClass $r): array + { + $props = []; - $this->applyPlugins($var, $object, self::TRIGGER_DEPTH_LIMIT); - unset($this->object_hashes[$hash]); + foreach ($r->getProperties() as $prop) { + if ($prop->isStatic()) { + continue; + } - return $object; + $props[] = $prop; } - $reflector = new ReflectionObject($var); + while ($r = $r->getParentClass()) { + foreach ($r->getProperties(ReflectionProperty::IS_PRIVATE) as $prop) { + if ($prop->isStatic()) { + continue; + } - if ($reflector->isUserDefined()) { - $object->filename = $reflector->getFileName(); - $object->startline = $reflector->getStartLine(); + $props[] = $prop; + } } - $rep = new Representation('Properties'); + return $props; + } - $readonly = []; + private function parseObject(object &$var, ContextInterface $c): AbstractValue + { + $hash = \spl_object_hash($var); + $classname = \get_class($var); - // Reflection is both slower and more painful to use than array casting - // We only use it to identify readonly and uninitialized properties - if (KINT_PHP74 && '__PHP_Incomplete_Class' != $object->classname) { - $rprops = $reflector->getProperties(); + if (isset($this->object_hashes[$hash])) { + $object = new InstanceValue($c, $classname, $hash, \spl_object_id($var)); + $object->flags |= AbstractValue::FLAG_RECURSION; - while ($reflector = $reflector->getParentClass()) { - $rprops = \array_merge($rprops, $reflector->getProperties(ReflectionProperty::IS_PRIVATE)); - } + return $this->applyPluginsComplete($var, $object, self::TRIGGER_RECURSION); + } - foreach ($rprops as $rprop) { - if ($rprop->isStatic()) { - continue; - } + $this->object_hashes[$hash] = true; - $rprop->setAccessible(true); + $cdepth = $c->getDepth(); + $ap = $c->getAccessPath(); - if (KINT_PHP81 && $rprop->isReadOnly()) { - if ($rprop->isPublic()) { - $readonly[$rprop->getName()] = true; - } elseif ($rprop->isProtected()) { - $readonly["\0*\0".$rprop->getName()] = true; - } elseif ($rprop->isPrivate()) { - $readonly["\0".$rprop->getDeclaringClass()->getName()."\0".$rprop->getName()] = true; - } - } + if ($this->depth_limit && $cdepth >= $this->depth_limit) { + $object = new InstanceValue($c, $classname, $hash, \spl_object_id($var)); + $object->flags |= AbstractValue::FLAG_DEPTH_LIMIT; - if ($rprop->isInitialized($var)) { - continue; - } + $object = $this->applyPluginsComplete($var, $object, self::TRIGGER_DEPTH_LIMIT); - $undefined = null; - - $child = new Value(); - $child->type = 'undefined'; - $child->depth = $object->depth + 1; - $child->owner_class = $rprop->getDeclaringClass()->getName(); - $child->operator = Value::OPERATOR_OBJECT; - $child->name = $rprop->getName(); - $child->readonly = KINT_PHP81 && $rprop->isReadOnly(); - - if ($rprop->isPublic()) { - $child->access = Value::ACCESS_PUBLIC; - } elseif ($rprop->isProtected()) { - $child->access = Value::ACCESS_PROTECTED; - } elseif ($rprop->isPrivate()) { - $child->access = Value::ACCESS_PRIVATE; - } + unset($this->object_hashes[$hash]); - // Can't dynamically add undefined properties, so no need to use var_export - if ($this->childHasPath($object, $child)) { - $child->access_path .= $object->access_path.'->'.$child->name; - } + return $object; + } - if ($this->applyPlugins($undefined, $child, self::TRIGGER_BEGIN)) { - $this->applyPlugins($undefined, $child, self::TRIGGER_SUCCESS); - } - $rep->contents[] = $child; - } + if (KINT_PHP81) { + $props = $this->getPropsOrdered(new ReflectionObject($var)); + } else { + $props = $this->getPropsOrderedOld(new ReflectionObject($var)); // @codeCoverageIgnore } - $copy = \array_values($values); - $refmarker = new stdClass(); - $i = 0; + $values = (array) $var; + $properties = []; + + foreach ($props as $rprop) { + $rprop->setAccessible(true); + $name = $rprop->getName(); - // Reflection will not show parent classes private properties, and if a - // property was unset it will happly trigger a notice looking for it. - foreach ($values as $key => &$val) { // Casting object to array: // private properties show in the form "\0$owner_class_name\0$property_name"; // protected properties show in the form "\0*\0$property_name"; // public properties show in the form "$property_name"; // http://www.php.net/manual/en/language.types.array.php#language.types.array.casting - - $child = new Value(); - $child->depth = $object->depth + 1; - $child->owner_class = $object->classname; - $child->operator = Value::OPERATOR_OBJECT; - $child->access = Value::ACCESS_PUBLIC; - if (isset($readonly[$key])) { - $child->readonly = true; + $key = $name; + if ($rprop->isProtected()) { + $key = "\0*\0".$name; + } elseif ($rprop->isPrivate()) { + $key = "\0".$rprop->getDeclaringClass()->getName()."\0".$name; + } + $initialized = \array_key_exists($key, $values); + if ($key === (string) (int) $key) { + $key = (int) $key; } - $split_key = \explode("\0", (string) $key, 3); + if ($rprop->isDefault()) { + $child = new PropertyContext( + $name, + $rprop->getDeclaringClass()->getName(), + ClassDeclaredContext::ACCESS_PUBLIC + ); - if (3 === \count($split_key) && '' === $split_key[0]) { - $child->name = $split_key[2]; - if ('*' === $split_key[1]) { - $child->access = Value::ACCESS_PROTECTED; - } else { - $child->access = Value::ACCESS_PRIVATE; - $child->owner_class = $split_key[1]; + $child->readonly = KINT_PHP81 && $rprop->isReadOnly(); + + if ($rprop->isProtected()) { + $child->access = ClassDeclaredContext::ACCESS_PROTECTED; + } elseif ($rprop->isPrivate()) { + $child->access = ClassDeclaredContext::ACCESS_PRIVATE; + } + + if (KINT_PHP84) { + if ($rprop->isProtectedSet()) { + $child->access_set = ClassDeclaredContext::ACCESS_PROTECTED; + } elseif ($rprop->isPrivateSet()) { + $child->access_set = ClassDeclaredContext::ACCESS_PRIVATE; + } + + $hooks = $rprop->getHooks(); + if (isset($hooks['get'])) { + $child->hooks |= PropertyContext::HOOK_GET; + if ($hooks['get']->returnsReference()) { + $child->hooks |= PropertyContext::HOOK_GET_REF; + } + } + if (isset($hooks['set'])) { + $child->hooks |= PropertyContext::HOOK_SET; + + $child->hook_set_type = (string) $rprop->getSettableType(); + if ($child->hook_set_type !== (string) $rprop->getType()) { + $child->hooks |= PropertyContext::HOOK_SET_TYPE; + } elseif ('' === $child->hook_set_type) { + $child->hook_set_type = null; + } + } } - } elseif (KINT_PHP72) { - $child->name = (string) $key; } else { - $child->name = $key; // @codeCoverageIgnore + $child = new ClassOwnedContext($name, $rprop->getDeclaringClass()->getName()); } - if ($this->childHasPath($object, $child)) { - $child->access_path = $object->access_path; + $child->reference = $initialized && null !== ReflectionReference::fromArrayElement($values, $key); + $child->depth = $cdepth + 1; - if (!KINT_PHP72 && \is_int($child->name)) { - $child->access_path = 'array_values((array) '.$child->access_path.')['.$i.']'; // @codeCoverageIgnore - } elseif (\preg_match('/^[a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff]*$/', $child->name)) { - $child->access_path .= '->'.$child->name; + if (null !== $ap && $child->isAccessible($this->caller_class)) { + /** @psalm-var string $child->name */ + if (Utils::isValidPhpName($child->name)) { + $child->access_path = $ap.'->'.$child->name; } else { - $child->access_path .= '->{'.\var_export((string) $child->name, true).'}'; + $child->access_path = $ap.'->{'.\var_export($child->name, true).'}'; } } - $stash = $val; - try { - $copy[$i] = $refmarker; - } catch (TypeError $e) { - $child->reference = true; - } - if ($val === $refmarker) { - $child->reference = true; - $val = $stash; + if (KINT_PHP84 && $rprop->isVirtual()) { + $properties[] = new VirtualValue($child); + } elseif (!$initialized) { + $properties[] = new UninitializedValue($child); + } else { + $properties[] = $this->parse($values[$key], $child); } + } - $rep->contents[] = $this->parse($val, $child); - ++$i; + $object = new InstanceValue($c, $classname, $hash, \spl_object_id($var)); + if ($props) { + $object->setChildren($properties); } - $object->addRepresentation($rep); - $object->value = $rep; - $this->applyPlugins($var, $object, self::TRIGGER_SUCCESS); + if ($properties) { + $object->addRepresentation(new ContainerRepresentation('Properties', $properties)); + } + + $object = $this->applyPluginsComplete($var, $object, self::TRIGGER_SUCCESS); unset($this->object_hashes[$hash]); return $object; } /** - * Parses a resource into a Kint ResourceValue structure. - * - * @param resource &$var The input variable - * @param Value $o The base object + * @psalm-param resource $var */ - private function parseResource(&$var, Value $o): Value + private function parseResource(&$var, ContextInterface $c): AbstractValue { - $resource = new ResourceValue(); - $resource->transplant($o); - $resource->resource_type = \get_resource_type($var); + $resource = new ResourceValue($c, \get_resource_type($var)); - $this->applyPlugins($var, $resource, self::TRIGGER_SUCCESS); + $resource = $this->applyPluginsComplete($var, $resource, self::TRIGGER_SUCCESS); return $resource; } /** - * Parses a closed resource into a Kint object structure. - * - * @param mixed &$var The input variable - * @param Value $o The base object + * @psalm-param mixed $var */ - private function parseResourceClosed(&$var, Value $o): Value + private function parseResourceClosed(&$var, ContextInterface $c): AbstractValue { - $o->type = 'resource (closed)'; - $this->applyPlugins($var, $o, self::TRIGGER_SUCCESS); + $v = new ClosedResourceValue($c); - return $o; + $v = $this->applyPluginsComplete($var, $v, self::TRIGGER_SUCCESS); + + return $v; } /** - * Applies plugins for an object type. + * Catch-all for any unexpectedgettype. + * + * This should never happen. * - * @param mixed &$var variable - * @param Value $o Kint object parsed so far - * @param int $trigger The trigger to check for the plugins + * @codeCoverageIgnore * - * @return bool Continue parsing + * @psalm-param mixed $var */ - private function applyPlugins(&$var, Value &$o, int $trigger): bool + private function parseUnknown(&$var, ContextInterface $c): AbstractValue { - $break_stash = $this->parse_break; + $v = new UnknownValue($c); - /** @psalm-var bool */ - $this->parse_break = false; + $v = $this->applyPluginsComplete($var, $v, self::TRIGGER_SUCCESS); - $plugins = []; + return $v; + } - if (isset($this->plugins[$o->type][$trigger])) { - $plugins = $this->plugins[$o->type][$trigger]; - } + /** + * Applies plugins for a yet-unparsed value. + * + * @param mixed &$var The input variable + */ + private function applyPluginsBegin(&$var, ContextInterface $c, string $type): ?AbstractValue + { + $plugins = $this->plugins[$type][self::TRIGGER_BEGIN] ?? []; foreach ($plugins as $plugin) { try { - $plugin->parse($var, $o, $trigger); + if ($v = $plugin->parseBegin($var, $c)) { + return $v; + } } catch (Exception $e) { \trigger_error( - 'An exception ('.\get_class($e).') was thrown in '.$e->getFile().' on line '.$e->getLine().' while executing Kint Parser Plugin "'.\get_class($plugin).'". Error message: '.$e->getMessage(), + 'An exception ('.Utils::errorSanitizeString(\get_class($e)).') was thrown in '.$e->getFile().' on line '.$e->getLine().' while executing "'.Utils::errorSanitizeString(\get_class($plugin)).'"->parseBegin. Error message: '.Utils::errorSanitizeString($e->getMessage()), E_USER_WARNING ); } + } + + return null; + } - if ($this->parse_break) { - $this->parse_break = $break_stash; + /** + * Applies plugins for a parsed AbstractValue. + * + * @param mixed &$var The input variable + */ + private function applyPluginsComplete(&$var, AbstractValue $v, int $trigger): AbstractValue + { + $plugins = $this->plugins[$v->getType()][$trigger] ?? []; - return false; + foreach ($plugins as $plugin) { + try { + $v = $plugin->parseComplete($var, $v, $trigger); + } catch (Exception $e) { + \trigger_error( + 'An exception ('.Utils::errorSanitizeString(\get_class($e)).') was thrown in '.$e->getFile().' on line '.$e->getLine().' while executing "'.Utils::errorSanitizeString(\get_class($plugin)).'"->parseComplete. Error message: '.Utils::errorSanitizeString($e->getMessage()), + E_USER_WARNING + ); } } - $this->parse_break = $break_stash; - - return true; + return $v; } } diff --git a/system/ThirdParty/Kint/Parser/PluginBeginInterface.php b/system/ThirdParty/Kint/Parser/PluginBeginInterface.php new file mode 100644 index 000000000000..25d72f4e1525 --- /dev/null +++ b/system/ThirdParty/Kint/Parser/PluginBeginInterface.php @@ -0,0 +1,39 @@ +getDepth()) { + $this->instance_counts = []; + $this->instance_complexity = []; + $this->instance_count_stack = []; + $this->class_complexity = []; + $this->class_count_stack = []; + } + + if (\is_object($var)) { + $hash = \spl_object_hash($var); + $this->instance_counts[$hash] ??= 0; + $this->instance_complexity[$hash] ??= 0; + $this->instance_count_stack[$hash] ??= 0; + + if (0 === $this->instance_count_stack[$hash]) { + foreach (\class_parents($var) as $class) { + $this->class_count_stack[$class] ??= 0; + ++$this->class_count_stack[$class]; + } + + foreach (\class_implements($var) as $iface) { + $this->class_count_stack[$iface] ??= 0; + ++$this->class_count_stack[$iface]; + } + } + + ++$this->instance_count_stack[$hash]; + } + + return null; + } + + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue + { + if ($v instanceof InstanceValue) { + --$this->instance_count_stack[$v->getSplObjectHash()]; + + if (0 === $this->instance_count_stack[$v->getSplObjectHash()]) { + foreach (\class_parents($var) as $class) { + --$this->class_count_stack[$class]; + } + + foreach (\class_implements($var) as $iface) { + --$this->class_count_stack[$iface]; + } + } + } + + // Don't check subs if we're in recursion or array limit + if (~$trigger & Parser::TRIGGER_SUCCESS) { + return $v; + } + + $sub_complexity = 1; + + foreach ($v->getRepresentations() as $rep) { + if ($rep instanceof ContainerRepresentation) { + foreach ($rep->getContents() as $value) { + $profile = $value->getRepresentation('profiling'); + $sub_complexity += $profile instanceof ProfileRepresentation ? $profile->complexity : 1; + } + } else { + ++$sub_complexity; + } + } + + if ($v instanceof InstanceValue) { + ++$this->instance_counts[$v->getSplObjectHash()]; + if (0 === $this->instance_count_stack[$v->getSplObjectHash()]) { + $this->instance_complexity[$v->getSplObjectHash()] += $sub_complexity; + + $this->class_complexity[$v->getClassName()] ??= 0; + $this->class_complexity[$v->getClassName()] += $sub_complexity; + + foreach (\class_parents($var) as $class) { + $this->class_complexity[$class] ??= 0; + if (0 === $this->class_count_stack[$class]) { + $this->class_complexity[$class] += $sub_complexity; + } + } + + foreach (\class_implements($var) as $iface) { + $this->class_complexity[$iface] ??= 0; + if (0 === $this->class_count_stack[$iface]) { + $this->class_complexity[$iface] += $sub_complexity; + } + } + } + } + + if (0 === $v->getContext()->getDepth()) { + $contents = []; + + \arsort($this->class_complexity); + + foreach ($this->class_complexity as $name => $complexity) { + $contents[] = new FixedWidthValue(new BaseContext($name), $complexity); + } + + if ($contents) { + $v->addRepresentation(new ContainerRepresentation('Class complexity', $contents), 0); + } + } + + $rep = new ProfileRepresentation($sub_complexity); + /** @psalm-suppress UnsupportedReferenceUsage */ + if ($v instanceof InstanceValue) { + $rep->instance_counts = &$this->instance_counts[$v->getSplObjectHash()]; + $rep->instance_complexity = &$this->instance_complexity[$v->getSplObjectHash()]; + } + + $v->addRepresentation($rep, 0); + + return $v; + } +} diff --git a/system/ThirdParty/Kint/Parser/ProxyPlugin.php b/system/ThirdParty/Kint/Parser/ProxyPlugin.php index 97050c6b4abf..e0db1b985708 100644 --- a/system/ThirdParty/Kint/Parser/ProxyPlugin.php +++ b/system/ThirdParty/Kint/Parser/ProxyPlugin.php @@ -27,25 +27,29 @@ namespace Kint\Parser; -use InvalidArgumentException; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\Context\ContextInterface; -class ProxyPlugin implements PluginInterface +/** + * @psalm-import-type ParserTrigger from Parser + * + * @psalm-api + */ +class ProxyPlugin implements PluginBeginInterface, PluginCompleteInterface { - protected $parser; - protected $types; - protected $triggers; + protected array $types; + /** @psalm-var ParserTrigger */ + protected int $triggers; + /** @psalm-var callable */ protected $callback; + private ?Parser $parser = null; /** - * @param callable $callback + * @psalm-param ParserTrigger $triggers + * @psalm-param callable $callback */ public function __construct(array $types, int $triggers, $callback) { - if (!\is_callable($callback)) { - throw new InvalidArgumentException('ProxyPlugin callback must be callable'); - } - $this->types = $types; $this->triggers = $triggers; $this->callback = $callback; @@ -66,8 +70,23 @@ public function getTriggers(): int return $this->triggers; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseBegin(&$var, ContextInterface $c): ?AbstractValue + { + return \call_user_func_array($this->callback, [ + &$var, + $c, + Parser::TRIGGER_BEGIN, + $this->parser, + ]); + } + + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { - \call_user_func_array($this->callback, [&$var, &$o, $trigger, $this->parser]); + return \call_user_func_array($this->callback, [ + &$var, + $v, + $trigger, + $this->parser, + ]); } } diff --git a/system/ThirdParty/Kint/Parser/SerializePlugin.php b/system/ThirdParty/Kint/Parser/SerializePlugin.php index 4991087848e6..a23aaae1af2c 100644 --- a/system/ThirdParty/Kint/Parser/SerializePlugin.php +++ b/system/ThirdParty/Kint/Parser/SerializePlugin.php @@ -27,10 +27,13 @@ namespace Kint\Parser; -use Kint\Zval\Representation\Representation; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\Context\BaseContext; +use Kint\Value\Representation\ValueRepresentation; +use Kint\Value\UninitializedValue; -class SerializePlugin extends AbstractPlugin +/** @psalm-api */ +class SerializePlugin extends AbstractPlugin implements PluginCompleteInterface { /** * Disables automatic unserialization on arrays and objects. @@ -43,13 +46,11 @@ class SerializePlugin extends AbstractPlugin * * The natural way to stop that from happening is to just refuse to unserialize * stuff by default. Which is what we're doing for anything that's not scalar. - * - * @var bool */ - public static $safe_mode = true; + public static bool $safe_mode = true; /** - * @var bool|class-string[] + * @psalm-var bool|class-string[] */ public static $allowed_classes = false; @@ -63,47 +64,48 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { $trimmed = \rtrim($var); if ('N;' !== $trimmed && !\preg_match('/^(?:[COabis]:\\d+[:;]|d:\\d+(?:\\.\\d+);)/', $trimmed)) { - return; + return $v; } $options = ['allowed_classes' => self::$allowed_classes]; - if (!self::$safe_mode || !\in_array($trimmed[0], ['C', 'O', 'a'], true)) { - // Suppress warnings on unserializeable variable - $data = @\unserialize($trimmed, $options); - - if (false === $data && 'b:0;' !== \substr($trimmed, 0, 4)) { - return; - } - } + $c = $v->getContext(); - $base_obj = new Value(); - $base_obj->depth = $o->depth + 1; - $base_obj->name = 'unserialize('.$o->name.')'; + $base = new BaseContext('unserialize('.$c->getName().')'); + $base->depth = $c->getDepth() + 1; - if ($o->access_path) { - $base_obj->access_path = 'unserialize('.$o->access_path; + if (null !== ($ap = $c->getAccessPath())) { + $base->access_path = 'unserialize('.$ap; if (true === self::$allowed_classes) { - $base_obj->access_path .= ')'; + $base->access_path .= ')'; } else { - $base_obj->access_path .= ', '.\var_export($options, true).')'; + $base->access_path .= ', '.\var_export($options, true).')'; } } - $r = new Representation('Serialized'); - - if (isset($data)) { - $r->contents = $this->parser->parse($data, $base_obj); + if (self::$safe_mode && \in_array($trimmed[0], ['C', 'O', 'a'], true)) { + $data = new UninitializedValue($base); + $data->flags |= AbstractValue::FLAG_BLACKLIST; } else { - $base_obj->hints[] = 'blacklist'; - $r->contents = $base_obj; + // Suppress warnings on unserializeable variable + $data = @\unserialize($trimmed, $options); + + if (false === $data && 'b:0;' !== \substr($trimmed, 0, 4)) { + return $v; + } + + $data = $this->getParser()->parse($data, $base); } - $o->addRepresentation($r, 0); + $data->flags |= AbstractValue::FLAG_GENERATED; + + $v->addRepresentation(new ValueRepresentation('Serialized', $data), 0); + + return $v; } } diff --git a/system/ThirdParty/Kint/Parser/SimpleXMLElementPlugin.php b/system/ThirdParty/Kint/Parser/SimpleXMLElementPlugin.php index db6f9b9c1c2b..4511c0b76f9e 100644 --- a/system/ThirdParty/Kint/Parser/SimpleXMLElementPlugin.php +++ b/system/ThirdParty/Kint/Parser/SimpleXMLElementPlugin.php @@ -27,20 +27,39 @@ namespace Kint\Parser; -use Kint\Zval\BlobValue; -use Kint\Zval\Representation\Representation; -use Kint\Zval\SimpleXMLElementValue; -use Kint\Zval\Value; +use Kint\Utils; +use Kint\Value\AbstractValue; +use Kint\Value\Context\ArrayContext; +use Kint\Value\Context\BaseContext; +use Kint\Value\Context\ClassOwnedContext; +use Kint\Value\Context\ContextInterface; +use Kint\Value\Representation\ContainerRepresentation; +use Kint\Value\Representation\ValueRepresentation; +use Kint\Value\SimpleXMLElementValue; use SimpleXMLElement; -class SimpleXMLElementPlugin extends AbstractPlugin +class SimpleXMLElementPlugin extends AbstractPlugin implements PluginBeginInterface { /** * Show all properties and methods. - * - * @var bool */ - public static $verbose = false; + public static bool $verbose = false; + + protected ClassMethodsPlugin $methods_plugin; + + public function __construct(Parser $parser) + { + parent::__construct($parser); + + $this->methods_plugin = new ClassMethodsPlugin($parser); + } + + public function setParser(Parser $p): void + { + parent::setParser($p); + + $this->methods_plugin->setParser($p); + } public function getTypes(): array { @@ -49,173 +68,228 @@ public function getTypes(): array public function getTriggers(): int { - return Parser::TRIGGER_SUCCESS; + // SimpleXMLElement is a weirdo. No recursion (Or rather everything is + // recursion) and depth limit will have to be handled manually anyway. + return Parser::TRIGGER_BEGIN; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseBegin(&$var, ContextInterface $c): ?AbstractValue { if (!$var instanceof SimpleXMLElement) { - return; + return null; } - if (!self::$verbose) { - $o->removeRepresentation('properties'); - $o->removeRepresentation('iterator'); - $o->removeRepresentation('methods'); - } + return $this->parseElement($var, $c); + } + + protected function parseElement(SimpleXMLElement &$var, ContextInterface $c): SimpleXMLElementValue + { + $parser = $this->getParser(); + $pdepth = $parser->getDepthLimit(); + $cdepth = $c->getDepth(); + + $depthlimit = $pdepth && $cdepth >= $pdepth; + $has_children = self::hasChildElements($var); - // An invalid SimpleXMLElement can gum up the works with - // warnings if we call stuff children/attributes on it. - if (!$var) { - $o->size = null; + if ($depthlimit && $has_children) { + $x = new SimpleXMLElementValue($c, $var, [], null); + $x->flags |= AbstractValue::FLAG_DEPTH_LIMIT; - return; + return $x; } - $x = new SimpleXMLElementValue(); - $x->transplant($o); + $children = $this->getChildren($c, $var); + $attributes = $this->getAttributes($c, $var); + $toString = (string) $var; + $string_body = !$has_children && \strlen($toString); + + $x = new SimpleXMLElementValue($c, $var, $children, \strlen($toString) ? $toString : null); + + if (self::$verbose) { + $x = $this->methods_plugin->parseComplete($var, $x, Parser::TRIGGER_SUCCESS); + } - $namespaces = \array_merge([null], $var->getDocNamespaces()); + if ($attributes) { + $x->addRepresentation(new ContainerRepresentation('Attributes', $attributes), 0); + } - // Attributes - $a = new Representation('Attributes'); + if ($string_body) { + $base = new BaseContext('(string) '.$c->getName()); + $base->depth = $cdepth + 1; + if (null !== ($ap = $c->getAccessPath())) { + $base->access_path = '(string) '.$ap; + } - $base_obj = new Value(); - $base_obj->depth = $x->depth; + $toString = $parser->parse($toString, $base); - if ($x->access_path) { - $base_obj->access_path = '(string) '.$x->access_path; + $x->addRepresentation(new ValueRepresentation('toString', $toString, null, true), 0); } - // Attributes are strings. If we're too deep set the - // depth limit to enable parsing them, but no deeper. - if ($this->parser->getDepthLimit() && $this->parser->getDepthLimit() - 2 < $base_obj->depth) { - $base_obj->depth = $this->parser->getDepthLimit() - 2; + if ($children) { + $x->addRepresentation(new ContainerRepresentation('Children', $children), 0); } - $attribs = []; + return $x; + } - foreach ($namespaces as $nsAlias => $nsUrl) { - if ($nsAttribs = $var->attributes($nsUrl)) { - $cleanAttribs = []; + /** @psalm-return list */ + protected function getAttributes(ContextInterface $c, SimpleXMLElement $var): array + { + $parser = $this->getParser(); + $namespaces = \array_merge(['' => null], $var->getDocNamespaces()); + + $cdepth = $c->getDepth(); + $ap = $c->getAccessPath(); + + $contents = []; + + foreach ($namespaces as $nsAlias => $_) { + if ((bool) $nsAttribs = $var->attributes($nsAlias, true)) { foreach ($nsAttribs as $name => $attrib) { - $cleanAttribs[(string) $name] = $attrib; - } + $obj = new ArrayContext($name); + $obj->depth = $cdepth + 1; - if (null === $nsUrl) { - $obj = clone $base_obj; - if ($obj->access_path) { - $obj->access_path .= '->attributes()'; + if (null !== $ap) { + $obj->access_path = '(string) '.$ap; + if ('' !== $nsAlias) { + $obj->access_path .= '->attributes('.\var_export($nsAlias, true).', true)'; + } + $obj->access_path .= '['.\var_export($name, true).']'; } - $a->contents = $this->parser->parse($cleanAttribs, $obj)->value->contents; - } else { - $obj = clone $base_obj; - if ($obj->access_path) { - $obj->access_path .= '->attributes('.\var_export($nsAlias, true).', true)'; + if ('' !== $nsAlias) { + $obj->name = $nsAlias.':'.$obj->name; } - $cleanAttribs = $this->parser->parse($cleanAttribs, $obj)->value->contents; + $string = (string) $attrib; + $attribute = $parser->parse($string, $obj); - foreach ($cleanAttribs as $attribute) { - $attribute->name = $nsAlias.':'.$attribute->name; - $a->contents[] = $attribute; - } + $contents[] = $attribute; } } } - if ($a->contents) { - $x->addRepresentation($a, 0); - } + return $contents; + } - // Children - $c = new Representation('Children'); + /** + * Alright kids, let's learn about SimpleXMLElement::children! + * children can take a namespace url or alias and provide a list of + * child nodes. This is great since just accessing the members through + * properties doesn't work on SimpleXMLElement when they have a + * namespace at all! + * + * Unfortunately SimpleXML decided to go the retarded route of + * categorizing elements by their tag name rather than by their local + * name (to put it in Dom terms) so if you have something like this: + * + * + * + * + * + * + * + * * children(null) will get the first 2 results + * * children('', true) will get the first 2 results + * * children('http://localhost/') will get the last 2 results + * * children('localhost', true) will get the last result + * + * So let's just give up and stick to aliases because fuck that mess! + * + * @psalm-return list + */ + protected function getChildren(ContextInterface $c, SimpleXMLElement $var): array + { + $namespaces = \array_merge(['' => null], $var->getDocNamespaces()); - foreach ($namespaces as $nsAlias => $nsUrl) { - // This is doubling items because of the root namespace - // and the implicit namespace on its children. - $thisNs = $var->getNamespaces(); - if (isset($thisNs['']) && $thisNs[''] === $nsUrl) { - continue; - } + $cdepth = $c->getDepth(); + $ap = $c->getAccessPath(); - if ($nsChildren = $var->children($nsUrl)) { + $contents = []; + + foreach ($namespaces as $nsAlias => $_) { + if ((bool) $nsChildren = $var->children($nsAlias, true)) { $nsap = []; foreach ($nsChildren as $name => $child) { - $obj = new Value(); - $obj->depth = $x->depth + 1; - $obj->name = (string) $name; - if ($x->access_path) { - if (null === $nsUrl) { - $obj->access_path = $x->access_path.'->children()->'; + $base = new ClassOwnedContext((string) $name, SimpleXMLElement::class); + $base->depth = $cdepth + 1; + + if ('' !== $nsAlias) { + $base->name = $nsAlias.':'.$name; + } + + if (null !== $ap) { + if ('' === $nsAlias) { + $base->access_path = $ap.'->'; } else { - $obj->access_path = $x->access_path.'->children('.\var_export($nsAlias, true).', true)->'; + $base->access_path = $ap.'->children('.\var_export($nsAlias, true).', true)->'; } - if (\preg_match('/^[a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff]+$/', (string) $name)) { - $obj->access_path .= (string) $name; + if (Utils::isValidPhpName((string) $name)) { + $base->access_path .= (string) $name; } else { - $obj->access_path .= '{'.\var_export((string) $name, true).'}'; + $base->access_path .= '{'.\var_export((string) $name, true).'}'; } - if (isset($nsap[$obj->access_path])) { - ++$nsap[$obj->access_path]; - $obj->access_path .= '['.$nsap[$obj->access_path].']'; + if (isset($nsap[$base->access_path])) { + ++$nsap[$base->access_path]; + $base->access_path .= '['.$nsap[$base->access_path].']'; } else { - $nsap[$obj->access_path] = 0; + $nsap[$base->access_path] = 0; } } - $value = $this->parser->parse($child, $obj); - - if ($value->access_path && 'string' === $value->type) { - $value->access_path = '(string) '.$value->access_path; - } - - $c->contents[] = $value; + $v = $this->parseElement($child, $base); + $v->flags |= AbstractValue::FLAG_GENERATED; + $contents[] = $v; } } } - $x->size = \count($c->contents); - - if ($x->size) { - $x->addRepresentation($c, 0); - } else { - $x->size = null; - - if (\strlen((string) $var)) { - $base_obj = new BlobValue(); - $base_obj->depth = $x->depth + 1; - $base_obj->name = $x->name; - if ($x->access_path) { - $base_obj->access_path = '(string) '.$x->access_path; - } - - $value = (string) $var; - - $s = $this->parser->parse($value, $base_obj); - $srep = $s->getRepresentation('contents'); - $svalrep = $s->value && 'contents' == $s->value->getName() ? $s->value : null; - - if ($srep || $svalrep) { - $x->setIsStringValue(true); - $x->value = $srep ?: $svalrep; - - if ($srep) { - $x->replaceRepresentation($srep, 0); - } - } + return $contents; + } - $reps = \array_reverse($s->getRepresentations()); + /** + * More SimpleXMLElement bullshit. + * + * If we want to know if the element contains text we can cast to string. + * Except if it contains text mixed with elements simplexml for some stupid + * reason decides to concatenate the text from between those elements + * rather than all the text in the hierarchy... + * + * So we have NO way of getting text nodes between elements, but we can + * still tell if we have elements right? If we have elements we assume it's + * not a string and call it a day! + * + * Well if you cast the element to an array attributes will be on it so + * you'd have to remove that key, and if it's a string it'll also have the + * 0 index used for the string contents too... + * + * Wait, can we use the 0 index to tell if it's a string? Nope! CDATA + * doesn't show up AT ALL when casting to anything but string, and we'll + * still get those concatenated strings of mostly whitespace if we just do + * (string) and check the length. + * + * Luckily, I found the only way to do this reliably is through children(). + * We still have to loop through all the namespaces and see if there's a + * match but then we have the problem of the attributes showing up again... + * + * Or at least that's what var_dump says. And when we cast the result to + * bool it's true too... But if we cast it to array then it's suddenly empty! + * + * Long story short the function below is the only way to reliably check if + * a SimpleXMLElement has children + */ + protected static function hasChildElements(SimpleXMLElement $var): bool + { + $namespaces = \array_merge(['' => null], $var->getDocNamespaces()); - foreach ($reps as $rep) { - $x->addRepresentation($rep, 0); - } + foreach ($namespaces as $nsAlias => $_) { + if ((array) $var->children($nsAlias, true)) { + return true; } } - $o = $x; + return false; } } diff --git a/system/ThirdParty/Kint/Parser/SplFileInfoPlugin.php b/system/ThirdParty/Kint/Parser/SplFileInfoPlugin.php index 696f36007013..c5eea6c1e8d5 100644 --- a/system/ThirdParty/Kint/Parser/SplFileInfoPlugin.php +++ b/system/ThirdParty/Kint/Parser/SplFileInfoPlugin.php @@ -27,12 +27,14 @@ namespace Kint\Parser; -use Kint\Zval\Representation\SplFileInfoRepresentation; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\InstanceValue; +use Kint\Value\Representation\SplFileInfoRepresentation; +use Kint\Value\SplFileInfoValue; use SplFileInfo; use SplFileObject; -class SplFileInfoPlugin extends AbstractPlugin +class SplFileInfoPlugin extends AbstractPlugin implements PluginCompleteInterface { public function getTypes(): array { @@ -41,17 +43,26 @@ public function getTypes(): array public function getTriggers(): int { - return Parser::TRIGGER_COMPLETE; + return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { + // SplFileObject throws exceptions in normal use in places SplFileInfo doesn't if (!$var instanceof SplFileInfo || $var instanceof SplFileObject) { - return; + return $v; } - $r = new SplFileInfoRepresentation(clone $var); - $o->addRepresentation($r, 0); - $o->size = $r->getSize(); + if (!$v instanceof InstanceValue) { + return $v; + } + + $out = new SplFileInfoValue($v->getContext(), $var); + $out->setChildren($v->getChildren()); + $out->flags = $v->flags; + $out->addRepresentation(new SplFileInfoRepresentation(clone $var)); + $out->appendRepresentations($v->getRepresentations()); + + return $out; } } diff --git a/system/ThirdParty/Kint/Parser/StreamPlugin.php b/system/ThirdParty/Kint/Parser/StreamPlugin.php index 748b53c51997..fdbc2573bc5a 100644 --- a/system/ThirdParty/Kint/Parser/StreamPlugin.php +++ b/system/ThirdParty/Kint/Parser/StreamPlugin.php @@ -27,12 +27,13 @@ namespace Kint\Parser; -use Kint\Zval\Representation\Representation; -use Kint\Zval\ResourceValue; -use Kint\Zval\StreamValue; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\Context\ArrayContext; +use Kint\Value\ResourceValue; +use Kint\Value\StreamValue; +use TypeError; -class StreamPlugin extends AbstractPlugin +class StreamPlugin extends AbstractPlugin implements PluginCompleteInterface { public function getTypes(): array { @@ -44,40 +45,44 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { - if (!$o instanceof ResourceValue || 'stream' !== $o->resource_type) { - return; + if (!$v instanceof ResourceValue) { + return $v; } // Doublecheck that the resource is open before we get the metadata if (!\is_resource($var)) { - return; + return $v; } - $meta = \stream_get_meta_data($var); - - $rep = new Representation('Stream'); - $rep->implicit_label = true; + try { + $meta = \stream_get_meta_data($var); + } catch (TypeError $e) { + return $v; + } - $base_obj = new Value(); - $base_obj->depth = $o->depth; + $c = $v->getContext(); - if ($o->access_path) { - $base_obj->access_path = 'stream_get_meta_data('.$o->access_path.')'; - } + $parser = $this->getParser(); + $parsed_meta = []; + foreach ($meta as $key => $val) { + $base = new ArrayContext($key); + $base->depth = $c->getDepth() + 1; - $rep->contents = $this->parser->parse($meta, $base_obj); + if (null !== ($ap = $c->getAccessPath())) { + $base->access_path = 'stream_get_meta_data('.$ap.')['.\var_export($key, true).']'; + } - if (!\in_array('depth_limit', $rep->contents->hints, true)) { - $rep->contents = $rep->contents->value->contents; + $val = $parser->parse($val, $base); + $val->flags |= AbstractValue::FLAG_GENERATED; + $parsed_meta[] = $val; } - $o->addRepresentation($rep, 0); - $o->value = $rep; + $stream = new StreamValue($c, $parsed_meta, $meta['uri'] ?? null); + $stream->flags = $v->flags; + $stream->appendRepresentations($v->getRepresentations()); - $stream = new StreamValue($meta); - $stream->transplant($o); - $o = $stream; + return $stream; } } diff --git a/system/ThirdParty/Kint/Parser/TablePlugin.php b/system/ThirdParty/Kint/Parser/TablePlugin.php index 5afe75980bc8..8bbda7f7b181 100644 --- a/system/ThirdParty/Kint/Parser/TablePlugin.php +++ b/system/ThirdParty/Kint/Parser/TablePlugin.php @@ -27,8 +27,9 @@ namespace Kint\Parser; -use Kint\Zval\Representation\Representation; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\ArrayValue; +use Kint\Value\Representation\TableRepresentation; // Note: Interaction with ArrayLimitPlugin: // Any array limited children will be shown in tables identically to @@ -36,8 +37,11 @@ // and it's size anyway. Because ArrayLimitPlugin halts the parse on finding // a limit all other plugins including this one are stopped, so you cannot get // a tabular representation of an array that is longer than the limit. -class TablePlugin extends AbstractPlugin +class TablePlugin extends AbstractPlugin implements PluginCompleteInterface { + public static int $max_width = 300; + public static int $min_width = 2; + public function getTypes(): array { return ['array']; @@ -48,48 +52,52 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { - if (empty($o->value->contents)) { - return; + if (!$v instanceof ArrayValue) { + return $v; } - $array = $this->parser->getCleanArray($var); - - if (\count($array) < 2) { - return; + if (\count($var) < 2) { + return $v; } // Ensure this is an array of arrays and that all child arrays have the // same keys. We don't care about their children - if there's another // "table" inside we'll just make another one down the value tab $keys = null; - foreach ($array as $elem) { - if (!\is_array($elem) || \count($elem) < 2) { - return; + foreach ($var as $elem) { + if (!\is_array($elem)) { + return $v; } if (null === $keys) { + if (\count($elem) < self::$min_width || \count($elem) > self::$max_width) { + return $v; + } + $keys = \array_keys($elem); } elseif (\array_keys($elem) !== $keys) { - return; + return $v; } } + $children = $v->getContents(); + + if (!$children) { + return $v; + } + // Ensure none of the child arrays are recursion or depth limit. We // don't care if their children are since they are the table cells - foreach ($o->value->contents as $childarray) { - if (empty($childarray->value->contents)) { - return; + foreach ($children as $childarray) { + if (!$childarray instanceof ArrayValue || empty($childarray->getContents())) { + return $v; } } - // Objects by reference for the win! We can do a copy-paste of the value - // representation contents and just slap a new hint on there and hey - // presto we have our table representation with no extra memory used! - $table = new Representation('Table'); - $table->contents = $o->value->contents; - $table->hints[] = 'table'; - $o->addRepresentation($table, 0); + $v->addRepresentation(new TableRepresentation($children), 0); + + return $v; } } diff --git a/system/ThirdParty/Kint/Parser/ThrowablePlugin.php b/system/ThirdParty/Kint/Parser/ThrowablePlugin.php index 7fd065f61910..c314fafb6c18 100644 --- a/system/ThirdParty/Kint/Parser/ThrowablePlugin.php +++ b/system/ThirdParty/Kint/Parser/ThrowablePlugin.php @@ -27,12 +27,14 @@ namespace Kint\Parser; -use Kint\Zval\Representation\SourceRepresentation; -use Kint\Zval\ThrowableValue; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\InstanceValue; +use Kint\Value\Representation\SourceRepresentation; +use Kint\Value\ThrowableValue; +use RuntimeException; use Throwable; -class ThrowablePlugin extends AbstractPlugin +class ThrowablePlugin extends AbstractPlugin implements PluginCompleteInterface { public function getTypes(): array { @@ -44,18 +46,22 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { - if (!$var instanceof Throwable) { - return; + if (!$var instanceof Throwable || !$v instanceof InstanceValue) { + return $v; } - $throw = new ThrowableValue($var); - $throw->transplant($o); - $r = new SourceRepresentation($var->getFile(), $var->getLine()); - $r->showfilename = true; - $throw->addRepresentation($r, 0); + $throw = new ThrowableValue($v->getContext(), $var); + $throw->setChildren($v->getChildren()); + $throw->flags = $v->flags; + $throw->appendRepresentations($v->getRepresentations()); - $o = $throw; + try { + $throw->addRepresentation(new SourceRepresentation($var->getFile(), $var->getLine(), null, true), 0); + } catch (RuntimeException $e) { + } + + return $throw; } } diff --git a/system/ThirdParty/Kint/Parser/TimestampPlugin.php b/system/ThirdParty/Kint/Parser/TimestampPlugin.php index 9136ca800d49..da3506fe7c62 100644 --- a/system/ThirdParty/Kint/Parser/TimestampPlugin.php +++ b/system/ThirdParty/Kint/Parser/TimestampPlugin.php @@ -27,11 +27,15 @@ namespace Kint\Parser; -use Kint\Zval\Value; +use DateTimeImmutable; +use Kint\Value\AbstractValue; +use Kint\Value\FixedWidthValue; +use Kint\Value\Representation\StringRepresentation; +use Kint\Value\StringValue; -class TimestampPlugin extends AbstractPlugin +class TimestampPlugin extends AbstractPlugin implements PluginCompleteInterface { - public static $blacklist = [ + public static array $blacklist = [ 2147483648, 2147483647, 1073741824, @@ -48,30 +52,38 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { if (\is_string($var) && !\ctype_digit($var)) { - return; + return $v; } if ($var < 0) { - return; + return $v; } if (\in_array($var, self::$blacklist, true)) { - return; + return $v; } $len = \strlen((string) $var); // Guess for anything between March 1973 and November 2286 - if (9 === $len || 10 === $len) { - // If it's an int or string that's this short it probably has no other meaning - // Additionally it's highly unlikely the shortValue will be clipped for length - // If you're writing a plugin that interferes with this, just put your - // parser plugin further down the list so that it gets loaded afterwards. - $o->value->label = 'Timestamp'; - $o->value->hints[] = 'timestamp'; + if ($len < 9 || $len > 10) { + return $v; } + + if (!$v instanceof StringValue && !$v instanceof FixedWidthValue) { + return $v; + } + + if (!$dt = DateTimeImmutable::createFromFormat('U', (string) $var)) { + return $v; + } + + $v->removeRepresentation('contents'); + $v->addRepresentation(new StringRepresentation('Timestamp', $dt->format('c'), null, true)); + + return $v; } } diff --git a/system/ThirdParty/Kint/Parser/ToStringPlugin.php b/system/ThirdParty/Kint/Parser/ToStringPlugin.php index 478442bebb73..6fa7bd4e2b33 100644 --- a/system/ThirdParty/Kint/Parser/ToStringPlugin.php +++ b/system/ThirdParty/Kint/Parser/ToStringPlugin.php @@ -27,15 +27,19 @@ namespace Kint\Parser; -use Kint\Zval\Representation\Representation; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; +use Kint\Value\Context\BaseContext; +use Kint\Value\Representation\ValueRepresentation; use ReflectionClass; +use SimpleXMLElement; +use SplFileInfo; +use Throwable; -class ToStringPlugin extends AbstractPlugin +class ToStringPlugin extends AbstractPlugin implements PluginCompleteInterface { - public static $blacklist = [ - 'SimpleXMLElement', - 'SplFileObject', + public static array $blacklist = [ + SimpleXMLElement::class, + SplFileInfo::class, ]; public function getTypes(): array @@ -48,22 +52,37 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { $reflection = new ReflectionClass($var); if (!$reflection->hasMethod('__toString')) { - return; + return $v; } foreach (self::$blacklist as $class) { if ($var instanceof $class) { - return; + return $v; } } - $r = new Representation('toString'); - $r->contents = (string) $var; + try { + $string = (string) $var; + } catch (Throwable $t) { + return $v; + } + + $c = $v->getContext(); + + $base = new BaseContext($c->getName()); + $base->depth = $c->getDepth() + 1; + if (null !== ($ap = $c->getAccessPath())) { + $base->access_path = '(string) '.$ap; + } + + $string = $this->getParser()->parse($string, $base); + + $v->addRepresentation(new ValueRepresentation('toString', $string)); - $o->addRepresentation($r); + return $v; } } diff --git a/system/ThirdParty/Kint/Parser/TracePlugin.php b/system/ThirdParty/Kint/Parser/TracePlugin.php index a5f47dcfcdf9..533456472609 100644 --- a/system/ThirdParty/Kint/Parser/TracePlugin.php +++ b/system/ThirdParty/Kint/Parser/TracePlugin.php @@ -28,14 +28,23 @@ namespace Kint\Parser; use Kint\Utils; -use Kint\Zval\TraceFrameValue; -use Kint\Zval\TraceValue; -use Kint\Zval\Value; - -class TracePlugin extends AbstractPlugin +use Kint\Value\AbstractValue; +use Kint\Value\ArrayValue; +use Kint\Value\Context\ArrayContext; +use Kint\Value\Representation\ContainerRepresentation; +use Kint\Value\Representation\SourceRepresentation; +use Kint\Value\Representation\ValueRepresentation; +use Kint\Value\TraceFrameValue; +use Kint\Value\TraceValue; +use RuntimeException; + +/** + * @psalm-import-type TraceFrame from TraceFrameValue + */ +class TracePlugin extends AbstractPlugin implements PluginCompleteInterface { - public static $blacklist = ['spl_autoload_call']; - public static $path_blacklist = []; + public static array $blacklist = ['spl_autoload_call']; + public static array $path_blacklist = []; public function getTypes(): array { @@ -47,42 +56,46 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { - if (!$o->value) { - return; + if (!$v instanceof ArrayValue) { + return $v; } - $trace = $this->parser->getCleanArray($var); + // Shallow copy so we don't have to worry about touching var + $trace = $var; - if (\count($trace) !== \count($o->value->contents) || !Utils::isTrace($trace)) { - return; + if (!Utils::isTrace($trace)) { + return $v; } - $traceobj = new TraceValue(); - $traceobj->transplant($o); - $rep = $traceobj->value; + $pdepth = $this->getParser()->getDepthLimit(); + $c = $v->getContext(); - $old_trace = $rep->contents; + // We need at least 2 levels in order to get $trace[n]['args'] + if ($pdepth && $c->getDepth() + 2 >= $pdepth) { + return $v; + } - Utils::normalizeAliases(self::$blacklist); - $path_blacklist = self::normalizePaths(self::$path_blacklist); + $contents = $v->getContents(); - $rep->contents = []; + self::$blacklist = Utils::normalizeAliases(self::$blacklist); + $path_blacklist = self::normalizePaths(self::$path_blacklist); - foreach ($old_trace as $frame) { - $index = $frame->name; + $frames = []; - if (!isset($trace[$index]['function'])) { - // Something's very very wrong here, but it's probably a plugin's fault + foreach ($contents as $frame) { + if (!$frame instanceof ArrayValue || !$frame->getContext() instanceof ArrayContext) { continue; } - if (Utils::traceFrameIsListed($trace[$index], self::$blacklist)) { + $index = $frame->getContext()->getName(); + + if (!isset($trace[$index]) || Utils::traceFrameIsListed($trace[$index], self::$blacklist)) { continue; } - if (isset($trace[$index]['file']) && ($realfile = \realpath($trace[$index]['file']))) { + if (isset($trace[$index]['file']) && false !== ($realfile = \realpath($trace[$index]['file']))) { foreach ($path_blacklist as $path) { if (0 === \strpos($realfile, $path)) { continue 2; @@ -90,16 +103,39 @@ public function parse(&$var, Value &$o, int $trigger): void } } - $rep->contents[$index] = new TraceFrameValue($frame, $trace[$index]); + $frame = new TraceFrameValue($frame, $trace[$index]); + + if (null !== ($file = $frame->getFile()) && null !== ($line = $frame->getLine())) { + try { + $frame->addRepresentation(new SourceRepresentation($file, $line)); + } catch (RuntimeException $e) { + } + } + + if ($args = $frame->getArgs()) { + $frame->addRepresentation(new ContainerRepresentation('Arguments', $args)); + } + + if ($obj = $frame->getObject()) { + $frame->addRepresentation( + new ValueRepresentation( + 'Callee object ['.$obj->getClassName().']', + $obj, + 'callee_object' + ) + ); + } + + $frames[$index] = $frame; } - \ksort($rep->contents); - $rep->contents = \array_values($rep->contents); + $traceobj = new TraceValue($c, \count($frames), $frames); + + if ($frames) { + $traceobj->addRepresentation(new ContainerRepresentation('Contents', $frames, null, true)); + } - $traceobj->clearRepresentations(); - $traceobj->addRepresentation($rep); - $traceobj->size = \count($rep->contents); - $o = $traceobj; + return $traceobj; } protected static function normalizePaths(array $paths): array diff --git a/system/ThirdParty/Kint/Parser/XmlPlugin.php b/system/ThirdParty/Kint/Parser/XmlPlugin.php index a5a31abd4361..64275a2e9740 100644 --- a/system/ThirdParty/Kint/Parser/XmlPlugin.php +++ b/system/ThirdParty/Kint/Parser/XmlPlugin.php @@ -27,12 +27,19 @@ namespace Kint\Parser; +use Dom\Node; +use Dom\XMLDocument; use DOMDocument; -use Exception; -use Kint\Zval\Representation\Representation; -use Kint\Zval\Value; - -class XmlPlugin extends AbstractPlugin +use DOMException; +use DOMNode; +use InvalidArgumentException; +use Kint\Value\AbstractValue; +use Kint\Value\Context\BaseContext; +use Kint\Value\Context\ContextInterface; +use Kint\Value\Representation\ValueRepresentation; +use Throwable; + +class XmlPlugin extends AbstractPlugin implements PluginCompleteInterface { /** * Which method to parse the variable with. @@ -41,9 +48,9 @@ class XmlPlugin extends AbstractPlugin * however it's memory usage is very high and it takes longer to parse and * render. Plus it's a pain to work with. So SimpleXML is the default. * - * @var string + * @psalm-var 'SimpleXML'|'DOMDocument'|'XMLDocument' */ - public static $parse_method = 'SimpleXML'; + public static string $parse_method = 'SimpleXML'; public function getTypes(): array { @@ -55,59 +62,54 @@ public function getTriggers(): int return Parser::TRIGGER_SUCCESS; } - public function parse(&$var, Value &$o, int $trigger): void + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue { if ('access_path); + $c = $v->getContext(); - if (empty($xml)) { - return; - } + $out = \call_user_func([$this, 'xmlTo'.self::$parse_method], $var, $c); - [$xml, $access_path, $name] = $xml; + if (null === $out) { + return $v; + } - $base_obj = new Value(); - $base_obj->depth = $o->depth + 1; - $base_obj->name = $name; - $base_obj->access_path = $access_path; + $out->flags |= AbstractValue::FLAG_GENERATED; - $r = new Representation('XML'); - $r->contents = $this->parser->parse($xml, $base_obj); + $v->addRepresentation(new ValueRepresentation('XML', $out), 0); - $o->addRepresentation($r, 0); + return $v; } - protected static function xmlToSimpleXML(string $var, ?string $parent_path): ?array + /** @psalm-suppress PossiblyUnusedMethod */ + protected function xmlToSimpleXML(string $var, ContextInterface $c): ?AbstractValue { $errors = \libxml_use_internal_errors(true); try { $xml = \simplexml_load_string($var); - } catch (Exception $e) { + if (!(bool) $xml) { + throw new InvalidArgumentException('Bad XML parse in XmlPlugin::xmlToSimpleXML'); + } + } catch (Throwable $t) { return null; } finally { \libxml_use_internal_errors($errors); + \libxml_clear_errors(); } - if (false === $xml) { - return null; - } - - if (null === $parent_path) { - $access_path = null; - } else { - $access_path = 'simplexml_load_string('.$parent_path.')'; + $base = new BaseContext($xml->getName()); + $base->depth = $c->getDepth() + 1; + if (null !== ($ap = $c->getAccessPath())) { + $base->access_path = 'simplexml_load_string('.$ap.')'; } - $name = $xml->getName(); - - return [$xml, $access_path, $name]; + return $this->getParser()->parse($xml, $base); } /** @@ -115,38 +117,63 @@ protected static function xmlToSimpleXML(string $var, ?string $parent_path): ?ar * * If it errors loading then we wouldn't have gotten this far in the first place. * - * @psalm-param non-empty-string $var The XML string - * - * @param ?string $parent_path The path to the parent, in this case the XML string + * @psalm-suppress PossiblyUnusedMethod * - * @return ?array The root element DOMNode, the access path, and the root element name + * @psalm-param non-empty-string $var */ - protected static function xmlToDOMDocument(string $var, ?string $parent_path): ?array + protected function xmlToDOMDocument(string $var, ContextInterface $c): ?AbstractValue { - // There's no way to check validity in DOMDocument without making errors. For shame! - if (!self::xmlToSimpleXML($var, $parent_path)) { + try { + $xml = new DOMDocument(); + $check = $xml->loadXML($var, LIBXML_NOWARNING | LIBXML_NOERROR); + + if (false === $check) { + throw new InvalidArgumentException('Bad XML parse in XmlPlugin::xmlToDOMDocument'); + } + } catch (Throwable $t) { return null; } - $xml = new DOMDocument(); - $xml->loadXML($var); + $xml = $xml->firstChild; - if ($xml->childNodes->count() > 1) { - $xml = $xml->childNodes; - $access_path = 'childNodes'; - } else { - $xml = $xml->firstChild; - $access_path = 'firstChild'; + /** + * @psalm-var DOMNode $xml + * Psalm bug #11120 + */ + $base = new BaseContext($xml->nodeName); + $base->depth = $c->getDepth() + 1; + if (null !== ($ap = $c->getAccessPath())) { + $base->access_path = '(function($s){$x = new \\DomDocument(); $x->loadXML($s); return $x;})('.$ap.')->firstChild'; } - if (null === $parent_path) { - $access_path = null; - } else { - $access_path = '(function($s){$x = new \\DomDocument(); $x->loadXML($s); return $x;})('.$parent_path.')->'.$access_path; + return $this->getParser()->parse($xml, $base); + } + + /** @psalm-suppress PossiblyUnusedMethod */ + protected function xmlToXMLDocument(string $var, ContextInterface $c): ?AbstractValue + { + if (!KINT_PHP84) { + return null; // @codeCoverageIgnore } - $name = $xml->nodeName ?? null; + try { + $xml = XMLDocument::createFromString($var, LIBXML_NOWARNING | LIBXML_NOERROR); + } catch (DOMException $e) { + return null; + } + + $xml = $xml->firstChild; + + /** + * @psalm-var Node $xml + * Psalm bug #11120 + */ + $base = new BaseContext($xml->nodeName); + $base->depth = $c->getDepth() + 1; + if (null !== ($ap = $c->getAccessPath())) { + $base->access_path = '\\Dom\\XMLDocument::createFromString('.$ap.')->firstChild'; + } - return [$xml, $access_path, $name]; + return $this->getParser()->parse($xml, $base); } } diff --git a/system/ThirdParty/Kint/Renderer/AbstractRenderer.php b/system/ThirdParty/Kint/Renderer/AbstractRenderer.php index adec8f071d97..fb4597c2d01f 100644 --- a/system/ThirdParty/Kint/Renderer/AbstractRenderer.php +++ b/system/ThirdParty/Kint/Renderer/AbstractRenderer.php @@ -27,67 +27,38 @@ namespace Kint\Renderer; -use Kint\Zval\InstanceValue; -use Kint\Zval\Value; - -/** - * @psalm-type PluginMap array - * - * @psalm-consistent-constructor - */ -abstract class AbstractRenderer implements RendererInterface +abstract class AbstractRenderer implements ConstructableRendererInterface { - public const SORT_NONE = 0; - public const SORT_VISIBILITY = 1; - public const SORT_FULL = 2; - - protected $call_info = []; - protected $statics = []; - protected $show_trace = true; - - public function setCallInfo(array $info): void - { - if (!isset($info['modifiers']) || !\is_array($info['modifiers'])) { - $info['modifiers'] = []; - } + public static ?string $js_nonce = null; + public static ?string $css_nonce = null; - if (!isset($info['trace']) || !\is_array($info['trace'])) { - $info['trace'] = []; - } + /** @psalm-var ?non-empty-string */ + public static ?string $file_link_format = null; - $this->call_info = [ - 'params' => $info['params'] ?? null, - 'modifiers' => $info['modifiers'], - 'callee' => $info['callee'] ?? null, - 'caller' => $info['caller'] ?? null, - 'trace' => $info['trace'], - ]; - } + protected bool $show_trace = true; + protected ?array $callee = null; + protected array $trace = []; - public function getCallInfo(): array - { - return $this->call_info; - } + protected bool $render_spl_ids = true; - public function setStatics(array $statics): void + public function __construct() { - $this->statics = $statics; - $this->setShowTrace(!empty($statics['display_called_from'])); } - public function getStatics(): array + public function shouldRenderObjectIds(): bool { - return $this->statics; + return $this->render_spl_ids; } - public function setShowTrace(bool $show_trace): void + public function setCallInfo(array $info): void { - $this->show_trace = $show_trace; + $this->callee = $info['callee'] ?? null; + $this->trace = $info['trace'] ?? []; } - public function getShowTrace(): bool + public function setStatics(array $statics): void { - return $this->show_trace; + $this->show_trace = !empty($statics['display_called_from']); } public function filterParserPlugins(array $plugins): array @@ -105,71 +76,12 @@ public function postRender(): string return ''; } - /** - * Returns the first compatible plugin available. - * - * @psalm-param PluginMap $plugins Array of hints to class strings - * @psalm-param string[] $hints Array of object hints - * - * @psalm-return PluginMap Array of hints to class strings filtered and sorted by object hints - */ - public function matchPlugins(array $plugins, array $hints): array - { - $out = []; - - foreach ($hints as $key) { - if (isset($plugins[$key])) { - $out[$key] = $plugins[$key]; - } - } - - return $out; - } - - public static function sortPropertiesFull(Value $a, Value $b): int + public static function getFileLink(string $file, int $line): ?string { - $sort = Value::sortByAccess($a, $b); - if ($sort) { - return $sort; + if (null === self::$file_link_format) { + return null; } - $sort = Value::sortByName($a, $b); - if ($sort) { - return $sort; - } - - return InstanceValue::sortByHierarchy($a->owner_class, $b->owner_class); - } - - /** - * Sorts an array of Value. - * - * @param Value[] $contents Object properties to sort - * - * @return Value[] - */ - public static function sortProperties(array $contents, int $sort): array - { - switch ($sort) { - case self::SORT_VISIBILITY: - // Containers to quickly stable sort by type - $containers = [ - Value::ACCESS_PUBLIC => [], - Value::ACCESS_PROTECTED => [], - Value::ACCESS_PRIVATE => [], - Value::ACCESS_NONE => [], - ]; - - foreach ($contents as $item) { - $containers[$item->access][] = $item; - } - - return \call_user_func_array('array_merge', $containers); - case self::SORT_FULL: - \usort($contents, [self::class, 'sortPropertiesFull']); - // no break - default: - return $contents; - } + return \str_replace(['%f', '%l'], [$file, $line], self::$file_link_format); } } diff --git a/system/ThirdParty/Kint/Zval/InstanceValue.php b/system/ThirdParty/Kint/Renderer/AssetRendererTrait.php similarity index 55% rename from system/ThirdParty/Kint/Zval/InstanceValue.php rename to system/ThirdParty/Kint/Renderer/AssetRendererTrait.php index 323733474825..11bf9547c4b4 100644 --- a/system/ThirdParty/Kint/Zval/InstanceValue.php +++ b/system/ThirdParty/Kint/Renderer/AssetRendererTrait.php @@ -25,50 +25,40 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace Kint\Zval; +namespace Kint\Renderer; -class InstanceValue extends Value +trait AssetRendererTrait { - public $type = 'object'; - public $classname; - public $spl_object_hash; - public $spl_object_id = null; - public $filename; - public $startline; - public $hints = ['object']; + public static ?string $theme = null; - public function getType(): ?string - { - return $this->classname; - } + /** @psalm-var array{js?:string, css?:array} */ + private static array $assetCache = []; - public function transplant(Value $old): void + /** @psalm-api */ + public static function renderJs(): string { - parent::transplant($old); - - if ($old instanceof self) { - $this->classname = $old->classname; - $this->spl_object_hash = $old->spl_object_hash; - $this->spl_object_id = $old->spl_object_id; - $this->filename = $old->filename; - $this->startline = $old->startline; + if (!isset(self::$assetCache['js'])) { + self::$assetCache['js'] = \file_get_contents(KINT_DIR.'/resources/compiled/main.js'); } + + return self::$assetCache['js']; } - /** - * @psalm-param class-string $a - * @psalm-param class-string $b - */ - public static function sortByHierarchy(string $a, string $b): int + /** @psalm-api */ + public static function renderCss(): ?string { - if (\is_subclass_of($a, $b)) { - return -1; + if (!isset(self::$theme)) { + return null; } - if (\is_subclass_of($b, $a)) { - return 1; + if (!isset(self::$assetCache['css'][self::$theme])) { + if (\file_exists(KINT_DIR.'/resources/compiled/'.self::$theme)) { + self::$assetCache['css'][self::$theme] = \file_get_contents(KINT_DIR.'/resources/compiled/'.self::$theme); + } else { + self::$assetCache['css'][self::$theme] = \file_get_contents(self::$theme); + } } - return 0; + return self::$assetCache['css'][self::$theme]; } } diff --git a/system/ThirdParty/Kint/Renderer/CliRenderer.php b/system/ThirdParty/Kint/Renderer/CliRenderer.php index c2ea103b6644..6efdbe83203d 100644 --- a/system/ThirdParty/Kint/Renderer/CliRenderer.php +++ b/system/ThirdParty/Kint/Renderer/CliRenderer.php @@ -27,7 +27,7 @@ namespace Kint\Renderer; -use Kint\Zval\Value; +use Kint\Value\AbstractValue; use Throwable; class CliRenderer extends TextRenderer @@ -35,52 +35,46 @@ class CliRenderer extends TextRenderer /** * @var bool enable colors */ - public static $cli_colors = true; - - /** - * Forces utf8 output on windows. - * - * @var bool - */ - public static $force_utf8 = false; + public static bool $cli_colors = true; /** * Detects the terminal width on startup. - * - * @var bool */ - public static $detect_width = true; + public static bool $detect_width = true; /** * The minimum width to detect terminal size as. * * Less than this is ignored and falls back to default width. - * - * @var int */ - public static $min_terminal_width = 40; + public static int $min_terminal_width = 40; + + /** + * Forces utf8 output on windows. + */ + public static bool $force_utf8 = false; /** * Which stream to check for VT100 support on windows. * * uses STDOUT by default if it's defined * - * @var ?resource + * @psalm-var ?resource */ public static $windows_stream = null; - protected static $terminal_width = null; + protected static ?int $terminal_width = null; - protected $windows_output = false; + protected bool $windows_output = false; - protected $colors = false; + protected bool $colors = false; public function __construct() { parent::__construct(); if (!self::$force_utf8 && KINT_WIN) { - if (!KINT_PHP72 || !\function_exists('sapi_windows_vt100_support')) { + if (!\function_exists('sapi_windows_vt100_support')) { $this->windows_output = true; } else { $stream = self::$windows_stream; @@ -97,16 +91,23 @@ public function __construct() } } - if (!self::$terminal_width) { - if (!KINT_WIN && self::$detect_width) { + if (null === self::$terminal_width) { + if (self::$detect_width) { try { - self::$terminal_width = (int) \exec('tput cols'); + $tput = KINT_WIN ? \exec('tput cols 2>nul') : \exec('tput cols 2>/dev/null'); + if ((bool) $tput) { + /** + * @psalm-suppress InvalidCast + * Psalm bug #11080 + */ + self::$terminal_width = (int) $tput; + } } catch (Throwable $t) { self::$terminal_width = self::$default_width; } } - if (self::$terminal_width < self::$min_terminal_width) { + if (!isset(self::$terminal_width) || self::$terminal_width < self::$min_terminal_width) { self::$terminal_width = self::$default_width; } } @@ -143,13 +144,13 @@ public function colorTitle(string $string): string return "\x1b[36m".\str_replace("\n", "\x1b[0m\n\x1b[36m", $string)."\x1b[0m"; } - public function renderTitle(Value $o): string + public function renderTitle(AbstractValue $v): string { if ($this->windows_output) { - return $this->utf8ToWindows(parent::renderTitle($o)); + return $this->utf8ToWindows(parent::renderTitle($v)); } - return parent::renderTitle($o); + return parent::renderTitle($v); } public function preRender(): string diff --git a/system/ThirdParty/Kint/Renderer/Text/DepthLimitPlugin.php b/system/ThirdParty/Kint/Renderer/ConstructableRendererInterface.php similarity index 85% rename from system/ThirdParty/Kint/Renderer/Text/DepthLimitPlugin.php rename to system/ThirdParty/Kint/Renderer/ConstructableRendererInterface.php index cbb151882044..686f37a645f6 100644 --- a/system/ThirdParty/Kint/Renderer/Text/DepthLimitPlugin.php +++ b/system/ThirdParty/Kint/Renderer/ConstructableRendererInterface.php @@ -25,14 +25,9 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace Kint\Renderer\Text; +namespace Kint\Renderer; -use Kint\Zval\Value; - -class DepthLimitPlugin extends AbstractPlugin +interface ConstructableRendererInterface extends RendererInterface { - public function render(Value $o): string - { - return $this->renderLockedHeader($o, 'DEPTH LIMIT'); - } + public function __construct(); } diff --git a/system/ThirdParty/Kint/Renderer/PlainRenderer.php b/system/ThirdParty/Kint/Renderer/PlainRenderer.php index 7210f55f3072..ac949e5b6169 100644 --- a/system/ThirdParty/Kint/Renderer/PlainRenderer.php +++ b/system/ThirdParty/Kint/Renderer/PlainRenderer.php @@ -27,46 +27,38 @@ namespace Kint\Renderer; -use Kint\Kint; -use Kint\Zval\BlobValue; -use Kint\Zval\Value; +use Kint\Utils; +use Kint\Value\AbstractValue; class PlainRenderer extends TextRenderer { - public static $pre_render_sources = [ + use AssetRendererTrait; + + public static array $pre_render_sources = [ 'script' => [ - ['Kint\\Renderer\\PlainRenderer', 'renderJs'], - ['Kint\\Renderer\\Text\\MicrotimePlugin', 'renderJs'], + [self::class, 'renderJs'], ], 'style' => [ - ['Kint\\Renderer\\PlainRenderer', 'renderCss'], + [self::class, 'renderCss'], ], 'raw' => [], ]; - /** - * Path to the CSS file to load by default. - * - * @var string - */ - public static $theme = 'plain.css'; - /** * Output htmlentities instead of utf8. - * - * @var bool */ - public static $disable_utf8 = false; + public static bool $disable_utf8 = false; - public static $needs_pre_render = true; + public static bool $needs_pre_render = true; - public static $always_pre_render = false; + public static bool $always_pre_render = false; - protected $force_pre_render = false; + protected bool $force_pre_render = false; public function __construct() { parent::__construct(); + self::$theme ??= 'plain.css'; $this->setForcePreRender(self::$always_pre_render); } @@ -74,7 +66,7 @@ public function setCallInfo(array $info): void { parent::setCallInfo($info); - if (\in_array('@', $this->call_info['modifiers'], true)) { + if (\in_array('@', $info['modifiers'], true)) { $this->setForcePreRender(true); } } @@ -118,13 +110,13 @@ public function colorTitle(string $string): string return ''.$string.''; } - public function renderTitle(Value $o): string + public function renderTitle(AbstractValue $v): string { if (self::$disable_utf8) { - return $this->utf8ToHtmlentity(parent::renderTitle($o)); + return $this->utf8ToHtmlentity(parent::renderTitle($v)); } - return parent::renderTitle($o); + return parent::renderTitle($v); } public function preRender(): string @@ -144,10 +136,18 @@ public function preRender(): string switch ($type) { case 'script': - $output .= ''; + $output .= '