diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index cf018d3c39d8..1f6dc36b395d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -25,11 +25,10 @@ body: description: Which PHP versions did you run your code? multiple: true options: - - '7.4' - - '8.0' - '8.1' - '8.2' - '8.3' + - '8.4' validations: required: true diff --git a/.github/prlint.json b/.github/prlint.json index a3744c5d5018..bb352e554b71 100644 --- a/.github/prlint.json +++ b/.github/prlint.json @@ -1,7 +1,7 @@ { "title": [ { - "pattern": "^(\\[\\d+\\.\\d+\\]\\s{1})?(feat|fix|chore|docs|perf|refactor|style|test|config|revert)(\\([\\-.@:`a-zA-Z0-9]+\\))?!?:\\s{1}\\S.+\\S|Prep for \\d\\.\\d\\.\\d release|\\d\\.\\d\\.\\d Ready code$", + "pattern": "^(\\[\\d+\\.\\d+\\]\\s{1})?(feat|fix|chore|docs|perf|refactor|style|test|config|revert)(\\([\\-.@:`a-zA-Z0-9]+\\))?!?:\\s{1}\\S.+\\S|Prep for \\d\\.\\d\\.\\d release|\\d\\.\\d\\.\\d (Ready|Merge) code$", "message": "PR title must include the type (feat, fix, chore, docs, perf, refactor, style, test, config, revert) of the commit per Conventional Commits specification. See https://www.conventionalcommits.org/en/v1.0.0/ for the discussion." } ] diff --git a/.github/workflows/test-coding-standards.yml b/.github/workflows/test-coding-standards.yml index a1e2c4770296..75fbabf2f14a 100644 --- a/.github/workflows/test-coding-standards.yml +++ b/.github/workflows/test-coding-standards.yml @@ -29,7 +29,7 @@ jobs: matrix: php-version: - '8.1' - - '8.3' + - '8.4' steps: - name: Checkout base branch for PR @@ -65,3 +65,5 @@ jobs: - name: Run lint run: composer cs + env: + PHP_CS_FIXER_IGNORE_ENV: ${{ matrix.php-version == '8.4' }} diff --git a/.github/workflows/test-phpunit.yml b/.github/workflows/test-phpunit.yml index ba566ce5c9d1..62a8ab0e4338 100644 --- a/.github/workflows/test-phpunit.yml +++ b/.github/workflows/test-phpunit.yml @@ -59,9 +59,7 @@ jobs: - '8.1' - '8.2' - '8.3' - include: - - php-version: '8.3' - composer-option: '--ignore-platform-req=php' + - '8.4' uses: ./.github/workflows/reusable-phpunit-test.yml # @TODO Extract to codeigniter4/.github repo with: @@ -88,6 +86,7 @@ jobs: - '8.1' - '8.2' - '8.3' + - '8.4' db-platform: - MySQLi - OCI8 @@ -100,8 +99,6 @@ jobs: - php-version: '8.1' db-platform: MySQLi mysql-version: '5.7' - - php-version: '8.3' - composer-option: '--ignore-platform-req=php' uses: ./.github/workflows/reusable-phpunit-test.yml # @TODO Extract to codeigniter4/.github repo with: @@ -129,9 +126,7 @@ jobs: - '8.1' - '8.2' - '8.3' - include: - - php-version: '8.3' - composer-option: '--ignore-platform-req=php' + - '8.4' uses: ./.github/workflows/reusable-phpunit-test.yml # @TODO Extract to codeigniter4/.github repo with: @@ -157,9 +152,7 @@ jobs: - '8.1' - '8.2' - '8.3' - include: - - php-version: '8.3' - composer-option: '--ignore-platform-req=php' + - '8.4' uses: ./.github/workflows/reusable-phpunit-test.yml # @TODO Extract to codeigniter4/.github repo with: diff --git a/.github/workflows/test-rector.yml b/.github/workflows/test-rector.yml index 0835d09edaf0..f1a81bd170bf 100644 --- a/.github/workflows/test-rector.yml +++ b/.github/workflows/test-rector.yml @@ -45,7 +45,7 @@ jobs: strategy: fail-fast: false matrix: - php-versions: ['8.1', '8.3'] + php-versions: ['8.1', '8.4'] steps: - name: Checkout base branch for PR if: github.event_name == 'pull_request' diff --git a/CHANGELOG.md b/CHANGELOG.md index 200b7adb037c..876a767edb70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,68 @@ # Changelog +## [v4.6.0](https://github.com/codeigniter4/CodeIgniter4/tree/v4.6.0) (2025-01-19) +[Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/v4.5.8...v4.6.0) + +### Breaking Changes + +* refactor: remove deprecated failValidationError() in API\ResponseTrait by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8793 +* refactor: remove depreacted ResponseInterface::getReason() by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8841 +* refactor: remove deprecated Logger::cleanFilenames() and TestLogger::cleanup() by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8843 +* fix: Exception rework by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8728 +* fix: DefinedRouteCollector to use RouteCollectionInterface by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8911 +* fix: View::renderSection() return type by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8965 +* feat: [Filters] enables a filter to run more than once with different arguments by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8977 +* fix: add check for duplicate Registrar Auto-Discovery runs by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/9073 +* fix: Time loses microseconds by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/9081 +* feat: fix spark db:table causes errors with table name including special chars by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8748 +* [4.6] fix: Time::createFromTimestamp() change for PHP 8.4 by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/9105 +* fix: Time::setTimestamp()'s different behavior than DateTime by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/9106 +* [4.6] fix: inconsistency in detailed error reporting by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/9144 +* [4.6] feat: force PHP default 32 chars length at 4 bits to Session ID by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/9139 +* fix: prioritize headers set by the `Response` class by @michalsn in https://github.com/codeigniter4/CodeIgniter4/pull/9235 + +### Fixed Bugs + +* [4.6] fix: add validation message for min_dims by @christianberkman in https://github.com/codeigniter4/CodeIgniter4/pull/8988 +* fix: [Filters] normalize `$filters` arguments by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8994 +### Enhancements +* feat: [FileCollection] add function to reatain multiple patterns by @christianberkman in https://github.com/codeigniter4/CodeIgniter4/pull/8960 +* feat: [Validation] add `min_dims` rule in FileRules by @christianberkman in https://github.com/codeigniter4/CodeIgniter4/pull/8966 +* feat: add `foundRows` option for MySQLi config by @ducng99 in https://github.com/codeigniter4/CodeIgniter4/pull/8979 +* feat: `spark filter:check` shows filter classnames by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8985 +* feat: add BaseConnection::resetTransStatus() by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8767 +* feat: add Services::resetServicesCache() to reset services cache by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/9012 +* feat: add "400 Bad Request" page for end users by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/9044 +* feat: add directives to `phpini:check` command by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/9117 +* feat: multiple hostname routing by @ddevsr in https://github.com/codeigniter4/CodeIgniter4/pull/9150 +* [4.6] feat: workaround for implicit nullable deprecations in PHP 8.4 by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/9140 +* feat: support CURL HTTP3 by @ddevsr in https://github.com/codeigniter4/CodeIgniter4/pull/9145 +* feat: design info environment top in `error_exception` by @ddevsr in https://github.com/codeigniter4/CodeIgniter4/pull/9241 +* feat: [Validation] add support for `$dbGroup` as parameter in `is_unique` and `is_not_unique` by @maniaba in https://github.com/codeigniter4/CodeIgniter4/pull/9216 +* feat: added the `namespace` option to the `publish` command by @dimtrovich in https://github.com/codeigniter4/CodeIgniter4/pull/9278 +* chore: update `Kint` to v6.0 by @ddevsr in https://github.com/codeigniter4/CodeIgniter4/pull/9289 +* feat: CURL option `force_ip_resolve` by @ddevsr in https://github.com/codeigniter4/CodeIgniter4/pull/9194 +* feat: add SQLite3 config synchronous by @michalsn in https://github.com/codeigniter4/CodeIgniter4/pull/9202 +* feat: Differentiate between kilobyte/kibibyte and megabyte/mebibyte by @ThomasMeschke in https://github.com/codeigniter4/CodeIgniter4/pull/9277 +* feat: Strict locale negotiation by @neznaika0 in https://github.com/codeigniter4/CodeIgniter4/pull/9360 +* fix: Add support for multibyte strings by @neznaika0 in https://github.com/codeigniter4/CodeIgniter4/pull/9372 +* feat: add page start end total to `PagerRenderer` by @murilohpucci in https://github.com/codeigniter4/CodeIgniter4/pull/9371 +* feat: New command `lang:sync` by @neznaika0 in https://github.com/codeigniter4/CodeIgniter4/pull/9023 +* feat: additional `opcache` setting in check php.ini by @ddevsr in https://github.com/codeigniter4/CodeIgniter4/pull/9032 + +### Refactoring + +* [4.6] refactor: Validation rules and tests by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8975 +* [4.6] refactor: add `: void` by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/9013 +* refactor: remove dependency on BaseConnection in TableName by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/9104 +* refactor: add return type to closuer in FilterCheck by @neznaika0 in https://github.com/codeigniter4/CodeIgniter4/pull/9190 +* refactor: Remove deprecated `RedirectException` by @neznaika0 in https://github.com/codeigniter4/CodeIgniter4/pull/9399 +* refactor: Remove deprecated `EVENT_PRIORITY_*` by @neznaika0 in https://github.com/codeigniter4/CodeIgniter4/pull/9401 +* refactor: Remove deprecated `View::$currentSection` by @neznaika0 in https://github.com/codeigniter4/CodeIgniter4/pull/9403 +* refactor: Remove deprecated `Cache::$storePath` by @neznaika0 in https://github.com/codeigniter4/CodeIgniter4/pull/9404 +* refactor: Remove deprecated `Config\Format::getFormatter()` by @neznaika0 in https://github.com/codeigniter4/CodeIgniter4/pull/9405 +* refactor: Remove deprecation related to cookies by @neznaika0 in https://github.com/codeigniter4/CodeIgniter4/pull/9406 + ## [v4.5.8](https://github.com/codeigniter4/CodeIgniter4/tree/v4.5.8) (2025-01-19) [Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/v4.5.7...v4.5.8) diff --git a/admin/framework/composer.json b/admin/framework/composer.json index 79fc5c282e50..82d06cd9d05c 100644 --- a/admin/framework/composer.json +++ b/admin/framework/composer.json @@ -13,18 +13,18 @@ "php": "^8.1", "ext-intl": "*", "ext-mbstring": "*", - "laminas/laminas-escaper": "^2.13", + "laminas/laminas-escaper": "^2.14", "psr/log": "^3.0" }, "require-dev": { "codeigniter/coding-standard": "^1.7", - "fakerphp/faker": "^1.9", + "fakerphp/faker": "^1.24", "friendsofphp/php-cs-fixer": "^3.47.1", - "kint-php/kint": "^5.0.4", - "mikey179/vfsstream": "^1.6", + "kint-php/kint": "^6.0", + "mikey179/vfsstream": "^1.6.12", "nexusphp/cs-config": "^3.6", "phpunit/phpunit": "^10.5.16 || ^11.2", - "predis/predis": "^1.1 || ^2.0" + "predis/predis": "^1.1 || ^2.3" }, "suggest": { "ext-curl": "If you use CURLRequest class", diff --git a/app/Config/Cache.php b/app/Config/Cache.php index 3fbade6840cc..9ae2e843ac91 100644 --- a/app/Config/Cache.php +++ b/app/Config/Cache.php @@ -34,18 +34,6 @@ class Cache extends BaseConfig */ public string $backupHandler = 'dummy'; - /** - * -------------------------------------------------------------------------- - * Cache Directory Path - * -------------------------------------------------------------------------- - * - * The path to where cache files should be stored, if using a file-based - * system. - * - * @deprecated Use the driver-specific variant under $file - */ - public string $storePath = WRITEPATH . 'cache/'; - /** * -------------------------------------------------------------------------- * Key Prefix @@ -86,6 +74,7 @@ class Cache extends BaseConfig * -------------------------------------------------------------------------- * File settings * -------------------------------------------------------------------------- + * * Your file storage preferences can be specified below, if you are using * the File driver. * @@ -100,6 +89,7 @@ class Cache extends BaseConfig * ------------------------------------------------------------------------- * Memcached settings * ------------------------------------------------------------------------- + * * Your Memcached servers can be specified below, if you are using * the Memcached drivers. * diff --git a/app/Config/Constants.php b/app/Config/Constants.php index 47b92f832935..fb56bb1c5b03 100644 --- a/app/Config/Constants.php +++ b/app/Config/Constants.php @@ -77,18 +77,3 @@ defined('EXIT_DATABASE') || define('EXIT_DATABASE', 8); // database error defined('EXIT__AUTO_MIN') || define('EXIT__AUTO_MIN', 9); // lowest automatically-assigned error code defined('EXIT__AUTO_MAX') || define('EXIT__AUTO_MAX', 125); // highest automatically-assigned error code - -/** - * @deprecated Use \CodeIgniter\Events\Events::PRIORITY_LOW instead. - */ -define('EVENT_PRIORITY_LOW', 200); - -/** - * @deprecated Use \CodeIgniter\Events\Events::PRIORITY_NORMAL instead. - */ -define('EVENT_PRIORITY_NORMAL', 100); - -/** - * @deprecated Use \CodeIgniter\Events\Events::PRIORITY_HIGH instead. - */ -define('EVENT_PRIORITY_HIGH', 10); diff --git a/app/Config/Database.php b/app/Config/Database.php index 7a1fd21e8d10..29f6f4a14f42 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', @@ -64,6 +65,7 @@ class Database extends Config // 'failover' => [], // 'foreignKeys' => true, // 'busyTimeout' => 1000, + // 'synchronous' => null, // 'dateFormat' => [ // 'date' => 'Y-m-d', // 'datetime' => 'Y-m-d H:i:s', diff --git a/app/Config/Feature.php b/app/Config/Feature.php index efd4a0b20ae8..ec1435af413f 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. @@ -26,4 +26,12 @@ class Feature extends BaseConfig * If false, `limit(0)` returns no records. (the behavior of 3.1.9 or later in version 3.x.) */ public bool $limitZeroAsAll = true; + + /** + * Use strict location negotiation. + * + * By default, the locale is selected based on a loose comparison of the language code (ISO 639-1) + * Enabling strict comparison will also consider the region code (ISO 3166-1 alpha-2). + */ + public bool $strictLocaleNegotiation = false; } diff --git a/app/Config/Format.php b/app/Config/Format.php index 2838f55ef0d9..0d334d72b3fb 100644 --- a/app/Config/Format.php +++ b/app/Config/Format.php @@ -3,7 +3,6 @@ namespace Config; use CodeIgniter\Config\BaseConfig; -use CodeIgniter\Format\FormatterInterface; use CodeIgniter\Format\JSONFormatter; use CodeIgniter\Format\XMLFormatter; @@ -62,16 +61,4 @@ class Format extends BaseConfig 'application/xml' => 0, 'text/xml' => 0, ]; - - /** - * A Factory method to return the appropriate formatter for the given mime type. - * - * @return FormatterInterface - * - * @deprecated This is an alias of `\CodeIgniter\Format\Format::getFormatter`. Use that instead. - */ - public function getFormatter(string $mime) - { - return service('format')->getFormatter($mime); - } } 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/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/app/Config/Security.php b/app/Config/Security.php index 0858b9bbb257..635f8b77b9b7 100644 --- a/app/Config/Security.php +++ b/app/Config/Security.php @@ -83,21 +83,4 @@ class Security extends BaseConfig * @see https://codeigniter4.github.io/userguide/libraries/security.html#redirection-on-failure */ public bool $redirect = (ENVIRONMENT === 'production'); - - /** - * -------------------------------------------------------------------------- - * CSRF SameSite - * -------------------------------------------------------------------------- - * - * Setting for CSRF SameSite cookie token. - * - * Allowed values are: None - Lax - Strict - ''. - * - * Defaults to `Lax` as recommended in this link: - * - * @see https://portswigger.net/web-security/csrf/samesite-cookies - * - * @deprecated `Config\Cookie` $samesite property is used. - */ - public string $samesite = 'Lax'; } diff --git a/app/Views/errors/html/debug.css b/app/Views/errors/html/debug.css index 6a050c8bb462..b20f36a53ce2 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); + width: 100%; + margin-top: -2.14rem; + position: fixed; } .source { 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/composer.json b/composer.json index 80fe952c9089..c762845f9a7e 100644 --- a/composer.json +++ b/composer.json @@ -13,21 +13,21 @@ "php": "^8.1", "ext-intl": "*", "ext-mbstring": "*", - "laminas/laminas-escaper": "^2.13", + "laminas/laminas-escaper": "^2.14", "psr/log": "^3.0" }, "require-dev": { "codeigniter/phpstan-codeigniter": "^1.5.1", - "fakerphp/faker": "^1.9", - "kint-php/kint": "^5.0.4", - "mikey179/vfsstream": "^1.6", + "fakerphp/faker": "^1.24", + "kint-php/kint": "^6.0", + "mikey179/vfsstream": "^1.6.12", "nexusphp/tachycardia": "^2.0", "phpstan/extension-installer": "^1.4", "phpstan/phpstan": "^2.0", "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpcov": "^9.0.2 || ^10.0", "phpunit/phpunit": "^10.5.16 || ^11.2", - "predis/predis": "^1.1 || ^2.0", + "predis/predis": "^1.1 || ^2.3", "rector/rector": "2.0.6", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, @@ -90,7 +90,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", diff --git a/phpdoc.dist.xml b/phpdoc.dist.xml index ec53ca076b8a..533ad3e033f0 100644 --- a/phpdoc.dist.xml +++ b/phpdoc.dist.xml @@ -5,12 +5,12 @@ xmlns="https://www.phpdoc.org" xsi:noNamespaceSchemaLocation="https://docs.phpdoc.org/latest/phpdoc.xsd" > - CodeIgniter v4.5 API + CodeIgniter v4.6 API api/build/ api/cache/ - + system diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 6be39bbef050..999972f556b8 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -38,5 +38,8 @@ parameters: disallowedImplicitArrayCreation: true disallowedShortTernary: true matchingInheritedMethodNames: true + codeigniter: + additionalServices: + - AfterAutoloadModule\Config\Services shipmonkBaselinePerIdentifier: directory: %currentWorkingDirectory% diff --git a/rector.php b/rector.php index e5c21c814ab2..ffa21b4e663e 100644 --- a/rector.php +++ b/rector.php @@ -19,7 +19,6 @@ use Rector\CodeQuality\Rector\FuncCall\CompactToVariablesRector; use Rector\CodeQuality\Rector\FunctionLike\SimplifyUselessVariableRector; use Rector\CodeQuality\Rector\Identical\FlipTypeControlToUseExclusiveTypeRector; -use Rector\CodeQuality\Rector\If_\ShortenElseIfRector; use Rector\CodeQuality\Rector\Ternary\TernaryEmptyArrayArrayDimFetchToCoalesceRector; use Rector\CodingStyle\Rector\ClassMethod\FuncGetArgsToVariadicParamRector; use Rector\CodingStyle\Rector\ClassMethod\MakeInheritedMethodVisibilitySameAsParentRector; @@ -183,7 +182,6 @@ ChangeIfElseValueAssignToEarlyReturnRector::class, InlineIfToExplicitIfRector::class, PreparedValueToEarlyReturnRector::class, - ShortenElseIfRector::class, UnusedForeachValueToArrayKeysRector::class, ChangeArrayPushToArrayAssignRector::class, RemoveErrorSuppressInTryCatchStmtsRector::class, @@ -208,4 +206,4 @@ // keep '\\' prefix string on string '\Foo\Bar' StringClassNameToClassConstantRector::SHOULD_KEEP_PRE_SLASH => true, ]) - ->withCodeQualityLevel(31); + ->withCodeQualityLevel(34); diff --git a/system/API/ResponseTrait.php b/system/API/ResponseTrait.php index dbfa83dc88dc..5f2e068b3918 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/system/Autoloader/Autoloader.php b/system/Autoloader/Autoloader.php index 50e6eaed2337..a88c673d5fc0 100644 --- a/system/Autoloader/Autoloader.php +++ b/system/Autoloader/Autoloader.php @@ -14,16 +14,16 @@ namespace CodeIgniter\Autoloader; use CodeIgniter\Exceptions\ConfigException; +use CodeIgniter\Exceptions\InvalidArgumentException; +use CodeIgniter\Exceptions\RuntimeException; use Composer\Autoload\ClassLoader; use Composer\InstalledVersions; use Config\Autoload; use Config\Kint as KintConfig; use Config\Modules; -use InvalidArgumentException; use Kint; use Kint\Renderer\CliRenderer; use Kint\Renderer\RichRenderer; -use RuntimeException; /** * An autoloader that uses both PSR4 autoloading, and traditional classmaps. @@ -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/BaseModel.php b/system/BaseModel.php index a10acef72e2e..b0be872050c6 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -21,12 +21,12 @@ 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 InvalidArgumentException; use ReflectionClass; use ReflectionException; use ReflectionProperty; diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index b8b90d77ba2d..a21996507d55 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -14,7 +14,7 @@ namespace CodeIgniter\CLI; use CodeIgniter\CLI\Exceptions\CLIException; -use InvalidArgumentException; +use CodeIgniter\Exceptions\InvalidArgumentException; use Throwable; /** diff --git a/system/CLI/Commands.php b/system/CLI/Commands.php index 2115714106bc..732cb370382d 100644 --- a/system/CLI/Commands.php +++ b/system/CLI/Commands.php @@ -52,12 +52,12 @@ public function __construct($logger = null) /** * Runs a command given * - * @return int|void Exit code + * @return int Exit code */ public function run(string $command, array $params) { if (! $this->verifyCommand($command, $this->commands)) { - return; + return EXIT_ERROR; } // The file would have already been loaded during the 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/Cache/Exceptions/CacheException.php b/system/Cache/Exceptions/CacheException.php index 852e6302ced1..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 RuntimeException; +use CodeIgniter\Exceptions\RuntimeException; /** * CacheException */ -class CacheException extends RuntimeException implements ExceptionInterface +class CacheException extends RuntimeException { use DebugTraceableTrait; diff --git a/system/Cache/Handlers/BaseHandler.php b/system/Cache/Handlers/BaseHandler.php index 2e35864f5aba..ebe0ca2f43ba 100644 --- a/system/Cache/Handlers/BaseHandler.php +++ b/system/Cache/Handlers/BaseHandler.php @@ -15,9 +15,10 @@ use Closure; use CodeIgniter\Cache\CacheInterface; +use CodeIgniter\Exceptions\BadMethodCallException; +use CodeIgniter\Exceptions\InvalidArgumentException; use Config\Cache; use Exception; -use InvalidArgumentException; /** * Base class for cache handling @@ -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/FileHandler.php b/system/Cache/Handlers/FileHandler.php index 525616a71068..0a97a89f4862 100644 --- a/system/Cache/Handlers/FileHandler.php +++ b/system/Cache/Handlers/FileHandler.php @@ -54,13 +54,6 @@ class FileHandler extends BaseHandler */ public function __construct(Cache $config) { - if (! property_exists($config, 'file')) { - $config->file = [ - 'storePath' => $config->storePath ?? WRITEPATH . 'cache', - 'mode' => 0640, - ]; - } - $this->path = ! empty($config->file['storePath']) ? $config->file['storePath'] : WRITEPATH . 'cache'; $this->path = rtrim($this->path, '/') . '/'; diff --git a/system/Cache/Handlers/MemcachedHandler.php b/system/Cache/Handlers/MemcachedHandler.php index f3bc00f40ba1..c6bb6ddcadce 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.'); } /** diff --git a/system/Cache/ResponseCache.php b/system/Cache/ResponseCache.php index 448e7b83dfd4..e984b8e3606f 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']; diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 7325c1c8431b..35f97f748fce 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; @@ -28,7 +29,6 @@ use CodeIgniter\HTTP\ResponsableInterface; use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\HTTP\URI; -use CodeIgniter\Router\Exceptions\RedirectException as DeprecatedRedirectException; use CodeIgniter\Router\RouteCollectionInterface; use CodeIgniter\Router\Router; use Config\App; @@ -41,7 +41,6 @@ use Kint\Renderer\CliRenderer; use Kint\Renderer\RichRenderer; use Locale; -use LogicException; use Throwable; /** @@ -56,7 +55,7 @@ class CodeIgniter /** * The current version of CodeIgniter Framework */ - public const CI_VERSION = '4.5.8'; + public const CI_VERSION = '4.6.0'; /** * App startup time. @@ -294,7 +293,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; } @@ -318,7 +317,7 @@ private function configureKint(): void * * @param bool $returnResponse Used for testing purposes only. * - * @return ResponseInterface|void + * @return ResponseInterface|null */ public function run(?RouteCollectionInterface $routes = null, bool $returnResponse = false) { @@ -353,11 +352,8 @@ public function run(?RouteCollectionInterface $routes = null, bool $returnRespon } else { try { $this->response = $this->handleRequest($routes, config(Cache::class), $returnResponse); - } catch (DeprecatedRedirectException|ResponsableInterface $e) { + } catch (ResponsableInterface $e) { $this->outputBufferingEnd(); - if ($e instanceof DeprecatedRedirectException) { - $e = new RedirectException($e->getMessage(), $e->getCode(), $e); - } $this->response = $e->getResponse(); } catch (PageNotFoundException $e) { @@ -379,6 +375,8 @@ public function run(?RouteCollectionInterface $routes = null, bool $returnRespon } $this->sendResponse(); + + return null; } /** @@ -863,7 +861,7 @@ protected function determinePath() * controller method and make the script go. If it's not able to, will * show the appropriate Page Not Found error. * - * @return ResponseInterface|string|void + * @return ResponseInterface|string|null */ protected function startController() { @@ -889,6 +887,8 @@ protected function startController() ) { throw PageNotFoundException::forControllerNotFound($this->controller, $this->method); } + + return null; } /** diff --git a/system/Commands/Database/MigrateRollback.php b/system/Commands/Database/MigrateRollback.php index e1c788e7702e..cb6cbe3a7adb 100644 --- a/system/Commands/Database/MigrateRollback.php +++ b/system/Commands/Database/MigrateRollback.php @@ -74,7 +74,7 @@ public function run(array $params) $force = array_key_exists('f', $params) || CLI::getOption('f'); if (! $force && CLI::prompt(lang('Migrations.rollBackConfirm'), ['y', 'n']) === 'n') { - return; + return null; } // @codeCoverageIgnoreEnd } @@ -115,5 +115,7 @@ public function run(array $params) $this->showError($e); // @codeCoverageIgnoreEnd } + + return null; } } diff --git a/system/Commands/Database/ShowTableInfo.php b/system/Commands/Database/ShowTableInfo.php index 2367bbff4dff..390bf586c51b 100644 --- a/system/Commands/Database/ShowTableInfo.php +++ b/system/Commands/Database/ShowTableInfo.php @@ -16,8 +16,9 @@ use CodeIgniter\CLI\BaseCommand; use CodeIgniter\CLI\CLI; use CodeIgniter\Database\BaseConnection; +use CodeIgniter\Database\TableName; +use CodeIgniter\Exceptions\InvalidArgumentException; use Config\Database; -use InvalidArgumentException; /** * Get table data if it exists in the 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->DBPrefix, $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->DBPrefix, $tableName)); $builder->limit($limitRows); if ($sortField !== null) { $builder->orderBy($sortField, $this->sortDesc ? 'DESC' : 'ASC'); diff --git a/system/Commands/Translation/LocalizationSync.php b/system/Commands/Translation/LocalizationSync.php new file mode 100644 index 000000000000..6381ab33ccad --- /dev/null +++ b/system/Commands/Translation/LocalizationSync.php @@ -0,0 +1,202 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Commands\Translation; + +use CodeIgniter\CLI\BaseCommand; +use CodeIgniter\CLI\CLI; +use CodeIgniter\Exceptions\LogicException; +use Config\App; +use ErrorException; +use FilesystemIterator; +use Locale; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; +use SplFileInfo; + +/** + * @see \CodeIgniter\Commands\Translation\LocalizationSyncTest + */ +class LocalizationSync extends BaseCommand +{ + protected $group = 'Translation'; + protected $name = 'lang:sync'; + protected $description = 'Synchronize translation files from one language to another.'; + protected $usage = 'lang:sync [options]'; + protected $arguments = []; + protected $options = [ + '--locale' => 'The original locale (en, ru, etc.).', + '--target' => 'Target locale (en, ru, etc.).', + ]; + private string $languagePath; + + public function run(array $params) + { + $optionTargetLocale = ''; + $optionLocale = $params['locale'] ?? Locale::getDefault(); + $this->languagePath = APPPATH . 'Language'; + + if (isset($params['target']) && $params['target'] !== '') { + $optionTargetLocale = $params['target']; + } + + if (! in_array($optionLocale, config(App::class)->supportedLocales, true)) { + CLI::error( + 'Error: "' . $optionLocale . '" is not supported. Supported locales: ' + . implode(', ', config(App::class)->supportedLocales), + ); + + return EXIT_USER_INPUT; + } + + if ($optionTargetLocale === '') { + CLI::error( + 'Error: "--target" is not configured. Supported locales: ' + . implode(', ', config(App::class)->supportedLocales), + ); + + return EXIT_USER_INPUT; + } + + if (! in_array($optionTargetLocale, config(App::class)->supportedLocales, true)) { + CLI::error( + 'Error: "' . $optionTargetLocale . '" is not supported. Supported locales: ' + . implode(', ', config(App::class)->supportedLocales), + ); + + return EXIT_USER_INPUT; + } + + if ($optionTargetLocale === $optionLocale) { + CLI::error( + 'Error: You cannot have the same values for "--target" and "--locale".', + ); + + return EXIT_USER_INPUT; + } + + if (ENVIRONMENT === 'testing') { + $this->languagePath = SUPPORTPATH . 'Language'; + } + + if ($this->process($optionLocale, $optionTargetLocale) === EXIT_ERROR) { + return EXIT_ERROR; + } + + CLI::write('All operations done!'); + + return EXIT_SUCCESS; + } + + private function process(string $originalLocale, string $targetLocale): int + { + $originalLocaleDir = $this->languagePath . DIRECTORY_SEPARATOR . $originalLocale; + $targetLocaleDir = $this->languagePath . DIRECTORY_SEPARATOR . $targetLocale; + + if (! is_dir($originalLocaleDir)) { + CLI::error( + 'Error: The "' . clean_path($originalLocaleDir) . '" directory was not found.', + ); + + return EXIT_ERROR; + } + + // Unifying the error - mkdir() may cause an exception. + try { + if (! is_dir($targetLocaleDir) && ! mkdir($targetLocaleDir, 0775)) { + throw new ErrorException(); + } + } catch (ErrorException $e) { + CLI::error( + 'Error: The target directory "' . clean_path($targetLocaleDir) . '" cannot be accessed.', + ); + + return EXIT_ERROR; + } + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( + $originalLocaleDir, + FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS, + ), + ); + + /** + * @var array $files + */ + $files = iterator_to_array($iterator, true); + ksort($files); + + foreach ($files as $originalLanguageFile) { + if ($originalLanguageFile->getExtension() !== 'php') { + continue; + } + + $targetLanguageFile = $targetLocaleDir . DIRECTORY_SEPARATOR . $originalLanguageFile->getFilename(); + + $targetLanguageKeys = []; + $originalLanguageKeys = include $originalLanguageFile; + + if (is_file($targetLanguageFile)) { + $targetLanguageKeys = include $targetLanguageFile; + } + + $targetLanguageKeys = $this->mergeLanguageKeys($originalLanguageKeys, $targetLanguageKeys, $originalLanguageFile->getBasename('.php')); + + $content = "|string|null> $originalLanguageKeys + * @param array|string|null> $targetLanguageKeys + * + * @return array|string|null> + */ + private function mergeLanguageKeys(array $originalLanguageKeys, array $targetLanguageKeys, string $prefix = ''): array + { + $mergedLanguageKeys = []; + + foreach ($originalLanguageKeys as $key => $value) { + $placeholderValue = $prefix !== '' ? $prefix . '.' . $key : $key; + + if (is_string($value)) { + // Keep the old value + // TODO: The value type may not match the original one + if (array_key_exists($key, $targetLanguageKeys)) { + $mergedLanguageKeys[$key] = $targetLanguageKeys[$key]; + + continue; + } + + // Set new key with placeholder + $mergedLanguageKeys[$key] = $placeholderValue; + } elseif (is_array($value)) { + if (! array_key_exists($key, $targetLanguageKeys)) { + $mergedLanguageKeys[$key] = $this->mergeLanguageKeys($value, [], $placeholderValue); + + continue; + } + + $mergedLanguageKeys[$key] = $this->mergeLanguageKeys($value, $targetLanguageKeys[$key], $placeholderValue); + } else { + throw new LogicException('Value for the key "' . $placeholderValue . '" is of the wrong type. Only "array" or "string" is allowed.'); + } + } + + return $mergedLanguageKeys; + } +} diff --git a/system/Commands/Utilities/FilterCheck.php b/system/Commands/Utilities/FilterCheck.php index a7a8803bd776..e2043fe9a3a4 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,15 +101,38 @@ public function run(array $params) return EXIT_ERROR; } - $filters = $this->addRequiredFilters($filterCollector, $filters); + $this->showTable($filterCollector, $filters, $method, $route); + $this->showFilterClasses($filterCollector, $method, $route); - $tbody[] = [ - strtoupper($method), - $route, - implode(' ', $filters['before']), - implode(' ', $filters['after']), - ]; + 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 { $thead = [ 'Method', 'Route', @@ -123,33 +140,60 @@ public function run(array $params) 'After Filters', ]; - CLI::table($tbody, $thead); + $required = $filterCollector->getRequiredFilters(); - return EXIT_SUCCESS; + $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); } - private function addRequiredFilters(FilterCollector $filterCollector, array $filters): array + /** + * Color all elements of the array. + * + * @param array $array + * + * @return array + */ + private function colorItems(array $array): array { - $output = []; + return array_map(function ($item): array|string { + if (is_array($item)) { + return $this->colorItems($item); + } - $required = $filterCollector->getRequiredFilters(); + return CLI::color($item, 'yellow'); + }, $array); + } - $colored = []; + private function showFilterClasses( + FilterCollector $filterCollector, + string $method, + string $route, + ): void { + $requiredFilterClasses = $filterCollector->getRequiredFilterClasses(); + $filterClasses = $filterCollector->getClasses($method, $route); - foreach ($required['before'] as $filter) { - $filter = CLI::color($filter, 'yellow'); - $colored[] = $filter; - } - $output['before'] = array_merge($colored, $filters['before']); + $coloredRequiredFilterClasses = $this->colorItems($requiredFilterClasses); - $colored = []; + $classList = [ + 'before' => array_merge($coloredRequiredFilterClasses['before'], $filterClasses['before']), + 'after' => array_merge($filterClasses['after'], $coloredRequiredFilterClasses['after']), + ]; - foreach ($required['after'] as $filter) { - $filter = CLI::color($filter, 'yellow'); - $colored[] = $filter; + foreach ($classList as $position => $classes) { + CLI::write(ucfirst($position) . ' Filter Classes:', 'cyan'); + CLI::write(implode(' → ', $classes)); } - $output['after'] = array_merge($filters['after'], $colored); - - return $output; } } diff --git a/system/Commands/Utilities/Namespaces.php b/system/Commands/Utilities/Namespaces.php index c16f692cfb37..8dda8ce0732e 100644 --- a/system/Commands/Utilities/Namespaces.php +++ b/system/Commands/Utilities/Namespaces.php @@ -120,10 +120,10 @@ private function outputAllNamespaces(array $params): array private function truncate(string $string, int $max): string { - $length = strlen($string); + $length = mb_strlen($string); if ($length > $max) { - return substr($string, 0, $max - 3) . '...'; + return mb_substr($string, 0, $max - 3) . '...'; } return $string; diff --git a/system/Commands/Utilities/Optimize.php b/system/Commands/Utilities/Optimize.php index 2c147de6c3f1..11872bebafa5 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/Commands/Utilities/PhpIniCheck.php b/system/Commands/Utilities/PhpIniCheck.php index 0426f9078b77..eb2192434133 100644 --- a/system/Commands/Utilities/PhpIniCheck.php +++ b/system/Commands/Utilities/PhpIniCheck.php @@ -14,6 +14,7 @@ namespace CodeIgniter\Commands\Utilities; use CodeIgniter\CLI\BaseCommand; +use CodeIgniter\CLI\CLI; use CodeIgniter\Security\CheckPhpIni; /** @@ -41,7 +42,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 @@ -56,6 +57,7 @@ final class PhpIniCheck extends BaseCommand * @var array */ protected $arguments = [ + 'opcache' => 'Check detail opcache values in production environment.', ]; /** @@ -70,7 +72,24 @@ final class PhpIniCheck extends BaseCommand */ public function run(array $params) { - CheckPhpIni::run(); + if (isset($params[0]) && ! in_array($params[0], array_keys($this->arguments), true)) { + CLI::error('You must specify a correct argument.'); + CLI::write(' Usage: ' . $this->usage); + CLI::write(' Example: phpini:check opcache'); + CLI::write('Arguments:'); + + $length = max(array_map(strlen(...), array_keys($this->arguments))); + + foreach ($this->arguments as $argument => $description) { + CLI::write(CLI::color($this->setPad($argument, $length, 2, 2), 'green') . $description); + } + + return EXIT_ERROR; + } + + $argument = $params[0] ?? null; + + CheckPhpIni::run(argument: $argument); return EXIT_SUCCESS; } 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/Commands/Utilities/Routes/FilterCollector.php b/system/Commands/Utilities/Routes/FilterCollector.php index 51b36662f490..052884a644b7 100644 --- a/system/Commands/Utilities/Routes/FilterCollector.php +++ b/system/Commands/Utilities/Routes/FilterCollector.php @@ -42,7 +42,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 { @@ -78,10 +78,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 = service('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 { @@ -96,6 +138,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 = service('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..f34ea26aa702 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,31 @@ 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'); + + $requiredBefore = []; + $requiredAfter = []; + + foreach ($before as $classInfo) { + $requiredBefore[] = $classInfo[0]; + } + + foreach ($after as $classInfo) { + $requiredAfter[] = $classInfo[0]; + } + + return [ + 'before' => $requiredBefore, + 'after' => $requiredAfter, + ]; + } } diff --git a/system/Common.php b/system/Common.php index 863ff3cdbabb..7fb4c5964254 100644 --- a/system/Common.php +++ b/system/Common.php @@ -20,6 +20,8 @@ use CodeIgniter\Database\BaseConnection; 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; diff --git a/system/Config/BaseConfig.php b/system/Config/BaseConfig.php index 8a82cbf7ce64..e056664e24be 100644 --- a/system/Config/BaseConfig.php +++ b/system/Config/BaseConfig.php @@ -11,11 +11,12 @@ namespace CodeIgniter\Config; +use CodeIgniter\Exceptions\ConfigException; +use CodeIgniter\Exceptions\RuntimeException; use Config\Encryption; use Config\Modules; use ReflectionClass; use ReflectionException; -use RuntimeException; /** * Class BaseConfig @@ -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(); diff --git a/system/Config/BaseService.php b/system/Config/BaseService.php index 687044be0713..0fb4c3c7c3ad 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; @@ -79,7 +80,6 @@ use Config\Toolbar as ConfigToolbar; use Config\Validation as ConfigValidation; use Config\View as ConfigView; -use InvalidArgumentException; /** * Services Configuration file. @@ -389,6 +389,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) { diff --git a/system/Config/DotEnv.php b/system/Config/DotEnv.php index 7e95d1352f00..6778c44e0d8d 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 fd7d6a32397b..797f9fd9c7de 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/CloneableCookieInterface.php b/system/Cookie/CloneableCookieInterface.php index 0b7d6fdf5015..3055129a0b4b 100644 --- a/system/Cookie/CloneableCookieInterface.php +++ b/system/Cookie/CloneableCookieInterface.php @@ -58,15 +58,6 @@ public function withExpires($expires); */ public function withExpired(); - /** - * Creates a new Cookie that will virtually never expire from the browser. - * - * @return static - * - * @deprecated See https://github.com/codeigniter4/CodeIgniter4/pull/6413 - */ - public function withNeverExpiring(); - /** * Creates a new Cookie with a new path on the server the cookie is available. * diff --git a/system/Cookie/Cookie.php b/system/Cookie/Cookie.php index d75cce26ac09..df75c03c7bcb 100644 --- a/system/Cookie/Cookie.php +++ b/system/Cookie/Cookie.php @@ -15,11 +15,11 @@ 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 InvalidArgumentException; -use LogicException; use ReturnTypeWillChange; /** @@ -465,18 +465,6 @@ public function withExpired() return $cookie; } - /** - * @deprecated See https://github.com/codeigniter4/CodeIgniter4/pull/6413 - */ - public function withNeverExpiring() - { - $cookie = clone $this; - - $cookie->expires = Time::now()->getTimestamp() + 5 * YEAR; - - return $cookie; - } - /** * {@inheritDoc} */ diff --git a/system/Cookie/CookieStore.php b/system/Cookie/CookieStore.php index 05ebf9925ee6..6d5caa2aa5e0 100644 --- a/system/Cookie/CookieStore.php +++ b/system/Cookie/CookieStore.php @@ -159,28 +159,6 @@ public function remove(string $name, string $prefix = '') return $store; } - /** - * Dispatches all cookies in store. - * - * @deprecated Response should dispatch cookies. - */ - public function dispatch(): void - { - foreach ($this->cookies as $cookie) { - $name = $cookie->getPrefixedName(); - $value = $cookie->getValue(); - $options = $cookie->getOptions(); - - if ($cookie->isRaw()) { - $this->setRawCookie($name, $value, $options); - } else { - $this->setCookie($name, $value, $options); - } - } - - $this->clear(); - } - /** * Returns all cookie instances in store. * @@ -232,28 +210,4 @@ protected function validateCookies(array $cookies): void } } } - - /** - * Extracted call to `setrawcookie()` in order to run unit tests on it. - * - * @codeCoverageIgnore - * - * @deprecated - */ - protected function setRawCookie(string $name, string $value, array $options): void - { - setrawcookie($name, $value, $options); - } - - /** - * Extracted call to `setcookie()` in order to run unit tests on it. - * - * @codeCoverageIgnore - * - * @deprecated - */ - protected function setCookie(string $name, string $value, array $options): void - { - setcookie($name, $value, $options); - } } diff --git a/system/DataCaster/Cast/BaseCast.php b/system/DataCaster/Cast/BaseCast.php index afe80982e3ba..06fb0eb4fa69 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 2953debfbf2e..b5f346c9c419 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 @@ -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); } @@ -62,6 +57,29 @@ 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 = 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]), + }; } } diff --git a/system/DataCaster/Cast/TimestampCast.php b/system/DataCaster/Cast/TimestampCast.php index e33938d3e2ba..aa94266001ef 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( diff --git a/system/DataCaster/DataCaster.php b/system/DataCaster/DataCaster.php index 47eac2ff7bde..ae3f40f4dc25 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/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index 010ac17b195f..e10dd557fda0 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 @@ -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)) { @@ -3014,7 +3019,7 @@ protected function _delete(string $table): string * * @param array|string $table The table to inspect * - * @return string|void + * @return string|null */ protected function trackAliases($table) { @@ -3023,7 +3028,7 @@ protected function trackAliases($table) $this->trackAliases($t); } - return; + return null; } // Does the string contain a comma? If so, we need to separate @@ -3038,11 +3043,13 @@ 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); } + + return null; } /** diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index b7107d19a34d..b274b02aa5a8 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,14 @@ 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 ($alias === '') { + return $this; + } + + if (! in_array($alias, $this->aliasedTables, true)) { + $this->aliasedTables[] = $alias; } return $this; @@ -897,6 +901,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 */ @@ -915,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 * @@ -1046,10 +1060,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) @@ -1070,6 +1084,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; @@ -1212,10 +1231,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, @@ -1225,6 +1252,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 * @@ -1538,12 +1576,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]; @@ -1553,7 +1595,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.'); } @@ -1769,9 +1811,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/BasePreparedQuery.php b/system/Database/BasePreparedQuery.php index 6ba2f6eb5a43..e9017bcf5ff9 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/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 e09dcdd27481..80e902d5ab58 100644 --- a/system/Database/Database.php +++ b/system/Database/Database.php @@ -15,7 +15,7 @@ use CodeIgniter\Exceptions\ConfigException; use CodeIgniter\Exceptions\CriticalError; -use InvalidArgumentException; +use CodeIgniter\Exceptions\InvalidArgumentException; /** * Database Connection Factory 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/Exceptions/DatabaseException.php b/system/Database/Exceptions/DatabaseException.php index cdb8e22832ec..77b170e0b407 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 ExceptionInterface, HasExitCodeInterface { public function getExitCode(): int { diff --git a/system/Database/Forge.php b/system/Database/Forge.php index 6bf574818919..53c819fb24c5 100644 --- a/system/Database/Forge.php +++ b/system/Database/Forge.php @@ -14,8 +14,8 @@ namespace CodeIgniter\Database; use CodeIgniter\Database\Exceptions\DatabaseException; -use InvalidArgumentException; -use RuntimeException; +use CodeIgniter\Exceptions\InvalidArgumentException; +use CodeIgniter\Exceptions\RuntimeException; use Throwable; /** diff --git a/system/Database/MigrationRunner.php b/system/Database/MigrationRunner.php index 0411e1593a68..32842916769d 100644 --- a/system/Database/MigrationRunner.php +++ b/system/Database/MigrationRunner.php @@ -16,10 +16,10 @@ 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 RuntimeException; use stdClass; /** diff --git a/system/Database/MySQLi/Connection.php b/system/Database/MySQLi/Connection.php index fa542e5708bb..2a9f9d3401c8 100644 --- a/system/Database/MySQLi/Connection.php +++ b/system/Database/MySQLi/Connection.php @@ -15,7 +15,8 @@ use CodeIgniter\Database\BaseConnection; use CodeIgniter\Database\Exceptions\DatabaseException; -use LogicException; +use CodeIgniter\Database\TableName; +use CodeIgniter\Exceptions\LogicException; use mysqli; use mysqli_result; use mysqli_sql_exception; @@ -81,6 +82,16 @@ class Connection extends BaseConnection */ public $numberNative = false; + /** + * Use MYSQLI_CLIENT_FOUND_ROWS + * + * Whether affectedRows() 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 +193,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, @@ -408,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/MySQLi/PreparedQuery.php b/system/Database/MySQLi/PreparedQuery.php index 34eedc25ff15..05ef5f151b7b 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/Connection.php b/system/Database/OCI8/Connection.php index 0ad7b66815e7..b6078febd030 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/OCI8/PreparedQuery.php b/system/Database/OCI8/PreparedQuery.php index e1577642c3a9..390ed938fc1c 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; use OCILob; /** diff --git a/system/Database/Postgre/Builder.php b/system/Database/Postgre/Builder.php index bf6e9416dc2c..27a72b58c333 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/Postgre/Connection.php b/system/Database/Postgre/Connection.php index 64b85eafb064..06ef62a9f842 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; @@ -305,13 +306,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/Postgre/PreparedQuery.php b/system/Database/Postgre/PreparedQuery.php index c55d5d8c5402..08900a6ddcfd 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/Connection.php b/system/Database/SQLSRV/Connection.php index 26b4983dba0a..c8e1e3fbee12 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/SQLSRV/PreparedQuery.php b/system/Database/SQLSRV/PreparedQuery.php index 425555927c60..19d2d5adfe6b 100644 --- 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/Builder.php b/system/Database/SQLite3/Builder.php index 64091cf8a2fb..6e62907ac1aa 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/SQLite3/Connection.php b/system/Database/SQLite3/Connection.php index 577e4b24ab76..0d3290b38d9d 100644 --- a/system/Database/SQLite3/Connection.php +++ b/system/Database/SQLite3/Connection.php @@ -15,6 +15,8 @@ use CodeIgniter\Database\BaseConnection; use CodeIgniter\Database\Exceptions\DatabaseException; +use CodeIgniter\Database\TableName; +use CodeIgniter\Exceptions\InvalidArgumentException; use Exception; use SQLite3; use SQLite3Result; @@ -55,6 +57,15 @@ class Connection extends BaseConnection */ protected $busyTimeout; + /** + * The setting of the "synchronous" flag + * + * @var int<0, 3>|null flag + * + * @see https://www.sqlite.org/pragma.html#pragma_synchronous + */ + protected ?int $synchronous = null; + /** * @return void */ @@ -69,6 +80,13 @@ public function initialize() if (is_int($this->busyTimeout)) { $this->connID->busyTimeout($this->busyTimeout); } + + if (is_int($this->synchronous)) { + if (! in_array($this->synchronous, [0, 1, 2, 3], true)) { + throw new InvalidArgumentException('Invalid synchronous value.'); + } + $this->connID->exec('PRAGMA synchronous = ' . $this->synchronous); + } } /** @@ -209,19 +227,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 +261,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/Database/SQLite3/PreparedQuery.php b/system/Database/SQLite3/PreparedQuery.php index 0e15b5d61f3b..afd2669c6436 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/Database/Seeder.php b/system/Database/Seeder.php index c70a34b45e9f..3530592fe671 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/Database/TableName.php b/system/Database/TableName.php new file mode 100644 index 000000000000..a34ac57aa1d2 --- /dev/null +++ b/system/Database/TableName.php @@ -0,0 +1,135 @@ + + * + * 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. + * + * @interal + * + * @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(string $dbPrefix, string $table, string $alias = ''): self + { + return new self( + $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(string $dbPrefix, string $actualTable, string $alias = ''): self + { + $prefix = $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( + string $dbPrefix, + string $table, + string $schema = '', + string $database = '', + string $alias = '', + ): self { + return new self( + $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/system/Debug/ExceptionHandler.php b/system/Debug/ExceptionHandler.php index 8283e1ff5362..68ab5feb768e 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) : ''; @@ -134,13 +136,8 @@ 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()) { + // If display_errors is enabled, shows the error details. $view = 'error_exception.php'; } @@ -158,4 +155,13 @@ protected function determineView( return $view; } + + private function isDisplayErrorsEnabled(): bool + { + return in_array( + strtolower(ini_get('display_errors')), + ['1', 'true', 'on', 'yes'], + true, + ); + } } diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php index 3293d75929a3..1a0293372953 100644 --- a/system/Debug/Exceptions.php +++ b/system/Debug/Exceptions.php @@ -208,6 +208,14 @@ 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->isImplicitNullableDeprecationError($message, $file, $line)) { + return true; + } + if (! $this->config->logDeprecations || (bool) env('CODEIGNITER_SCREAM_DEPRECATIONS')) { throw new ErrorException($message, 0, $severity, $file, $line); } @@ -222,6 +230,64 @@ 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; + } + + /** + * 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" + * + * @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, + '[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. 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..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 RuntimeException; +use CodeIgniter\Exceptions\RuntimeException; /** * Encryption exception */ -class EncryptionException extends RuntimeException implements ExceptionInterface +class EncryptionException extends RuntimeException { use DebugTraceableTrait; 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)) { 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/ConfigException.php b/system/Exceptions/ConfigException.php index d8849b809c68..b30818124901 100644 --- a/system/Exceptions/ConfigException.php +++ b/system/Exceptions/ConfigException.php @@ -14,9 +14,10 @@ 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 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/DownloadException.php b/system/Exceptions/DownloadException.php index 8ed2ba4849ab..c3bde8ed2556 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/Exceptions/FrameworkException.php b/system/Exceptions/FrameworkException.php index 7ea9e6d7d51f..4650c6c3541f 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; 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. 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 +{ +} 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/PageNotFoundException.php b/system/Exceptions/PageNotFoundException.php index 1a3b27a76a87..f84763ec25ff 100644 --- a/system/Exceptions/PageNotFoundException.php +++ b/system/Exceptions/PageNotFoundException.php @@ -13,9 +13,7 @@ namespace CodeIgniter\Exceptions; -use OutOfBoundsException; - -class PageNotFoundException extends OutOfBoundsException implements ExceptionInterface, HTTPExceptionInterface +class PageNotFoundException extends RuntimeException implements HTTPExceptionInterface { use DebugTraceableTrait; 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 +{ +} diff --git a/system/Exceptions/TestException.php b/system/Exceptions/TestException.php index f533dc279335..4487ae1e1815 100644 --- a/system/Exceptions/TestException.php +++ b/system/Exceptions/TestException.php @@ -14,9 +14,9 @@ namespace CodeIgniter\Exceptions; /** - * Exception for automatic logging. + * Exception thrown when there is an error with the test code. */ -class TestException extends CriticalError +class TestException extends LogicException { use DebugTraceableTrait; 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/Files/Exceptions/FileException.php b/system/Files/Exceptions/FileException.php index 5feb97929ead..3202978c3af6 100644 --- a/system/Files/Exceptions/FileException.php +++ b/system/Files/Exceptions/FileException.php @@ -14,8 +14,7 @@ namespace CodeIgniter\Files\Exceptions; use CodeIgniter\Exceptions\DebugTraceableTrait; -use CodeIgniter\Exceptions\ExceptionInterface; -use RuntimeException; +use CodeIgniter\Exceptions\RuntimeException; class FileException extends RuntimeException implements ExceptionInterface { diff --git a/system/Files/Exceptions/FileNotFoundException.php b/system/Files/Exceptions/FileNotFoundException.php index 86b22625aa9e..4c043f9ab02c 100644 --- a/system/Files/Exceptions/FileNotFoundException.php +++ b/system/Files/Exceptions/FileNotFoundException.php @@ -14,8 +14,7 @@ namespace CodeIgniter\Files\Exceptions; use CodeIgniter\Exceptions\DebugTraceableTrait; -use CodeIgniter\Exceptions\ExceptionInterface; -use RuntimeException; +use CodeIgniter\Exceptions\RuntimeException; class FileNotFoundException extends RuntimeException implements ExceptionInterface { diff --git a/system/Files/File.php b/system/Files/File.php index 15b53262e9c2..23eb324d2684 100644 --- a/system/Files/File.php +++ b/system/Files/File.php @@ -70,16 +70,38 @@ public function getSize() return $this->size ?? ($this->size = parent::getSize()); } + /** + * Retrieve the file size by unit, calculated in IEC standards with 1024 as base value. + * + * @phpstan-param positive-int $precision + */ + public function getSizeByBinaryUnit(FileSizeUnit $unit = FileSizeUnit::B, int $precision = 3): int|string + { + return $this->getSizeByUnitInternal(1024, $unit, $precision); + } + + /** + * Retrieve the file size by unit, calculated in metric standards with 1000 as base value. + * + * @phpstan-param positive-int $precision + */ + public function getSizeByMetricUnit(FileSizeUnit $unit = FileSizeUnit::B, int $precision = 3): int|string + { + return $this->getSizeByUnitInternal(1000, $unit, $precision); + } + /** * Retrieve the file size by unit. * + * @deprecated 4.6.0 Use getSizeByBinaryUnit() or getSizeByMetricUnit() instead + * * @return false|int|string */ public function getSizeByUnit(string $unit = 'b') { return match (strtolower($unit)) { - 'kb' => number_format($this->getSize() / 1024, 3), - 'mb' => number_format(($this->getSize() / 1024) / 1024, 3), + 'kb' => $this->getSizeByBinaryUnit(FileSizeUnit::KB), + 'mb' => $this->getSizeByBinaryUnit(FileSizeUnit::MB), default => $this->getSize(), }; } @@ -189,4 +211,17 @@ public function getDestination(string $destination, string $delimiter = '_', int return $destination; } + + private function getSizeByUnitInternal(int $fileSizeBase, FileSizeUnit $unit, int $precision): int|string + { + $exponent = $unit->value; + $divider = $fileSizeBase ** $exponent; + $size = $this->getSize() / $divider; + + if ($unit !== FileSizeUnit::B) { + $size = number_format($size, $precision); + } + + return $size; + } } diff --git a/system/Files/FileCollection.php b/system/Files/FileCollection.php index 369cea9ddd02..b293b4d3c08d 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; /** @@ -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 list $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 ($patterns === []) { + 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 // -------------------------------------------------------------------- diff --git a/system/Files/FileSizeUnit.php b/system/Files/FileSizeUnit.php new file mode 100644 index 000000000000..84c2ba44ca13 --- /dev/null +++ b/system/Files/FileSizeUnit.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Files; + +use CodeIgniter\Exceptions\InvalidArgumentException; + +enum FileSizeUnit: int +{ + case B = 0; + case KB = 1; + case MB = 2; + case GB = 3; + case TB = 4; + + /** + * Allows the creation of a FileSizeUnit from Strings like "kb" or "mb" + * + * @throws InvalidArgumentException + */ + public static function fromString(string $unit): self + { + return match (strtolower($unit)) { + 'b' => self::B, + 'kb' => self::KB, + 'mb' => self::MB, + 'gb' => self::GB, + 'tb' => self::TB, + default => throw new InvalidArgumentException("Invalid unit: {$unit}"), + }; + } +} diff --git a/system/Filters/CSRF.php b/system/Filters/CSRF.php index beb15f62b03a..ea9a9939f2de 100644 --- a/system/Filters/CSRF.php +++ b/system/Filters/CSRF.php @@ -35,14 +35,14 @@ class CSRF implements FilterInterface * * @param list|null $arguments * - * @return RedirectResponse|void + * @return RedirectResponse|null * * @throws SecurityException */ public function before(RequestInterface $request, $arguments = null) { if (! $request instanceof IncomingRequest) { - return; + return null; } /** @var Security $security */ @@ -57,16 +57,17 @@ public function before(RequestInterface $request, $arguments = null) throw $e; } + + return null; } /** * We don't have anything to do here. * * @param list|null $arguments - * - * @return void */ public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { + return null; } } diff --git a/system/Filters/Cors.php b/system/Filters/Cors.php index 93ca551b42f2..9a9bbb208152 100644 --- a/system/Filters/Cors.php +++ b/system/Filters/Cors.php @@ -48,18 +48,18 @@ public function __construct(array $config = []) /** * @param list|null $arguments * - * @return ResponseInterface|string|void + * @return ResponseInterface|null */ public function before(RequestInterface $request, $arguments = null) { if (! $request instanceof IncomingRequest) { - return; + return null; } $this->createCorsService($arguments); if (! $this->cors->isPreflightRequest($request)) { - return; + return null; } /** @var ResponseInterface $response */ @@ -88,12 +88,12 @@ private function createCorsService(?array $arguments): void /** * @param list|null $arguments * - * @return ResponseInterface|void + * @return ResponseInterface|null */ public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { if (! $request instanceof IncomingRequest) { - return; + return null; } $this->createCorsService($arguments); diff --git a/system/Filters/DebugToolbar.php b/system/Filters/DebugToolbar.php index 9f864ee572c5..7199b85f9d62 100644 --- a/system/Filters/DebugToolbar.php +++ b/system/Filters/DebugToolbar.php @@ -30,6 +30,7 @@ class DebugToolbar implements FilterInterface */ public function before(RequestInterface $request, $arguments = null) { + return null; } /** @@ -41,5 +42,7 @@ public function before(RequestInterface $request, $arguments = null) public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { service('toolbar')->prepare($request, $response); + + return null; } } 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/Filters/FilterInterface.php b/system/Filters/FilterInterface.php index 0353fb2ab9d0..6df33e912b87 100644 --- a/system/Filters/FilterInterface.php +++ b/system/Filters/FilterInterface.php @@ -33,7 +33,7 @@ interface FilterInterface * * @param list|null $arguments * - * @return RequestInterface|ResponseInterface|string|void + * @return RequestInterface|ResponseInterface|string|null */ public function before(RequestInterface $request, $arguments = null); @@ -45,7 +45,7 @@ public function before(RequestInterface $request, $arguments = null); * * @param list|null $arguments * - * @return ResponseInterface|void + * @return ResponseInterface|null */ public function after(RequestInterface $request, ResponseInterface $response, $arguments = null); } diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index 28f712f66114..412a9f457d53 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; @@ -30,7 +29,7 @@ class Filters { /** - * The original config file + * The Config\Filters instance * * @var FiltersConfig */ @@ -51,27 +50,42 @@ 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 */ 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,22 +93,43 @@ class Filters ]; /** - * The collection of filters' class names that will - * be used to execute in each position. + * The collection of filter classnames and their arguments 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' => [], 'after' => [], ]; + /** + * List of filter class instances. + * + * @var array [classname => instance] + */ + protected array $filterClassInstances = []; + /** * Any arguments to be passed to filters. * * @var array|null> [name => params] + * + * @deprecated 4.6.0 No longer used. */ protected $arguments = []; @@ -102,6 +137,8 @@ class Filters * Any arguments to be passed to filtersClass. * * @var array|null> [classname => arguments] + * + * @deprecated 4.6.0 No longer used. */ protected $argumentsClass = []; @@ -125,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. @@ -166,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 @@ -189,21 +226,19 @@ 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 $className) { - $class = new $className(); + foreach ($filterClassList as $filterClassInfo) { + $className = $filterClassInfo[0]; + $arguments = ($filterClassInfo[1] === []) ? null : $filterClassInfo[1]; - if (! $class instanceof FilterInterface) { - throw FilterException::forIncorrectInterface($class::class); - } + $instance = $this->createFilter($className); - $result = $class->before( - $this->request, - $this->argumentsClass[$className] ?? null, - ); + $result = $instance->before($this->request, $arguments); if ($result instanceof RequestInterface) { $this->request = $result; @@ -229,20 +264,18 @@ 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 $className) { - $class = new $className(); + foreach ($filterClassList as $filterClassInfo) { + $className = $filterClassInfo[0]; + $arguments = ($filterClassInfo[1] === []) ? null : $filterClassInfo[1]; - if (! $class instanceof FilterInterface) { - throw FilterException::forIncorrectInterface($class::class); - } + $instance = $this->createFilter($className); - $result = $class->after( - $this->request, - $this->response, - $this->argumentsClass[$className] ?? null, - ); + $result = $instance->after($this->request, $this->response, $arguments); if ($result instanceof ResponseInterface) { $this->response = $result; @@ -255,40 +288,80 @@ private function runAfter(array $filterClasses): ResponseInterface } /** - * Runs "Required Filters" for the specified position. - * - * @return RequestInterface|ResponseInterface|string|null + * @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; + } + + /** + * 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 = []; + $filterClassList = []; foreach ($filters as $alias) { if (is_array($aliases[$alias])) { - $filterClasses[$position] = array_merge($filterClasses[$position], $aliases[$alias]); + foreach ($this->config->aliases[$alias] as $class) { + $filterClassList[] = [$class, []]; + } } else { - $filterClasses[$position][] = $aliases[$alias]; + $filterClassList[] = [$aliases[$alias], []]; } } + return $filterClassList; + } + + /** + * 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') + { + $filterClassList = $this->getRequiredClasses($position); + + if ($filterClassList === []) { + return $position === 'before' ? $this->request : $this->response; + } + if ($position === 'before') { - return $this->runBefore($filterClasses[$position]); + return $this->runBefore($filterClassList); } // After - return $this->runAfter($filterClasses[$position]); + return $this->runAfter($filterClassList); } /** @@ -379,6 +452,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) { @@ -403,6 +479,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'); @@ -433,6 +514,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 { @@ -442,6 +528,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 { @@ -453,9 +544,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); @@ -463,13 +556,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; } @@ -481,53 +574,54 @@ 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' + * @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 $when = 'before'): void + private function enableFilter(string $filter, string $position = 'before'): void { - // Get arguments and clean name - [$name, $arguments] = $this->getCleanName($name); - $this->arguments[$name] = ($arguments !== []) ? $arguments : null; + // Normalize the arguments. + [$alias, $arguments] = $this->getCleanName($filter); + $filter = ($arguments === []) ? $alias : $alias . ':' . implode(',', $arguments); - if (class_exists($name)) { - $this->config->aliases[$name] = $name; - } elseif (! array_key_exists($name, $this->config->aliases)) { - throw FilterException::forNoAlias($name); + if (class_exists($alias)) { + $this->config->aliases[$alias] = $alias; + } elseif (! array_key_exists($alias, $this->config->aliases)) { + throw FilterException::forNoAlias($alias); } - $classNames = (array) $this->config->aliases[$name]; - - foreach ($classNames as $className) { - $this->argumentsClass[$className] = $this->arguments[$name] ?? null; + if (! isset($this->filters[$position][$filter])) { + $this->filters[$position][] = $filter; } - if (! isset($this->filters[$when][$name])) { - $this->filters[$when][] = $name; - $this->filtersClass[$when] = array_merge($this->filtersClass[$when], $classNames); - } + // Since some filters like rate limiters rely on being executed once a request, + // we filter em here. + $this->filters[$position] = array_unique($this->filters[$position]); } /** * 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); - - $arguments = explode(',', $arguments); - array_walk($arguments, static function (&$item): void { - $item = trim($item); - }); + if (! str_contains($filter, ':')) { + return [$filter, $arguments]; } - return [$name, $arguments]; + [$alias, $arguments] = explode(':', $filter); + + $arguments = explode(',', $arguments); + array_walk($arguments, static function (&$item): void { + $item = trim($item); + }); + + return [$alias, $arguments]; } /** @@ -537,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); } @@ -554,6 +648,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) { @@ -683,18 +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($filter); + $filter = ($arguments === []) ? $alias : $alias . ':' . implode(',', $arguments); + // Look for inclusion rules if (isset($settings['before'])) { $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'][] = $filter; } } @@ -702,14 +797,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'][] = $filter; } } } @@ -733,68 +821,41 @@ 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 * + * @phpstan-param 'before'|'after' $position + * * @return void * * @throws FilterException */ protected function processAliasesToClass(string $position) { - $filterClasses = []; + $filterClassList = []; - 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) { + $filterClassList[] = [$class, $arguments]; + } } else { - $filterClasses[] = $this->config->aliases[$alias]; + $filterClassList[] = [$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]); + $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); } - - // 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/system/Filters/ForceHTTPS.php b/system/Filters/ForceHTTPS.php index f415bd00e3b8..aa9cb3716d08 100644 --- a/system/Filters/ForceHTTPS.php +++ b/system/Filters/ForceHTTPS.php @@ -32,14 +32,14 @@ class ForceHTTPS implements FilterInterface * * @param array|null $arguments * - * @return ResponseInterface|void + * @return ResponseInterface|null */ public function before(RequestInterface $request, $arguments = null) { $config = config(App::class); if ($config->forceGlobalSecureRequests !== true) { - return; + return null; } $response = service('response'); @@ -49,16 +49,17 @@ public function before(RequestInterface $request, $arguments = null) } catch (RedirectException $e) { return $e->getResponse(); } + + return null; } /** * We don't have anything to do here. * * @param array|null $arguments - * - * @return void */ public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { + return null; } } diff --git a/system/Filters/Honeypot.php b/system/Filters/Honeypot.php index cdff293aacb4..4b24691ef905 100644 --- a/system/Filters/Honeypot.php +++ b/system/Filters/Honeypot.php @@ -36,12 +36,14 @@ class Honeypot implements FilterInterface public function before(RequestInterface $request, $arguments = null) { if (! $request instanceof IncomingRequest) { - return; + return null; } if (service('honeypot')->hasContent($request)) { throw HoneypotException::isBot(); } + + return null; } /** @@ -52,5 +54,7 @@ public function before(RequestInterface $request, $arguments = null) public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { service('honeypot')->attachHoneypot($response); + + return null; } } diff --git a/system/Filters/InvalidChars.php b/system/Filters/InvalidChars.php index 542b12d7ffbe..1cd775153c0e 100644 --- a/system/Filters/InvalidChars.php +++ b/system/Filters/InvalidChars.php @@ -48,13 +48,11 @@ class InvalidChars implements FilterInterface * Check invalid characters. * * @param list|null $arguments - * - * @return void */ public function before(RequestInterface $request, $arguments = null) { if (! $request instanceof IncomingRequest) { - return; + return null; } $data = [ @@ -69,17 +67,18 @@ public function before(RequestInterface $request, $arguments = null) $this->checkEncoding($values); $this->checkControl($values); } + + return null; } /** * We don't have anything to do here. * * @param list|null $arguments - * - * @return void */ public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { + return null; } /** diff --git a/system/Filters/PageCache.php b/system/Filters/PageCache.php index a3d3af832a65..b0013c81ce37 100644 --- a/system/Filters/PageCache.php +++ b/system/Filters/PageCache.php @@ -38,7 +38,7 @@ public function __construct() * * @param array|null $arguments * - * @return ResponseInterface|void + * @return ResponseInterface|null */ public function before(RequestInterface $request, $arguments = null) { @@ -51,14 +51,14 @@ public function before(RequestInterface $request, $arguments = null) if ($cachedResponse instanceof ResponseInterface) { return $cachedResponse; } + + return null; } /** * Cache the page. * * @param array|null $arguments - * - * @return void */ public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { @@ -72,6 +72,10 @@ public function after(RequestInterface $request, ResponseInterface $response, $a // so that we can have live speed updates along the way. // Must be run after filters to preserve the Response headers. $this->pageCache->make($request, $response); + + return $response; } + + return null; } } diff --git a/system/Filters/PerformanceMetrics.php b/system/Filters/PerformanceMetrics.php index 51b3fa9f57d5..97582ad8dc05 100644 --- a/system/Filters/PerformanceMetrics.php +++ b/system/Filters/PerformanceMetrics.php @@ -28,14 +28,13 @@ class PerformanceMetrics implements FilterInterface */ public function before(RequestInterface $request, $arguments = null) { + return null; } /** * Replaces the performance metrics. * * @param array|null $arguments - * - * @return void */ public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { @@ -57,6 +56,10 @@ public function after(RequestInterface $request, ResponseInterface $response, $a ); $response->setBody($output); + + return $response; } + + return null; } } diff --git a/system/Filters/SecureHeaders.php b/system/Filters/SecureHeaders.php index b8bd6e05f8eb..42e5842384db 100644 --- a/system/Filters/SecureHeaders.php +++ b/system/Filters/SecureHeaders.php @@ -52,24 +52,23 @@ class SecureHeaders implements FilterInterface * We don't have anything to do here. * * @param list|null $arguments - * - * @return void */ public function before(RequestInterface $request, $arguments = null) { + return null; } /** * Add security headers. * * @param list|null $arguments - * - * @return void */ public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { foreach ($this->headers as $header => $value) { $response->setHeader($header, $value); } + + return $response; } } 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/HTTP/CLIRequest.php b/system/HTTP/CLIRequest.php index fbc5165e9a9a..6ef97170914e 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/CURLRequest.php b/system/HTTP/CURLRequest.php index 9ce664f80865..e9e2d89d9481 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. @@ -649,6 +649,15 @@ protected function setCURLOptions(array $curlOptions = [], array $config = []) $this->setHeader('Content-Length', (string) strlen($json)); } + // Resolve IP + if (array_key_exists('force_ip_resolve', $config)) { + $curlOptions[CURLOPT_IPRESOLVE] = match ($config['force_ip_resolve']) { + 'v4' => CURL_IPRESOLVE_V4, + 'v6' => CURL_IPRESOLVE_V6, + default => CURL_IPRESOLVE_WHATEVER, + }; + } + // version if (! empty($config['version'])) { $version = sprintf('%.1F', $config['version']); @@ -658,6 +667,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/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 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/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 5ebec8e1f7a4..792ffe921d70 100644 --- a/system/HTTP/Exceptions/RedirectException.php +++ b/system/HTTP/Exceptions/RedirectException.php @@ -14,17 +14,17 @@ namespace CodeIgniter\HTTP\Exceptions; 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 Exception; -use InvalidArgumentException; -use LogicException; use Throwable; /** * RedirectException */ -class RedirectException extends Exception implements ResponsableInterface, HTTPExceptionInterface +class RedirectException extends RuntimeException implements ExceptionInterface, ResponsableInterface, HTTPExceptionInterface { /** * HTTP status code for redirects diff --git a/system/HTTP/Files/UploadedFileInterface.php b/system/HTTP/Files/UploadedFileInterface.php index 12cc25a0d050..6e849d04a76b 100644 --- a/system/HTTP/Files/UploadedFileInterface.php +++ b/system/HTTP/Files/UploadedFileInterface.php @@ -13,8 +13,8 @@ namespace CodeIgniter\HTTP\Files; -use InvalidArgumentException; -use RuntimeException; +use CodeIgniter\Exceptions\InvalidArgumentException; +use CodeIgniter\Exceptions\RuntimeException; /** * Value object representing a single file uploaded through an diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index c905de0a3ae3..ba6f61d7349f 100644 --- 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 6b2a5e8ba2c4..5a490e09f780 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 9ae7737a8e8f..50daed9c0f8f 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/Negotiate.php b/system/HTTP/Negotiate.php index ff4d2998766b..f35a761c2fc4 100644 --- a/system/HTTP/Negotiate.php +++ b/system/HTTP/Negotiate.php @@ -14,6 +14,7 @@ namespace CodeIgniter\HTTP; use CodeIgniter\HTTP\Exceptions\HTTPException; +use Config\Feature; /** * Class Negotiate @@ -122,11 +123,15 @@ public function encoding(array $supported = []): string * types the application says it supports, and the types requested * by the client. * - * If no match is found, the first, highest-ranking client requested + * If strict locale negotiation is disabled and no match is found, the first, highest-ranking client requested * type is returned. */ public function language(array $supported): string { + if (config(Feature::class)->strictLocaleNegotiation) { + return $this->getBestLocaleMatch($supported, $this->request->getHeaderLine('accept-language')); + } + return $this->getBestMatch($supported, $this->request->getHeaderLine('accept-language'), false, false, true); } @@ -189,6 +194,69 @@ protected function getBestMatch( return $strictMatch ? '' : $supported[0]; } + /** + * Try to find the best matching locale. It supports strict locale comparison. + * + * If Config\App::$supportedLocales have "en-US" and "en-GB" locales, they can be recognized + * as two different locales. This method checks first for the strict match, then fallback + * to the most general locale (in this case "en") ISO 639-1 and finally to the locale variant + * "en-*" (ISO 639-1 plus "wildcard" for ISO 3166-1 alpha-2). + * + * If nothing from above is matched, then it returns the first option from the $supportedLocales array. + * + * @param list $supportedLocales App-supported values + * @param ?string $header Compatible 'Accept-Language' header string + */ + protected function getBestLocaleMatch(array $supportedLocales, ?string $header): string + { + if ($supportedLocales === []) { + throw HTTPException::forEmptySupportedNegotiations(); + } + + if ($header === null || $header === '') { + return $supportedLocales[0]; + } + + $acceptable = $this->parseHeader($header); + $fallbackLocales = []; + + foreach ($acceptable as $accept) { + // if acceptable quality is zero, skip it. + if ($accept['q'] === 0.0) { + continue; + } + + // if acceptable value is "anything", return the first available + if ($accept['value'] === '*') { + return $supportedLocales[0]; + } + + // look for exact match + if (in_array($accept['value'], $supportedLocales, true)) { + return $accept['value']; + } + + // set a fallback locale + $fallbackLocales[] = strtok($accept['value'], '-'); + } + + foreach ($fallbackLocales as $fallbackLocale) { + // look for exact match + if (in_array($fallbackLocale, $supportedLocales, true)) { + return $fallbackLocale; + } + + // look for regional locale match + foreach ($supportedLocales as $locale) { + if (str_starts_with($locale, $fallbackLocale . '-')) { + return $locale; + } + } + } + + return $supportedLocales[0]; + } + /** * Parses an Accept* header into it's multiple values. * 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/Response.php b/system/HTTP/Response.php index ac853c3de93b..9de285b85d08 100644 --- a/system/HTTP/Response.php +++ b/system/HTTP/Response.php @@ -204,21 +204,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 58f11a0f9065..66053c412939 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. @@ -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/system/HTTP/ResponseTrait.php b/system/HTTP/ResponseTrait.php index b23e40a9046b..d39e8f9fdfaf 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 @@ -406,16 +406,19 @@ public function sendHeaders() if ($value instanceof Header) { header( $name . ': ' . $value->getValueLine(), - false, + true, $this->getStatusCode(), ); } else { + $replace = true; + foreach ($value as $header) { header( $name . ': ' . $header->getValueLine(), - false, + $replace, $this->getStatusCode(), ); + $replace = false; } } } diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index 63181b89fead..25b5db560d4b 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 c9d634d3846d..dc0fffbd79ea 100644 --- a/system/HTTP/URI.php +++ b/system/HTTP/URI.php @@ -13,10 +13,10 @@ namespace CodeIgniter\HTTP; -use BadMethodCallException; +use CodeIgniter\Exceptions\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 1b812e6a5123..6741dc4bbb0e 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/Helpers/filesystem_helper.php b/system/Helpers/filesystem_helper.php index 9c0196b4752f..c45bcdae0d64 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')) { diff --git a/system/Helpers/number_helper.php b/system/Helpers/number_helper.php index 0999d200f99b..bf577ed293f6 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')) { diff --git a/system/Helpers/text_helper.php b/system/Helpers/text_helper.php index 6e9db747305c..c8067a88a74b 100644 --- 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 @@ -43,35 +44,40 @@ function word_limiter(string $str, int $limit = 100, string $endChar = '…' /** * Character Limiter * - * Limits the string based on the character count. Preserves complete words + * Limits the string based on the character count. Preserves complete words * so the character count may not be exactly as specified. * * @param string $endChar the end character. Usually an ellipsis */ - function character_limiter(string $str, int $n = 500, string $endChar = '…'): string + function character_limiter(string $string, int $limit = 500, string $endChar = '…'): string { - if (mb_strlen($str) < $n) { - return $str; + if (mb_strlen($string) < $limit) { + return $string; } // a bit complicated, but faster than preg_replace with \s+ - $str = preg_replace('/ {2,}/', ' ', str_replace(["\r", "\n", "\t", "\x0B", "\x0C"], ' ', $str)); + $string = preg_replace('/ {2,}/', ' ', str_replace(["\r", "\n", "\t", "\x0B", "\x0C"], ' ', $string)); + $stringLength = mb_strlen($string); - if (mb_strlen($str) <= $n) { - return $str; + if ($stringLength <= $limit) { + return $string; } - $out = ''; + $output = ''; + $outputLength = 0; + $words = explode(' ', trim($string)); - foreach (explode(' ', trim($str)) as $val) { - $out .= $val . ' '; - if (mb_strlen($out) >= $n) { - $out = trim($out); + foreach ($words as $word) { + $output .= $word . ' '; + $outputLength = mb_strlen($output); + + if ($outputLength >= $limit) { + $output = trim($output); break; } } - return (mb_strlen($out) === mb_strlen($str)) ? $out : $out . $endChar; + return ($outputLength === $stringLength) ? $output : $output . $endChar; } } @@ -711,38 +717,44 @@ function alternator(...$args): string function excerpt(string $text, ?string $phrase = null, int $radius = 100, string $ellipsis = '...'): string { if (isset($phrase)) { - $phrasePos = stripos($text, $phrase); - $phraseLen = strlen($phrase); + $phrasePosition = mb_stripos($text, $phrase); + $phraseLength = mb_strlen($phrase); } else { - $phrasePos = $radius / 2; - $phraseLen = 1; + $phrasePosition = $radius / 2; + $phraseLength = 1; } - $pre = explode(' ', substr($text, 0, $phrasePos)); - $pos = explode(' ', substr($text, $phrasePos + $phraseLen)); + $beforeWords = explode(' ', mb_substr($text, 0, $phrasePosition)); + $afterWords = explode(' ', mb_substr($text, $phrasePosition + $phraseLength)); - $prev = ' '; - $post = ' '; - $count = 0; + $firstPartOutput = ' '; + $endPartOutput = ' '; + $count = 0; - foreach (array_reverse($pre) as $e) { - if ((strlen($e) + $count + 1) < $radius) { - $prev = ' ' . $e . $prev; + foreach (array_reverse($beforeWords) as $beforeWord) { + $beforeWordLength = mb_strlen($beforeWord); + + if (($beforeWordLength + $count + 1) < $radius) { + $firstPartOutput = ' ' . $beforeWord . $firstPartOutput; } - $count = ++$count + strlen($e); + + $count = ++$count + $beforeWordLength; } $count = 0; - foreach ($pos as $s) { - if ((strlen($s) + $count + 1) < $radius) { - $post .= $s . ' '; + foreach ($afterWords as $afterWord) { + $afterWordLength = mb_strlen($afterWord); + + if (($afterWordLength + $count + 1) < $radius) { + $endPartOutput .= $afterWord . ' '; } - $count = ++$count + strlen($s); + + $count = ++$count + $afterWordLength; } $ellPre = $phrase !== null ? $ellipsis : ''; - return str_replace(' ', ' ', $ellPre . $prev . $phrase . $post . $ellipsis); + return str_replace(' ', ' ', $ellPre . $firstPartOutput . $phrase . $endPartOutput . $ellipsis); } } 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/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..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. @@ -39,10 +41,29 @@ * @property string $weekOfYear read-only * @property string $year read-only * + * @phpstan-consistent-constructor + * * @deprecated Use Time instead. * @see \CodeIgniter\I18n\TimeLegacyTest */ 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); + } } diff --git a/system/I18n/TimeTrait.php b/system/I18n/TimeTrait.php index bcdc86314974..5e908bef5c2e 100644 --- a/system/I18n/TimeTrait.php +++ b/system/I18n/TimeTrait.php @@ -78,13 +78,13 @@ 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'); + $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); @@ -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'), $timezone); + return new static($date->format('Y-m-d H:i:s.u'), $timezone); } /** @@ -261,15 +261,13 @@ 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); + $time = new static(sprintf('@%.6f', $timestamp), 'UTC', $locale); - $timezone ??= date_default_timezone_get(); + $timezone ??= 'UTC'; return $time->setTimezone($timezone); } @@ -277,22 +275,22 @@ public static function createFromTimestamp(int $timestamp, $timezone = null, ?st /** * Takes an instance of DateTimeInterface and returns an instance of Time with it's same values. * - * @return self + * @return static * * @throws Exception */ 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); + 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 * @@ -302,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); } /** @@ -314,10 +312,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(), + ); } // -------------------------------------------------------------------- @@ -346,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'), $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; @@ -476,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()); } /** @@ -533,7 +532,7 @@ public function getTimezoneName(): string * * @param int|string $value * - * @return self + * @return static * * @throws Exception */ @@ -547,7 +546,7 @@ public function setYear($value) * * @param int|string $value * - * @return self + * @return static * * @throws Exception */ @@ -569,7 +568,7 @@ public function setMonth($value) * * @param int|string $value * - * @return self + * @return static * * @throws Exception */ @@ -593,7 +592,7 @@ public function setDay($value) * * @param int|string $value * - * @return self + * @return static * * @throws Exception */ @@ -611,7 +610,7 @@ public function setHour($value) * * @param int|string $value * - * @return self + * @return static * * @throws Exception */ @@ -629,7 +628,7 @@ public function setMinute($value) * * @param int|string $value * - * @return self + * @return static * * @throws Exception */ @@ -647,7 +646,7 @@ public function setSecond($value) * * @param int $value * - * @return self + * @return static * * @throws Exception */ @@ -657,7 +656,7 @@ protected function setValue(string $name, $value) ${$name} = $value; - return self::create( + return static::create( (int) $year, (int) $month, (int) $day, @@ -674,7 +673,7 @@ protected function setValue(string $name, $value) * * @param DateTimeZone|string $timezone * - * @return self + * @return static * * @throws Exception */ @@ -683,24 +682,7 @@ public function setTimezone($timezone) { $timezone = $timezone instanceof DateTimeZone ? $timezone : new DateTimeZone($timezone); - return self::createFromInstance($this->toDateTime()->setTimezone($timezone), $this->locale); - } - - /** - * Returns a new instance with the date set to the new timestamp. - * - * @param int $timestamp - * - * @return self - * - * @throws Exception - */ - #[ReturnTypeWillChange] - public function setTimestamp($timestamp) - { - $time = date('Y-m-d H:i:s', $timestamp); - - return self::parse($time, $this->timezone, $this->locale); + return static::createFromInstance($this->toDateTime()->setTimezone($timezone), $this->locale); } // -------------------------------------------------------------------- @@ -941,9 +923,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 +938,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 !== null && $timezone !== '' && $timezone !== '0' ? $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; } @@ -979,10 +961,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; } /** @@ -995,10 +983,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; } // -------------------------------------------------------------------- @@ -1074,7 +1068,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(); } @@ -1105,7 +1099,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 !== null && $timezone !== '' && $timezone !== '0' ? $timezone : $this->timezone; 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. diff --git a/system/Images/Handlers/BaseHandler.php b/system/Images/Handlers/BaseHandler.php index 30c87535479e..9c263afb89d1 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 @@ -700,6 +700,8 @@ public function __call(string $name, array $args = []) if (method_exists($this->image(), $name)) { return $this->image()->{$name}(...$args); } + + return null; } /** diff --git a/system/Language/Language.php b/system/Language/Language.php index 9847553127fe..a4034bdf6b90 100644 --- a/system/Language/Language.php +++ b/system/Language/Language.php @@ -232,7 +232,7 @@ protected function formatMessage($message, array $args = []) * will return the file's contents, otherwise will merge with * the existing language lines. * - * @return array|void + * @return list|null */ protected function load(string $file, string $locale, bool $return = false) { @@ -265,6 +265,8 @@ protected function load(string $file, string $locale, bool $return = false) // Merge our string $this->language[$locale][$file] = $lang; + + return null; } /** diff --git a/system/Language/en/Errors.php b/system/Language/en/Errors.php index 29d6ad364687..bd2fc963412b 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 is wrong with your request.', 'whoops' => 'Whoops!', 'weHitASnag' => 'We seem to have hit a snag. Please try again later...', ]; 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/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.', ]; diff --git a/system/Log/Logger.php b/system/Log/Logger.php index fca608f91ff8..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; @@ -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/Model.php b/system/Model.php index 8734ceec954a..473584cb8163 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/Pager/PagerRenderer.php b/system/Pager/PagerRenderer.php index 7257f2becf31..2352a98460e3 100644 --- a/system/Pager/PagerRenderer.php +++ b/system/Pager/PagerRenderer.php @@ -82,6 +82,21 @@ class PagerRenderer */ protected $pageSelector; + /** + * Returns the number of results per page that should be shown. + */ + protected ?int $perPage; + + /** + * The number of items the page starts with. + */ + protected ?int $perPageStart = null; + + /** + * The number of items the page ends with. + */ + protected ?int $perPageEnd = null; + /** * Constructor. */ @@ -98,6 +113,8 @@ public function __construct(array $details) $this->pageCount = $details['pageCount']; $this->segment = $details['segment'] ?? 0; $this->pageSelector = $details['pageSelector'] ?? 'page'; + $this->perPage = $details['perPage'] ?? null; + $this->updatePerPages(); } /** @@ -307,6 +324,28 @@ protected function updatePages(?int $count = null) $this->last = $this->current + $count <= $this->pageCount ? $this->current + $count : (int) $this->pageCount; } + /** + * Updates the start and end items per pages, which is + * the number of items displayed on the active page. + */ + protected function updatePerPages(): void + { + if ($this->total === null || $this->perPage === null) { + return; + } + + // When the page is the last, perform a different calculation. + if ($this->last === $this->current) { + $this->perPageStart = $this->perPage * ($this->current - 1) + 1; + $this->perPageEnd = $this->total; + + return; + } + + $this->perPageStart = $this->current === 1 ? 1 : ($this->perPage * $this->current) - $this->perPage + 1; + $this->perPageEnd = $this->perPage * $this->current; + } + /** * Checks to see if there is a "previous" page before our "first" page. */ @@ -430,4 +469,36 @@ public function getNextPageNumber(): ?int { return ($this->current === $this->pageCount) ? null : $this->current + 1; } + + /** + * Returns the total items of the page. + */ + public function getTotal(): ?int + { + return $this->total; + } + + /** + * Returns the number of items to be displayed on the page. + */ + public function getPerPage(): ?int + { + return $this->perPage; + } + + /** + * Returns the number of items the page starts with. + */ + public function getPerPageStart(): ?int + { + return $this->perPageStart; + } + + /** + * Returns the number of items the page ends with. + */ + public function getPerPageEnd(): ?int + { + return $this->perPageEnd; + } } 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..d65765947db1 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; /** @@ -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/system/Router/DefinedRouteCollector.php b/system/Router/DefinedRouteCollector.php index 2452a141fa75..0aa4fdd063d8 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) { } 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 +{ +} diff --git a/system/Router/Exceptions/MethodNotFoundException.php b/system/Router/Exceptions/MethodNotFoundException.php index 6e82fb09583c..2dabad79b6a1 100644 --- a/system/Router/Exceptions/MethodNotFoundException.php +++ b/system/Router/Exceptions/MethodNotFoundException.php @@ -13,11 +13,11 @@ namespace CodeIgniter\Router\Exceptions; -use RuntimeException; +use CodeIgniter\Exceptions\RuntimeException; /** * @internal */ -final class MethodNotFoundException extends RuntimeException +final class MethodNotFoundException extends RuntimeException implements ExceptionInterface { } diff --git a/system/Router/Exceptions/RedirectException.php b/system/Router/Exceptions/RedirectException.php deleted file mode 100644 index 4baef3278dd9..000000000000 --- a/system/Router/Exceptions/RedirectException.php +++ /dev/null @@ -1,32 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -namespace CodeIgniter\Router\Exceptions; - -use CodeIgniter\Exceptions\HTTPExceptionInterface; -use Exception; - -/** - * RedirectException - * - * @deprecated Use \CodeIgniter\HTTP\Exceptions\RedirectException instead - */ -class RedirectException extends Exception implements HTTPExceptionInterface -{ - /** - * HTTP status code for redirects - * - * @var int - */ - protected $code = 302; -} 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 diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 400ef6773b0d..66c3ace6fe1a 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -15,13 +15,13 @@ use Closure; use CodeIgniter\Autoloader\FileLocatorInterface; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\HTTP\Method; use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\Router\Exceptions\RouterException; use Config\App; use Config\Modules; use Config\Routing; -use InvalidArgumentException; /** * @todo Implement nested resource routing (See CakePHP) @@ -1563,7 +1563,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 { @@ -1572,6 +1572,13 @@ private function checkHostname($hostname): bool return false; } + // Has multiple hostnames + 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/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; } diff --git a/system/Router/Router.php b/system/Router/Router.php index 8de447d6cc3c..9ce9a5c7a553 100644 --- a/system/Router/Router.php +++ b/system/Router/Router.php @@ -162,6 +162,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(), @@ -171,7 +173,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(), @@ -402,7 +404,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 diff --git a/system/Security/CheckPhpIni.php b/system/Security/CheckPhpIni.php index 1e92cdc403aa..e2ff075678cd 100644 --- a/system/Security/CheckPhpIni.php +++ b/system/Security/CheckPhpIni.php @@ -27,11 +27,11 @@ class CheckPhpIni /** * @param bool $isCli Set false if you run via Web * - * @return string|void HTML string or void in CLI + * @return string|null HTML string or void in CLI */ - public static function run(bool $isCli = true) + public static function run(bool $isCli = true, ?string $argument = null) { - $output = static::checkIni(); + $output = static::checkIni($argument); $thead = ['Directive', 'Global', 'Current', 'Recommended', 'Remark']; $tbody = []; @@ -40,7 +40,7 @@ public static function run(bool $isCli = true) if ($isCli) { self::outputForCli($output, $thead, $tbody); - return; + return null; } // Web @@ -115,8 +115,9 @@ private static function outputForWeb(array $output, array $thead, array $tbody): * @internal Used for testing purposes only. * @testTag */ - public static function checkIni(): array + public static function checkIni(?string $argument = null): array { + // Default items $items = [ 'error_reporting' => ['recommended' => '5111'], 'display_errors' => ['recommended' => '0'], @@ -124,19 +125,41 @@ 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'], 'mbstring.language' => ['recommended' => 'neutral'], 'opcache.enable' => ['recommended' => '1'], - 'opcache.enable_cli' => [], - 'opcache.jit' => [], - 'opcache.jit_buffer_size' => [], + 'opcache.enable_cli' => ['recommended' => '1'], + 'opcache.jit' => ['recommended' => 'tracing'], + 'opcache.jit_buffer_size' => ['recommended' => '128', 'remark' => 'Adjust with your free space of memory'], + 'zend.assertions' => ['recommended' => '-1'], ]; + if ($argument === 'opcache') { + $items = [ + 'opcache.enable' => ['recommended' => '1'], + 'opcache.enable_cli' => ['recommended' => '0', 'remark' => 'Enable when you using CLI'], + 'opcache.jit' => ['recommended' => 'tracing', 'remark' => 'Disable when you used third-party extensions'], + 'opcache.jit_buffer_size' => ['recommended' => '128', 'remark' => 'Adjust with your free space of memory'], + 'opcache.memory_consumption' => ['recommended' => '128', 'remark' => 'Adjust with your free space of memory'], + 'opcache.interned_strings_buffer' => ['recommended' => '16'], + 'opcache.max_accelerated_files' => ['remark' => 'Adjust based on the number of PHP files in your project (e.g.: find your_project/ -iname \'*.php\'|wc -l)'], + 'opcache.max_wasted_percentage' => ['recommended' => '10'], + 'opcache.validate_timestamps' => ['recommended' => '0', 'remark' => 'When you disabled, opcache hold your code into shared memory. Restart webserver needed'], + 'opcache.revalidate_freq' => [], + 'opcache.file_cache' => ['remark' => 'Location file caching, It should improve performance when SHM memory is full'], + 'opcache.file_cache_only' => ['remark' => 'Opcode caching in shared memory, Disabled when you using Windows'], + 'opcache.file_cache_fallback' => ['remark' => 'Set enable when you using Windows'], + 'opcache.save_comments' => ['recommended' => '0', 'remark' => 'Enable when you using package require docblock annotation'], + ]; + } + $output = []; $ini = ini_get_all(); diff --git a/system/Security/Security.php b/system/Security/Security.php index 574da8d50dc1..9e87a2177cda 100644 --- a/system/Security/Security.php +++ b/system/Security/Security.php @@ -14,6 +14,8 @@ namespace CodeIgniter\Security; use CodeIgniter\Cookie\Cookie; +use CodeIgniter\Exceptions\InvalidArgumentException; +use CodeIgniter\Exceptions\LogicException; use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\Method; use CodeIgniter\HTTP\Request; @@ -24,8 +26,6 @@ use Config\Cookie as CookieConfig; use Config\Security as SecurityConfig; use ErrorException; -use InvalidArgumentException; -use LogicException; /** * Class Security diff --git a/system/Session/Handlers/FileHandler.php b/system/Session/Handlers/FileHandler.php index 5a2e864453bf..68fcd894c583 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 a9b7bd4fa5fe..05d0011dabdc 100644 --- a/system/Session/Session.php +++ b/system/Session/Session.php @@ -205,7 +205,7 @@ public function __construct(SessionHandlerInterface $driver, SessionConfig $conf /** * Initialize the session container and starts up the session. * - * @return $this|void + * @return $this|null */ public function start() { @@ -213,20 +213,20 @@ public function start() // @codeCoverageIgnoreStart $this->logger->debug('Session: Initialization under CLI aborted.'); - return; + return null; // @codeCoverageIgnoreEnd } if ((bool) ini_get('session.auto_start')) { $this->logger->error('Session: session.auto_start is enabled in php.ini. Aborting.'); - return; + return null; } if (session_status() === PHP_SESSION_ACTIVE) { $this->logger->warning('Session: Sessions is enabled, and one exists. Please don\'t $session->start();'); - return; + return null; } $this->configure(); @@ -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); + $bitsPerCharacter = (int) ini_get('session.sid_bits_per_character'); + $sidLength = (int) ini_get('session.sid_length'); - $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); - } - - // 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}'; } /** diff --git a/system/Test/CIUnitTestCase.php b/system/Test/CIUnitTestCase.php index 997c0cded837..0c39e69e14b0 100644 --- a/system/Test/CIUnitTestCase.php +++ b/system/Test/CIUnitTestCase.php @@ -461,7 +461,7 @@ public function assertCloseEnough(int $expected, $actual, string $message = '', * @param mixed $expected * @param mixed $actual * - * @return bool|void + * @return bool|null * * @throws Exception */ @@ -482,6 +482,8 @@ public function assertCloseEnoughString($expected, $actual, string $message = '' } catch (Exception) { return false; } + + return null; } // -------------------------------------------------------------------- diff --git a/system/Test/ConfigFromArrayTrait.php b/system/Test/ConfigFromArrayTrait.php index c6beb6e63795..330549118e22 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/ControllerTestTrait.php b/system/Test/ControllerTestTrait.php index 5a5bc708692f..2ac68938e251 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..f46ce1d65206 100644 --- a/system/Test/DOMParser.php +++ b/system/Test/DOMParser.php @@ -13,11 +13,11 @@ namespace CodeIgniter\Test; -use BadMethodCallException; +use CodeIgniter\Exceptions\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/Fabricator.php b/system/Test/Fabricator.php index ffd4cd288450..6c7533e19194 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 RuntimeException; +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 } diff --git a/system/Test/FilterTestTrait.php b/system/Test/FilterTestTrait.php index d004ec22c5c1..d3091b6b4ed7 100644 --- a/system/Test/FilterTestTrait.php +++ b/system/Test/FilterTestTrait.php @@ -14,6 +14,8 @@ namespace CodeIgniter\Test; use Closure; +use CodeIgniter\Exceptions\InvalidArgumentException; +use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\Filters\Exceptions\FilterException; use CodeIgniter\Filters\FilterInterface; use CodeIgniter\Filters\Filters; @@ -21,8 +23,6 @@ use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\Router\RouteCollection; use Config\Filters as FiltersConfig; -use InvalidArgumentException; -use RuntimeException; /** * Filter Test Trait 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/system/Test/Mock/MockInputOutput.php b/system/Test/Mock/MockInputOutput.php index 87d09832df56..7f4370a2d22f 100644 --- a/system/Test/Mock/MockInputOutput.php +++ b/system/Test/Mock/MockInputOutput.php @@ -14,10 +14,10 @@ namespace CodeIgniter\Test\Mock; use CodeIgniter\CLI\InputOutput; +use CodeIgniter\Exceptions\InvalidArgumentException; +use CodeIgniter\Exceptions\LogicException; use CodeIgniter\Test\Filters\CITestStreamFilter; use CodeIgniter\Test\PhpStreamWrapper; -use InvalidArgumentException; -use LogicException; final class MockInputOutput extends InputOutput { 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 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/system/ThirdParty/Escaper/Escaper.php b/system/ThirdParty/Escaper/Escaper.php index c4964cb55e4a..1e5bc7f9fad9 100644 --- a/system/ThirdParty/Escaper/Escaper.php +++ b/system/ThirdParty/Escaper/Escaper.php @@ -25,6 +25,8 @@ /** * Context specific methods for use in secure output escaping + * + * @final */ class Escaper { @@ -49,7 +51,7 @@ class Escaper * Current encoding for escaping. If not UTF-8, we convert strings from this encoding * pre-escaping and back to this encoding post-escaping. * - * @var string + * @var non-empty-string */ protected $encoding = 'utf-8'; @@ -88,7 +90,7 @@ class Escaper /** * List of all encoding supported by this class * - * @var array + * @var list */ protected $supportedEncodings = [ 'iso-8859-1', @@ -131,6 +133,7 @@ class Escaper * Constructor: Single parameter allows setting of global encoding for use by * the current object. * + * @param non-empty-string|null $encoding * @throws Exception\InvalidArgumentException */ public function __construct(?string $encoding = null) @@ -159,25 +162,19 @@ public function __construct(?string $encoding = null) // set matcher callbacks $this->htmlAttrMatcher = /** @param array $matches */ - function (array $matches): string { - return $this->htmlAttrMatcher($matches); - }; + fn(array $matches): string => $this->htmlAttrMatcher($matches); $this->jsMatcher = /** @param array $matches */ - function (array $matches): string { - return $this->jsMatcher($matches); - }; + fn(array $matches): string => $this->jsMatcher($matches); $this->cssMatcher = /** @param array $matches */ - function (array $matches): string { - return $this->cssMatcher($matches); - }; + fn(array $matches): string => $this->cssMatcher($matches); } /** * Return the encoding that all output/input is expected to be encoded in. * - * @return string + * @return non-empty-string */ public function getEncoding() { @@ -188,7 +185,7 @@ public function getEncoding() * Escape a string for the HTML Body context where there are very few characters * of special meaning. Internally this will use htmlspecialchars(). * - * @return string + * @return ($string is non-empty-string ? non-empty-string : string) */ public function escapeHtml(string $string) { @@ -200,7 +197,7 @@ public function escapeHtml(string $string) * to escape that are not covered by htmlspecialchars() to cover cases where an attribute * might be unquoted or quoted illegally (e.g. backticks are valid quotes for IE). * - * @return string + * @return ($string is non-empty-string ? non-empty-string : string) */ public function escapeHtmlAttr(string $string) { @@ -222,7 +219,7 @@ public function escapeHtmlAttr(string $string) * Backslash escaping is not used as it still leaves the escaped character as-is and so * is not useful in a HTML context. * - * @return string + * @return ($string is non-empty-string ? non-empty-string : string) */ public function escapeJs(string $string) { @@ -240,7 +237,7 @@ public function escapeJs(string $string) * an entire URI - only a subcomponent being inserted. The function is a simple proxy * to rawurlencode() which now implements RFC 3986 since PHP 5.3 completely. * - * @return string + * @return ($string is non-empty-string ? non-empty-string : string) */ public function escapeUrl(string $string) { @@ -251,7 +248,7 @@ public function escapeUrl(string $string) * Escape a string for the CSS context. CSS escaping can be applied to any string being * inserted into CSS and escapes everything except alphanumerics. * - * @return string + * @return ($string is non-empty-string ? non-empty-string : string) */ public function escapeCss(string $string) { 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..8a18a00aee31 100644 --- a/system/ThirdParty/Kint/Parser/ClassStaticsPlugin.php +++ b/system/ThirdParty/Kint/Parser/ClassStaticsPlugin.php @@ -27,17 +27,22 @@ 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\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 +54,177 @@ 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); + $deep = 0 === $this->getParser()->getDepthLimit(); - // Constants - if (!isset(self::$cache[$class])) { - $consts = []; + $r = new ReflectionClass($v->getClassName()); - foreach ($reflection->getConstants() as $name => $val) { - // Skip enum constants - if ($var instanceof UnitEnum && $val instanceof UnitEnum && $o->classname == \get_class($val)) { - continue; - } + if ($statics = $this->getStatics($r, $v->getContext()->getDepth() + 1)) { + $v->addRepresentation(new ContainerRepresentation('Static properties', \array_values($statics), 'statics')); + } - $const = Value::blank($name); - $const->const = true; - $const->depth = $o->depth + 1; - $const->owner_class = $class; - $const->operator = Value::OPERATOR_STATIC; + if ($consts = $this->getCachedConstants($r, $deep)) { + $v->addRepresentation(new ContainerRepresentation('Class constants', \array_values($consts), 'constants')); + } - $creflection = new ReflectionClassConstant($class, $name); + return $v; + } - $const->access = Value::ACCESS_PUBLIC; - if ($creflection->isProtected()) { - $const->access = Value::ACCESS_PROTECTED; - } elseif ($creflection->isPrivate()) { - $const->access = Value::ACCESS_PRIVATE; - } + /** @psalm-return array */ + private function getStatics(ReflectionClass $r, int $depth): array + { + $cdepth = $depth ?: 1; + $class = $r->getName(); + $parent = $r->getParentClass(); - if ($this->parser->childHasPath($o, $const)) { - $const->access_path = '\\'.$class.'::'.$name; - } + $parent_statics = $parent ? $this->getStatics($parent, $depth) : []; + $statics = []; - $const = $this->parser->parse($val, $const); + foreach ($r->getProperties(ReflectionProperty::IS_STATIC) as $pr) { + $canon_name = \strtolower($pr->getDeclaringClass()->name.'::'.$pr->name); - $consts[] = $const; + if ($pr->getDeclaringClass()->name === $class) { + $statics[$canon_name] = $this->buildStaticValue($pr, $cdepth); + } elseif (isset($parent_statics[$canon_name])) { + $statics[$canon_name] = $parent_statics[$canon_name]; + unset($parent_statics[$canon_name]); + } else { + // This should never happen since abstract static properties can't exist + $statics[$canon_name] = $this->buildStaticValue($pr, $cdepth); // @codeCoverageIgnore } + } - self::$cache[$class] = $consts; + foreach ($parent_statics as $canon_name => $value) { + $statics[$canon_name] = $value; } - $statics = new Representation('Static class properties', 'statics'); - $statics->contents = self::$cache[$class]; - - 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; - - $prop->access = Value::ACCESS_PUBLIC; - if ($static->isProtected()) { - $prop->access = Value::ACCESS_PROTECTED; - } elseif ($static->isPrivate()) { - $prop->access = Value::ACCESS_PRIVATE; - } + return $statics; + } - if ($this->parser->childHasPath($o, $prop)) { - $prop->access_path = '\\'.$prop->owner_class.'::'.$prop->name; - } + private function buildStaticValue(ReflectionProperty $pr, int $depth): AbstractValue + { + $context = new StaticPropertyContext( + $pr->name, + $pr->getDeclaringClass()->name, + ClassDeclaredContext::ACCESS_PUBLIC + ); + $context->depth = $depth; + $context->final = KINT_PHP84 && $pr->isFinal(); + + if ($pr->isProtected()) { + $context->access = ClassDeclaredContext::ACCESS_PROTECTED; + } elseif ($pr->isPrivate()) { + $context->access = ClassDeclaredContext::ACCESS_PRIVATE; + } - $static->setAccessible(true); + $parser = $this->getParser(); - if (KINT_PHP74 && !$static->isInitialized()) { - $prop->type = 'uninitialized'; - $statics->contents[] = $prop; - } else { - $static = $static->getValue(); - $statics->contents[] = $this->parser->parse($static, $prop); - } + if ($context->isAccessible($parser->getCallerClass())) { + $context->access_path = '\\'.$context->owner_class.'::$'.$context->name; } - if (empty($statics->contents)) { - return; + $pr->setAccessible(true); + + /** + * @psalm-suppress TooFewArguments + * Appears to have been fixed in master. + */ + if (!$pr->isInitialized()) { + $context->access_path = null; + + return new UninitializedValue($context); } - \usort($statics->contents, ['Kint\\Parser\\ClassStaticsPlugin', 'sort']); + $val = $pr->getValue(); + + $out = $this->getParser()->parse($val, $context); + $context->access_path = null; - $o->addRepresentation($statics); + return $out; } - private static function sort(Value $a, Value $b): int + /** @psalm-return array */ + private function getCachedConstants(ReflectionClass $r, bool $deep): array { - $sort = ((int) $a->const) - ((int) $b->const); - if ($sort) { - return $sort; + $parser = $this->getParser(); + $cdepth = $parser->getDepthLimit() ?: 1; + $deepkey = (int) $deep; + $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][$deepkey])) { + $consts = []; + + $parent_consts = []; + if ($parent = $r->getParentClass()) { + $parent_consts = $this->getCachedConstants($parent, $deep); + } + foreach ($r->getConstants() as $name => $val) { + $cr = new ReflectionClassConstant($class, $name); + + // Skip enum constants + if ($cr->class === $class && \is_a($class, UnitEnum::class, true)) { + continue; + } + + $canon_name = \strtolower($cr->getDeclaringClass()->name.'::'.$name); + + if ($cr->getDeclaringClass()->name === $class) { + $context = $this->buildConstContext($cr); + $context->depth = $cdepth; + + $consts[$canon_name] = $parser->parse($val, $context); + $context->access_path = null; + } elseif (isset($parent_consts[$canon_name])) { + $consts[$canon_name] = $parent_consts[$canon_name]; + } else { + $context = $this->buildConstContext($cr); + $context->depth = $cdepth; + + $consts[$canon_name] = $parser->parse($val, $context); + $context->access_path = null; + } + + unset($parent_consts[$canon_name]); + } + + $this->cache[$class][$deepkey] = $consts + $parent_consts; } - $sort = Value::sortByAccess($a, $b); - if ($sort) { - return $sort; + return $this->cache[$class][$deepkey]; + } + + private function buildConstContext(ReflectionClassConstant $cr): ClassConstContext + { + $context = new ClassConstContext( + $cr->name, + $cr->getDeclaringClass()->name, + ClassDeclaredContext::ACCESS_PUBLIC + ); + $context->final = KINT_PHP81 && $cr->isFinal(); + + if ($cr->isProtected()) { + $context->access = ClassDeclaredContext::ACCESS_PROTECTED; + } elseif ($cr->isPrivate()) { + $context->access = ClassDeclaredContext::ACCESS_PRIVATE; + } else { + $context->access_path = '\\'.$context->owner_class.'::'.$context->name; } - return InstanceValue::sortByHierarchy($a->owner_class, $b->owner_class); + return $context; } } 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..358c0b917094 --- /dev/null +++ b/system/ThirdParty/Kint/Parser/DomPlugin.php @@ -0,0 +1,543 @@ + 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, + 'outerHTML' => false, + 'substitutedNodeValue' => false, + ]; + + public const DOM_NS_VERSIONS = [ + 'outerHTML' => KINT_PHP85, + ]; + + /** + * @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; + } + + foreach (self::DOM_NS_VERSIONS as $key => $val) { + /** + * @psalm-var bool $val + * Psalm bug #4509 + */ + if (false === $val) { + unset($known_properties[$key]); // @codeCoverageIgnore + } + } + } 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..31b037acc0f4 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..a3c2fbb01468 100644 --- a/system/ThirdParty/Kint/Parser/Parser.php +++ b/system/ThirdParty/Kint/Parser/Parser.php @@ -28,17 +28,34 @@ namespace Kint\Parser; 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; +use Throwable; +/** + * @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,73 @@ 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 { + try { + $this->noRecurseCall(); + } catch (DomainException $e) { // @codeCoverageIgnore + \trigger_error('Calling Kint\\Parser::addPlugin from inside a parse is deprecated', E_USER_DEPRECATED); // @codeCoverageIgnore + } + 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,481 +211,382 @@ public function addPlugin(PluginInterface $p): bool } } } - - return true; } 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; + try { + $this->noRecurseCall(); + } catch (DomainException $e) { // @codeCoverageIgnore + \trigger_error('Calling Kint\\Parser::clearPlugins from inside a parse is deprecated', E_USER_DEPRECATED); // @codeCoverageIgnore } - 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; + $this->plugins = []; } protected function noRecurseCall(): void { $bt = \debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS); - $caller_frame = [ - 'function' => __FUNCTION__, - ]; - - while (isset($bt[0]['object']) && $bt[0]['object'] === $this) { - $caller_frame = \array_shift($bt); - } + \reset($bt); + /** @psalm-var class-string $caller_frame['class'] */ + $caller_frame = \next($bt); foreach ($bt as $frame) { - if (isset($frame['object']) && $frame['object'] === $this) { - throw new DomainException(__CLASS__.'::'.$caller_frame['function'].' cannot be called from inside a parse'); + if (isset($frame['object']) && $frame['object'] === $this && 'parse' === $frame['function']) { + throw new DomainException($caller_frame['class'].'::'.$caller_frame['function'].' cannot be called from inside a parse'); } } } /** - * @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; + $v = new FixedWidthValue($c, $var); - $this->applyPlugins($var, $o, self::TRIGGER_SUCCESS); - - 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); - - $rep = new Representation('Contents'); - $rep->contents = $var; - $rep->implicit_label = true; + $string = new StringValue($c, $var, Utils::detectEncoding($var)); - $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; - - if (!$array->size) { - $this->applyPlugins($var, $array, self::TRIGGER_SUCCESS); - - return $array; - } + try { + $this->array_ref_stack[$parentRef] = true; - if ($this->depth_limit && $o->depth >= $this->depth_limit) { - $array->hints[] = 'depth_limit'; + $cdepth = $c->getDepth(); + $ap = $c->getAccessPath(); - $this->applyPlugins($var, $array, self::TRIGGER_DEPTH_LIMIT); + if ($size > 0 && $this->depth_limit && $cdepth >= $this->depth_limit) { + $array = new ArrayValue($c, $size, $contents); + $array->flags |= AbstractValue::FLAG_DEPTH_LIMIT; - return $array; - } + return $this->applyPluginsComplete($var, $array, self::TRIGGER_DEPTH_LIMIT); + } - $copy = \array_values($var); + foreach ($var as $key => $_) { + $child = new ArrayContext($key); + $child->depth = $cdepth + 1; + $child->reference = null !== ReflectionReference::fromArrayElement($var, $key); - // 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; + if (null !== $ap) { + $child->access_path = $ap.'['.\var_export($key, true).']'; + } - // Set the marker for recursion - $var[$this->marker] = $array->depth; + $contents[$key] = $this->parse($var[$key], $child); + } - $refmarker = new stdClass(); + $array = new ArrayValue($c, $size, $contents); - foreach ($var as $key => &$val) { - if ($key === $this->marker) { - continue; + if ($contents) { + $array->addRepresentation(new ContainerRepresentation('Contents', $contents, null, true)); } - $child = new Value(); - $child->name = $key; - $child->depth = $array->depth + 1; - $child->access = Value::ACCESS_NONE; - $child->operator = Value::OPERATOR_ARRAY; + return $this->applyPluginsComplete($var, $array, self::TRIGGER_SUCCESS); + } finally { + unset($this->array_ref_stack[$parentRef]); + } + } - 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).']'; - } - } + /** + * @psalm-return ReflectionProperty[] + */ + private function getPropsOrdered(ReflectionClass $r): array + { + if ($parent = $r->getParentClass()) { + $props = self::getPropsOrdered($parent); + } else { + $props = []; + } - $stash = $val; - try { - $copy[$i] = $refmarker; - } catch (TypeError $e) { - $child->reference = true; - } - if ($val === $refmarker) { - $child->reference = true; - $val = $stash; + foreach ($r->getProperties() as $prop) { + if ($prop->isStatic()) { + continue; } - $rep->contents[] = $this->parse($val, $child); - ++$i; + if ($prop->isPrivate()) { + $props[] = $prop; + } else { + $props[$prop->name] = $prop; + } } - $this->applyPlugins($var, $array, self::TRIGGER_SUCCESS); - unset($var[$this->marker]); - - return $array; + return $props; } /** - * Parses an object into a Kint InstanceValue structure. + * @codeCoverageIgnore * - * @param object &$var The input variable - * @param Value $o The base object + * @psalm-return ReflectionProperty[] */ - private function parseObject(&$var, Value $o): Value + private function getPropsOrderedOld(ReflectionClass $r): array { - $hash = \spl_object_hash($var); - $values = (array) $var; + $props = []; - $object = new InstanceValue(); - $object->transplant($o); - $object->classname = \get_class($var); - $object->spl_object_hash = $hash; - $object->size = \count($values); + foreach ($r->getProperties() as $prop) { + if ($prop->isStatic()) { + continue; + } - if (KINT_PHP72) { - $object->spl_object_id = \spl_object_id($var); + $props[] = $prop; } - if (isset($this->object_hashes[$hash])) { - $object->hints[] = 'recursion'; - - $this->applyPlugins($var, $object, self::TRIGGER_RECURSION); + while ($r = $r->getParentClass()) { + foreach ($r->getProperties(ReflectionProperty::IS_PRIVATE) as $prop) { + if ($prop->isStatic()) { + continue; + } - return $object; + $props[] = $prop; + } } - $this->object_hashes[$hash] = $object; + return $props; + } - if ($this->depth_limit && $o->depth >= $this->depth_limit) { - $object->hints[] = 'depth_limit'; + private function parseObject(object &$var, ContextInterface $c): AbstractValue + { + $hash = \spl_object_hash($var); + $classname = \get_class($var); - $this->applyPlugins($var, $object, self::TRIGGER_DEPTH_LIMIT); - unset($this->object_hashes[$hash]); + if (isset($this->object_hashes[$hash])) { + $object = new InstanceValue($c, $classname, $hash, \spl_object_id($var)); + $object->flags |= AbstractValue::FLAG_RECURSION; - return $object; + return $this->applyPluginsComplete($var, $object, self::TRIGGER_RECURSION); } - $reflector = new ReflectionObject($var); + try { + $this->object_hashes[$hash] = true; - if ($reflector->isUserDefined()) { - $object->filename = $reflector->getFileName(); - $object->startline = $reflector->getStartLine(); - } + $cdepth = $c->getDepth(); + $ap = $c->getAccessPath(); - $rep = new Representation('Properties'); + if ($this->depth_limit && $cdepth >= $this->depth_limit) { + $object = new InstanceValue($c, $classname, $hash, \spl_object_id($var)); + $object->flags |= AbstractValue::FLAG_DEPTH_LIMIT; - $readonly = []; - - // 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(); + return $this->applyPluginsComplete($var, $object, self::TRIGGER_DEPTH_LIMIT); + } - while ($reflector = $reflector->getParentClass()) { - $rprops = \array_merge($rprops, $reflector->getProperties(ReflectionProperty::IS_PRIVATE)); + if (KINT_PHP81) { + $props = $this->getPropsOrdered(new ReflectionObject($var)); + } else { + $props = $this->getPropsOrderedOld(new ReflectionObject($var)); // @codeCoverageIgnore } - foreach ($rprops as $rprop) { - if ($rprop->isStatic()) { - continue; - } + $values = (array) $var; + $properties = []; + foreach ($props as $rprop) { $rprop->setAccessible(true); - - 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 ($rprop->isInitialized($var)) { - continue; - } - - $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; + $name = $rprop->getName(); + + // 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 + $key = $name; + if ($rprop->isProtected()) { + $key = "\0*\0".$name; } elseif ($rprop->isPrivate()) { - $child->access = Value::ACCESS_PRIVATE; + $key = "\0".$rprop->getDeclaringClass()->getName()."\0".$name; } - - // 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; + $initialized = \array_key_exists($key, $values); + if ($key === (string) (int) $key) { + $key = (int) $key; } - if ($this->applyPlugins($undefined, $child, self::TRIGGER_BEGIN)) { - $this->applyPlugins($undefined, $child, self::TRIGGER_SUCCESS); - } - $rep->contents[] = $child; - } - } + if ($rprop->isDefault()) { + $child = new PropertyContext( + $name, + $rprop->getDeclaringClass()->getName(), + ClassDeclaredContext::ACCESS_PUBLIC + ); - $copy = \array_values($values); - $refmarker = new stdClass(); - $i = 0; - - // 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; - } + $child->readonly = KINT_PHP81 && $rprop->isReadOnly(); - $split_key = \explode("\0", (string) $key, 3); + if ($rprop->isProtected()) { + $child->access = ClassDeclaredContext::ACCESS_PROTECTED; + } elseif ($rprop->isPrivate()) { + $child->access = ClassDeclaredContext::ACCESS_PRIVATE; + } - if (3 === \count($split_key) && '' === $split_key[0]) { - $child->name = $split_key[2]; - if ('*' === $split_key[1]) { - $child->access = Value::ACCESS_PROTECTED; + 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; + } + } + } } else { - $child->access = Value::ACCESS_PRIVATE; - $child->owner_class = $split_key[1]; + $child = new ClassOwnedContext($name, $rprop->getDeclaringClass()->getName()); } - } elseif (KINT_PHP72) { - $child->name = (string) $key; - } else { - $child->name = $key; // @codeCoverageIgnore - } - if ($this->childHasPath($object, $child)) { - $child->access_path = $object->access_path; + $child->reference = $initialized && null !== ReflectionReference::fromArrayElement($values, $key); + $child->depth = $cdepth + 1; + + 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 = $ap.'->{'.\var_export($child->name, true).'}'; + } + } - 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 (KINT_PHP84 && $rprop->isVirtual()) { + $properties[] = new VirtualValue($child); + } elseif (!$initialized) { + $properties[] = new UninitializedValue($child); } else { - $child->access_path .= '->{'.\var_export((string) $child->name, true).'}'; + $properties[] = $this->parse($values[$key], $child); } } - $stash = $val; - try { - $copy[$i] = $refmarker; - } catch (TypeError $e) { - $child->reference = true; + $object = new InstanceValue($c, $classname, $hash, \spl_object_id($var)); + if ($props) { + $object->setChildren($properties); } - if ($val === $refmarker) { - $child->reference = true; - $val = $stash; + + if ($properties) { + $object->addRepresentation(new ContainerRepresentation('Properties', $properties)); } - $rep->contents[] = $this->parse($val, $child); - ++$i; + return $this->applyPluginsComplete($var, $object, self::TRIGGER_SUCCESS); + } finally { + unset($this->object_hashes[$hash]); } - - $object->addRepresentation($rep); - $object->value = $rep; - $this->applyPlugins($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); - } catch (Exception $e) { + if ($v = $plugin->parseBegin($var, $c)) { + return $v; + } + } catch (Throwable $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(), + 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 ); } + } - if ($this->parse_break) { - $this->parse_break = $break_stash; + return null; + } + + /** + * 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 (Throwable $e) { + \trigger_error( + 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 @@ +'.$this->renderLockedHeader($o, 'Recursion').''; - } + /** + * @psalm-param mixed &$var + * @psalm-param ParserTrigger $trigger + */ + public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractValue; } diff --git a/system/ThirdParty/Kint/Parser/PluginInterface.php b/system/ThirdParty/Kint/Parser/PluginInterface.php index 3afb1a77674b..64d2dfe22f1e 100644 --- a/system/ThirdParty/Kint/Parser/PluginInterface.php +++ b/system/ThirdParty/Kint/Parser/PluginInterface.php @@ -27,18 +27,17 @@ namespace Kint\Parser; -use Kint\Zval\Value; - +/** + * @psalm-import-type ParserTrigger from Parser + */ interface PluginInterface { public function setParser(Parser $p): void; public function getTypes(): array; - public function getTriggers(): int; - /** - * @psalm-param mixed &$var + * @psalm-return ParserTrigger */ - public function parse(&$var, Value &$o, int $trigger): void; + public function getTriggers(): int; } diff --git a/system/ThirdParty/Kint/Parser/ProfilePlugin.php b/system/ThirdParty/Kint/Parser/ProfilePlugin.php new file mode 100644 index 000000000000..ec5bb0a17dd6 --- /dev/null +++ b/system/ThirdParty/Kint/Parser/ProfilePlugin.php @@ -0,0 +1,174 @@ +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/Renderer/Rich/MethodDefinitionPlugin.php b/system/ThirdParty/Kint/Renderer/AssetRendererTrait.php similarity index 52% rename from system/ThirdParty/Kint/Renderer/Rich/MethodDefinitionPlugin.php rename to system/ThirdParty/Kint/Renderer/AssetRendererTrait.php index f5ed37b624b5..fb66f2db1b7b 100644 --- a/system/ThirdParty/Kint/Renderer/Rich/MethodDefinitionPlugin.php +++ b/system/ThirdParty/Kint/Renderer/AssetRendererTrait.php @@ -25,52 +25,46 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace Kint\Renderer\Rich; +namespace Kint\Renderer; -use Kint\Kint; -use Kint\Zval\Representation\MethodDefinitionRepresentation; -use Kint\Zval\Representation\Representation; - -class MethodDefinitionPlugin extends AbstractPlugin implements TabPluginInterface +trait AssetRendererTrait { - public function renderTab(Representation $r): ?string - { - if (!$r instanceof MethodDefinitionRepresentation) { - return null; - } + public static ?string $theme = null; - if (isset($r->contents)) { - $docstring = []; - foreach (\explode("\n", $r->contents) as $line) { - $docstring[] = \trim($line); - } + /** @psalm-var array{js?:string, css?:array} */ + private static array $assetCache = []; - $docstring = $this->renderer->escape(\implode("\n", $docstring)); + /** @psalm-api */ + public static function renderJs(): string + { + if (!isset(self::$assetCache['js'])) { + self::$assetCache['js'] = \file_get_contents(KINT_DIR.'/resources/compiled/main.js'); } - $addendum = []; - if (isset($r->class) && $r->inherited) { - $addendum[] = 'Inherited from '.$this->renderer->escape($r->class); - } + return self::$assetCache['js']; + } - if (isset($r->file, $r->line)) { - $addendum[] = 'Defined in '.$this->renderer->escape(Kint::shortenPath($r->file)).':'.((int) $r->line); + /** @psalm-api */ + public static function renderCss(): ?string + { + if (!isset(self::$theme)) { + return null; } - if ($addendum) { - $addendum = ''.\implode("\n", $addendum).''; - - if (isset($docstring)) { - $docstring .= "\n\n".$addendum; + 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); + } elseif (\file_exists(self::$theme)) { + self::$assetCache['css'][self::$theme] = \file_get_contents(self::$theme); } else { - $docstring = $addendum; + self::$assetCache['css'][self::$theme] = false; } } - if (!isset($docstring)) { + if (false === self::$assetCache['css'][self::$theme]) { return null; } - return '
'.$docstring.'
'; + 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/BlacklistPlugin.php b/system/ThirdParty/Kint/Renderer/ConstructableRendererInterface.php similarity index 85% rename from system/ThirdParty/Kint/Renderer/Text/BlacklistPlugin.php rename to system/ThirdParty/Kint/Renderer/ConstructableRendererInterface.php index d07891247a72..686f37a645f6 100644 --- a/system/ThirdParty/Kint/Renderer/Text/BlacklistPlugin.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 BlacklistPlugin extends AbstractPlugin +interface ConstructableRendererInterface extends RendererInterface { - public function render(Value $o): string - { - return $this->renderLockedHeader($o, 'BLACKLISTED'); - } + 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 .= '