diff --git a/.github/workflows/test-coding-standards.yml b/.github/workflows/test-coding-standards.yml index 75fbabf2f14a..c49fd416ea8e 100644 --- a/.github/workflows/test-coding-standards.yml +++ b/.github/workflows/test-coding-standards.yml @@ -65,5 +65,3 @@ jobs: - name: Run lint run: composer cs - env: - PHP_CS_FIXER_IGNORE_ENV: ${{ matrix.php-version == '8.4' }} diff --git a/.github/workflows/test-psalm.yml b/.github/workflows/test-psalm.yml index ea948ab3bc65..d24c2916df5e 100644 --- a/.github/workflows/test-psalm.yml +++ b/.github/workflows/test-psalm.yml @@ -66,12 +66,7 @@ jobs: restore-keys: ${{ runner.os }}-psalm- - name: Install dependencies - run: | - if [ -f composer.lock ]; then - composer install --no-progress - else - composer update --no-progress - fi + run: composer update --ansi --no-interaction - name: Run Psalm analysis run: utils/vendor/bin/psalm diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index e9240c590917..f256758deb2b 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -13,8 +13,6 @@ use CodeIgniter\CodingStandard\CodeIgniter4; use Nexus\CsConfig\Factory; -use Nexus\CsConfig\Fixer\Comment\NoCodeSeparatorCommentFixer; -use Nexus\CsConfig\FixerGenerator; use PhpCsFixer\Finder; $finder = Finder::create() @@ -44,12 +42,8 @@ ]; $options = [ - 'cacheFile' => 'build/.php-cs-fixer.cache', - 'finder' => $finder, - 'customFixers' => FixerGenerator::create('utils/vendor/nexusphp/cs-config/src/Fixer', 'Nexus\\CsConfig\\Fixer'), - 'customRules' => [ - NoCodeSeparatorCommentFixer::name() => true, - ], + 'cacheFile' => 'build/.php-cs-fixer.cache', + 'finder' => $finder, ]; return Factory::create(new CodeIgniter4(), $overrides, $options)->forLibrary( diff --git a/.php-cs-fixer.tests.php b/.php-cs-fixer.tests.php index bf37256da58a..412400aebaee 100644 --- a/.php-cs-fixer.tests.php +++ b/.php-cs-fixer.tests.php @@ -31,7 +31,8 @@ ->notName('#Foobar.php$#'); $overrides = [ - 'void_return' => true, + 'phpdoc_to_return_type' => true, + 'void_return' => true, ]; return $config diff --git a/CHANGELOG.md b/CHANGELOG.md index 3556e750e802..aafcb2d746b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,60 @@ # Changelog -## [v4.6.1](https://github.com/codeigniter4/CodeIgniter4/tree/v4.6.0) (2025-05-02) +## [v4.6.2](https://github.com/codeigniter4/CodeIgniter4/tree/v4.6.2) (2025-07-26) +[Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/v4.6.1...v4.6.2) + +### Security + +* **ImageMagickHandler**: *Command Injection Vulnerability in ImageMagick Handler* + Fixes a vulnerability relating to uses of `ImageMagickHandler`'s `resize()` or `text()` methods + where an attacker can upload malicious filenames containing shell metacharacters that get executed when + the image is processed or when text is added to the image. + + See the [security advisory](https://github.com/codeigniter4/CodeIgniter4/security/advisories/GHSA-9952-gv64-x94c) + for details. Credits to @vicevirus for reporting the issue. + +### Fixed Bugs + +* chore: add missing EscaperInterface to the AutoloadConfig by @michalsn in https://github.com/codeigniter4/CodeIgniter4/pull/9561 +* fix: remove service dependency from sanitize_filename() helper function by @michalsn in https://github.com/codeigniter4/CodeIgniter4/pull/9560 +* fix: use native PHP truthiness for condition evaluation in when()/whenNot() by @michalsn in https://github.com/codeigniter4/CodeIgniter4/pull/9576 +* fix: add error handling for corrupted cache files in `FileHandler` by @michalsn in https://github.com/codeigniter4/CodeIgniter4/pull/9586 +* fix: correct `getHostname()` fallback logic in `Email` class by @michalsn in https://github.com/codeigniter4/CodeIgniter4/pull/9587 +* fix: encapsulation violation in `BasePreparedQuery` class by @michalsn in https://github.com/codeigniter4/CodeIgniter4/pull/9603 +* fix: URI authority generation for schemes without default ports by @michalsn in https://github.com/codeigniter4/CodeIgniter4/pull/9605 +* fix: correct path parsing in `SiteURIFactory::parseRequestURI()` by @michalsn in https://github.com/codeigniter4/CodeIgniter4/pull/9613 +* fix: support for multibyte folder names when the app is served from a subfolder by @michalsn in https://github.com/codeigniter4/CodeIgniter4/pull/9615 +* fix: use correct 24-hour time format in development error page. by @ping-yee in https://github.com/codeigniter4/CodeIgniter4/pull/9628 +* fix: improve CURLRequest intermediate HTTP response handling by @michalsn in https://github.com/codeigniter4/CodeIgniter4/pull/9627 +* fix: ensure `make:test` works on Windows by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/9635 +* fix: ensure `make:test` generates test files ending in `Test` by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/9636 +* fix: `make:test` requires 3 inputs after entering an empty class name by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/9637 +* fix: add filename parameters to inline Content-Disposition headers by @michalsn in https://github.com/codeigniter4/CodeIgniter4/pull/9638 + +### Refactoring + +* refactor: add `system/util_bootstrap.php` to curb overreliance to `system/Test/bootstrap.php` by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/9562 +* refactor: update places to use `system/util_bootstrap.php` by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/9568 +* refactor: more accurate array PHPDocs of Cookie by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/9569 +* refactor: use native phpdocs wherever possible by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/9571 +* refactor: fix `notIdentical.alwaysTrue` error by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/9579 +* refactor: fix phpstan errors in `Events` by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/9580 +* refactor: fix non-booleans in if conditions by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/9578 +* refactor: fix and micro-optimize code in `Format` by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/9583 +* refactor: fix various phpstan errors in Log component by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/9581 +* refactor: partial fix errors on Email by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/9582 +* refactor: fix phpstan errors in `ResponseTrait` by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/9591 +* refactor: precise PHPDocs for Autoloader by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/9593 +* refactor: fix phpstan errors in mock classes by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/9594 +* refactor: fix various phpstan errors in Cache by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/9610 +* fix: apply rector rule TernaryImplodeToImplodeRector by @michalsn in https://github.com/codeigniter4/CodeIgniter4/pull/9614 +* refactor: `Console::showHeader()` call `date()` only once by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/9616 + +## [v4.6.1](https://github.com/codeigniter4/CodeIgniter4/tree/v4.6.1) (2025-05-02) [Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/v4.6.0...v4.6.1) ### Fixed Bugs + * fix(CURLRequest): multiple header sections after redirects by @ducng99 in https://github.com/codeigniter4/CodeIgniter4/pull/9426 * fix: set headers for CORS by @michalsn in https://github.com/codeigniter4/CodeIgniter4/pull/9437 * fix: upsert with composite unique index by @michalsn in https://github.com/codeigniter4/CodeIgniter4/pull/9454 @@ -13,6 +64,7 @@ * fix: added "application/octet-stream" to the "stl" mime type in the M… by @Franky5831 in https://github.com/codeigniter4/CodeIgniter4/pull/9543 ### Refactoring + * refactor: get upper first protocol only one call in Email by @ddevsr in https://github.com/codeigniter4/CodeIgniter4/pull/9449 * refactor: PHPDocs in `env()` by @ddevsr in https://github.com/codeigniter4/CodeIgniter4/pull/9468 * refactor: remove lowercase event name for logging by @ddevsr in https://github.com/codeigniter4/CodeIgniter4/pull/9483 diff --git a/admin/framework/.gitignore b/admin/framework/.gitignore index e24e7ce497d1..87e86b93bd42 100644 --- a/admin/framework/.gitignore +++ b/admin/framework/.gitignore @@ -61,7 +61,7 @@ writable/uploads/* !writable/uploads/index.html writable/debugbar/* -!writable/debugbar/.gitkeep +!writable/debugbar/index.html php_errors.log diff --git a/admin/framework/composer.json b/admin/framework/composer.json index 82d06cd9d05c..499a97eae337 100644 --- a/admin/framework/composer.json +++ b/admin/framework/composer.json @@ -13,7 +13,7 @@ "php": "^8.1", "ext-intl": "*", "ext-mbstring": "*", - "laminas/laminas-escaper": "^2.14", + "laminas/laminas-escaper": "^2.17", "psr/log": "^3.0" }, "require-dev": { @@ -24,7 +24,7 @@ "mikey179/vfsstream": "^1.6.12", "nexusphp/cs-config": "^3.6", "phpunit/phpunit": "^10.5.16 || ^11.2", - "predis/predis": "^1.1 || ^2.3" + "predis/predis": "^3.0" }, "suggest": { "ext-curl": "If you use CURLRequest class", diff --git a/admin/starter/.gitignore b/admin/starter/.gitignore index e24e7ce497d1..87e86b93bd42 100644 --- a/admin/starter/.gitignore +++ b/admin/starter/.gitignore @@ -61,7 +61,7 @@ writable/uploads/* !writable/uploads/index.html writable/debugbar/* -!writable/debugbar/.gitkeep +!writable/debugbar/index.html php_errors.log diff --git a/app/Config/Cache.php b/app/Config/Cache.php index e6efa3acf6f5..1169c95ff7ee 100644 --- a/app/Config/Cache.php +++ b/app/Config/Cache.php @@ -78,7 +78,7 @@ class Cache extends BaseConfig * Your file storage preferences can be specified below, if you are using * the File driver. * - * @var array + * @var array{storePath?: string, mode?: int} */ public array $file = [ 'storePath' => WRITEPATH . 'cache/', @@ -95,7 +95,7 @@ class Cache extends BaseConfig * * @see https://codeigniter.com/user_guide/libraries/caching.html#memcached * - * @var array + * @var array{host?: string, port?: int, weight?: int, raw?: bool} */ public array $memcached = [ 'host' => '127.0.0.1', @@ -112,7 +112,7 @@ class Cache extends BaseConfig * Your Redis server can be specified below, if you are using * the Redis or Predis drivers. * - * @var array + * @var array{host?: string, password?: string|null, port?: int, timeout?: int, database?: int} */ public array $redis = [ 'host' => '127.0.0.1', diff --git a/app/Config/Cookie.php b/app/Config/Cookie.php index 84ccc0e99d80..3bad184797ce 100644 --- a/app/Config/Cookie.php +++ b/app/Config/Cookie.php @@ -85,7 +85,7 @@ class Cookie extends BaseConfig * (empty string) means default SameSite attribute set by browsers (`Lax`) * will be set on cookies. If set to `None`, `$secure` must also be set. * - * @phpstan-var 'None'|'Lax'|'Strict'|'' + * @var ''|'Lax'|'None'|'Strict' */ public string $samesite = 'Lax'; diff --git a/app/Config/Logger.php b/app/Config/Logger.php index ab6997e52fb9..799dc2c39080 100644 --- a/app/Config/Logger.php +++ b/app/Config/Logger.php @@ -4,6 +4,7 @@ use CodeIgniter\Config\BaseConfig; use CodeIgniter\Log\Handlers\FileHandler; +use CodeIgniter\Log\Handlers\HandlerInterface; class Logger extends BaseConfig { @@ -73,7 +74,7 @@ class Logger extends BaseConfig * Handlers are executed in the order defined in this array, starting with * the handler on top and continuing down. * - * @var array|string>> + * @var array, array|string>> */ public array $handlers = [ /* diff --git a/app/Views/errors/html/error_exception.php b/app/Views/errors/html/error_exception.php index d5e0c2ec7ee0..2c4e00911365 100644 --- a/app/Views/errors/html/error_exception.php +++ b/app/Views/errors/html/error_exception.php @@ -24,7 +24,7 @@
- Displayed at — + Displayed at — PHP: — CodeIgniter: -- Environment: diff --git a/composer.json b/composer.json index 1f07772400f4..64253d857046 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "php": "^8.1", "ext-intl": "*", "ext-mbstring": "*", - "laminas/laminas-escaper": "^2.14", + "laminas/laminas-escaper": "^2.17", "psr/log": "^3.0" }, "require-dev": { @@ -27,8 +27,8 @@ "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.3", - "rector/rector": "2.0.14", + "predis/predis": "^3.0", + "rector/rector": "2.1.2", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { @@ -89,7 +89,7 @@ "CodeIgniter\\ComposerScripts::postUpdate" ], "post-autoload-dump": [ - "@composer update --working-dir=utils --ignore-platform-req=php" + "@composer update --ansi --working-dir=utils" ], "analyze": [ "Composer\\Config::disableProcessTimeout", diff --git a/phpdoc.dist.xml b/phpdoc.dist.xml index 07612e40acf8..b2df39d10bca 100644 --- a/phpdoc.dist.xml +++ b/phpdoc.dist.xml @@ -10,7 +10,7 @@ api/build/ api/cache/ - + system diff --git a/phpstan-bootstrap.php b/phpstan-bootstrap.php index 99acf96227fe..0f45bf0cb559 100644 --- a/phpstan-bootstrap.php +++ b/phpstan-bootstrap.php @@ -1,7 +1,18 @@ isFile() && $file->getExtension() === 'php') { + require_once $file->getRealPath(); + } + } } diff --git a/psalm-autoload.php b/psalm-autoload.php new file mode 100644 index 000000000000..852ef4d6aab3 --- /dev/null +++ b/psalm-autoload.php @@ -0,0 +1,50 @@ +isFile()) { + continue; + } + + if (in_array($file->getPath(), $excludeDirs, true)) { + continue; + } + + if ($file->getExtension() !== 'php') { + continue; + } + + if (in_array($file->getPathname(), $excludeFiles, true)) { + continue; + } + + require_once $file->getPathname(); + } +} diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 53d1b6ce876a..778bdd654739 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + |parser_callable_string|parser_callable>]]> @@ -10,28 +10,6 @@ - - - - - - - - memcached]]> - memcached]]> - memcached]]> - memcached]]> - memcached]]> - memcached]]> - - - - - - - - - |parser_callable_string|parser_callable>]]> @@ -39,11 +17,6 @@ ]]> - - - db->transStatus]]> - - @@ -53,6 +26,17 @@ + + + + + + + + + + + @@ -82,7 +66,6 @@ - @@ -112,11 +95,6 @@ - - - - - @@ -140,12 +118,64 @@ + + attributes]]> + casts]]> + casts]]> + casts]]> + casts]]> + + + + country]]> + created_at]]> + deleted]]> + email]]> + name]]> + country]]> + created_at]]> + deleted]]> + email]]> + name]]> + + + + + country]]> + created_at]]> + created_at]]> + deleted]]> + email]]> + name]]> + name]]> + name]]> + name]]> + + + + + country]]> + deleted]]> + email]]> + id]]> + name]]> + country]]> + country]]> + deleted]]> + id]]> + name]]> + country]]> + deleted]]> + id]]> + name]]> + + diff --git a/psalm.xml b/psalm.xml index 45242620028f..d6033313a621 100644 --- a/psalm.xml +++ b/psalm.xml @@ -5,9 +5,10 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="https://getpsalm.org/schema/config" xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd" - autoloader="psalm_autoload.php" + autoloader="psalm-autoload.php" cacheDirectory="build/psalm/" errorBaseline="psalm-baseline.xml" + ensureOverrideAttribute="false" findUnusedBaselineEntry="false" findUnusedCode="false" > diff --git a/psalm_autoload.php b/psalm_autoload.php deleted file mode 100644 index b973cc88bc7d..000000000000 --- a/psalm_autoload.php +++ /dev/null @@ -1,47 +0,0 @@ -withBootstrapFiles([ - __DIR__ . '/system/Test/bootstrap.php', + __DIR__ . '/phpstan-bootstrap.php', ]) ->withPHPStanConfigs([ __DIR__ . '/phpstan.neon.dist', diff --git a/system/API/ResponseTrait.php b/system/API/ResponseTrait.php index 5f2e068b3918..318af4b28bea 100644 --- a/system/API/ResponseTrait.php +++ b/system/API/ResponseTrait.php @@ -13,8 +13,10 @@ namespace CodeIgniter\API; +use CodeIgniter\Format\Format; use CodeIgniter\Format\FormatterInterface; use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; /** @@ -22,8 +24,10 @@ * consistent HTTP responses under a variety of common * situations when working as an API. * - * @property bool $stringAsHtml Whether to treat string data as HTML in JSON response. - * Setting `true` is only for backward compatibility. + * @property RequestInterface $request + * @property ResponseInterface $response + * @property bool $stringAsHtml Whether to treat string data as HTML in JSON response. + * Setting `true` is only for backward compatibility. */ trait ResponseTrait { @@ -69,8 +73,7 @@ trait ResponseTrait * Either 'json' or 'xml'. If null is set, it will be determined through * content negotiation. * - * @var string|null - * @phpstan-var 'html'|'json'|'xml'|null + * @var 'html'|'json'|'xml'|null */ protected $format = 'json'; @@ -85,7 +88,7 @@ trait ResponseTrait * Provides a single, simple method to return an API response, formatted * to match the requested format, with proper content-type and status code. * - * @param array|string|null $data + * @param array|string|null $data * * @return ResponseInterface */ @@ -119,9 +122,9 @@ protected function respond($data = null, ?int $status = null, string $message = /** * Used for generic failures that no custom methods exist for. * - * @param array|string $messages - * @param int $status HTTP status code - * @param string|null $code Custom, API-specific, error code + * @param list|string $messages + * @param int $status HTTP status code + * @param string|null $code Custom, API-specific, error code * * @return ResponseInterface */ @@ -147,7 +150,7 @@ protected function fail($messages, int $status = 400, ?string $code = null, stri /** * Used after successfully creating a new resource. * - * @param array|string|null $data + * @param array|string|null $data * * @return ResponseInterface */ @@ -159,7 +162,7 @@ protected function respondCreated($data = null, string $message = '') /** * Used after a resource has been successfully deleted. * - * @param array|string|null $data + * @param array|string|null $data * * @return ResponseInterface */ @@ -171,7 +174,7 @@ protected function respondDeleted($data = null, string $message = '') /** * Used after a resource has been successfully updated. * - * @param array|string|null $data + * @param array|string|null $data * * @return ResponseInterface */ @@ -288,15 +291,17 @@ protected function failServerError(string $description = 'Internal Server Error' * Handles formatting a response. Currently, makes some heavy assumptions * and needs updating! :) * - * @param array|string|null $data + * @param array|string|null $data * * @return string|null */ protected function format($data = null) { + /** @var Format $format */ $format = service('format'); - $mime = ($this->format === null) ? $format->getConfig()->supportedResponseFormats[0] + $mime = $this->format === null + ? $format->getConfig()->supportedResponseFormats[0] : "application/{$this->format}"; // Determine correct response type through content negotiation if not explicitly declared @@ -314,14 +319,10 @@ protected function format($data = null) $this->response->setContentType($mime); // if we don't have a formatter, make one - if (! isset($this->formatter)) { - // if no formatter, use the default - $this->formatter = $format->getFormatter($mime); - } + $this->formatter ??= $format->getFormatter($mime); $asHtml = $this->stringAsHtml ?? false; - // Returns as HTML. if ( ($mime === 'application/json' && $asHtml && is_string($data)) || ($mime !== 'application/json' && is_string($data)) @@ -339,6 +340,7 @@ protected function format($data = null) if ($mime !== 'application/json') { // Recursively convert objects into associative arrays // Conversion not required for JSONFormatter + /** @var array|string|null $data */ $data = json_decode(json_encode($data), true); } @@ -348,14 +350,13 @@ protected function format($data = null) /** * Sets the format the response should be in. * - * @param string|null $format Response format - * @phpstan-param 'json'|'xml' $format + * @param 'json'|'xml' $format Response format * * @return $this */ protected function setResponseFormat(?string $format = null) { - $this->format = ($format === null) ? null : strtolower($format); + $this->format = $format === null ? null : strtolower($format); return $this; } diff --git a/system/Autoloader/Autoloader.php b/system/Autoloader/Autoloader.php index 2e628f1590d6..65c535e8611e 100644 --- a/system/Autoloader/Autoloader.php +++ b/system/Autoloader/Autoloader.php @@ -21,7 +21,7 @@ use Config\Autoload; use Config\Kint as KintConfig; use Config\Modules; -use Kint; +use Kint\Kint; use Kint\Renderer\CliRenderer; use Kint\Renderer\RichRenderer; @@ -67,21 +67,21 @@ class Autoloader /** * Stores namespaces as key, and path as values. * - * @var array> + * @var array> */ protected $prefixes = []; /** * Stores class name as key, and path as values. * - * @var array + * @var array */ protected $classmap = []; /** * Stores files as a list. * - * @var list + * @var list */ protected $files = []; @@ -89,7 +89,7 @@ class Autoloader * Stores helper list. * Always load the URL helper, it should be used in most apps. * - * @var list + * @var list */ protected $helpers = ['url']; @@ -147,36 +147,35 @@ private function loadComposerAutoloader(Modules $modules): void // Should we load through Composer's namespaces, also? if ($modules->discoverInComposer) { - // @phpstan-ignore-next-line - $this->loadComposerNamespaces($composer, $modules->composerPackages ?? []); + $composerPackages = $modules->composerPackages; + $this->loadComposerNamespaces($composer, $composerPackages ?? []); } unset($composer); } /** - * Register the loader with the SPL autoloader stack. + * Register the loader with the SPL autoloader stack + * in the following order: + * + * 1. Classmap loader + * 2. PSR-4 autoloader + * 3. Non-class files * * @return void */ public function register() { - // Register classmap loader for the files in our class map. spl_autoload_register($this->loadClassmap(...), true); - - // Register the PSR-4 autoloader. spl_autoload_register($this->loadClass(...), true); - // Load our non-class files foreach ($this->files as $file) { $this->includeFile($file); } } /** - * Unregister autoloader. - * - * This method is for testing. + * Unregisters the autoloader from the SPL autoload stack. */ public function unregister(): void { @@ -187,7 +186,7 @@ public function unregister(): void /** * Registers namespaces with the autoloader. * - * @param array|string>|string $namespace + * @param array|non-empty-string>|non-empty-string $namespace * * @return $this */ @@ -219,8 +218,7 @@ public function addNamespace($namespace, ?string $path = null) * * If a prefix param is set, returns only paths to the given prefix. * - * @return array>|list - * @phpstan-return ($prefix is null ? array> : list) + * @return ($prefix is null ? array> : list) */ public function getNamespace(?string $prefix = null) { @@ -248,6 +246,8 @@ public function removeNamespace(string $namespace) /** * Load a class using available class mapping. * + * @param class-string $class The fully qualified class name. + * * @internal For `spl_autoload_register` use. */ public function loadClassmap(string $class): void @@ -262,9 +262,9 @@ public function loadClassmap(string $class): void /** * Loads the class file for a given class name. * - * @internal For `spl_autoload_register` use. + * @param class-string $class The fully qualified class name. * - * @param string $class The fully qualified class name. + * @internal For `spl_autoload_register` use. */ public function loadClass(string $class): void { @@ -274,9 +274,9 @@ public function loadClass(string $class): void /** * Loads the class file for a given class name. * - * @param string $class The fully-qualified class name + * @param class-string $class The fully qualified class name. * - * @return false|string The mapped file name on success, or boolean false on fail + * @return false|non-empty-string The mapped file name on success, or boolean false on fail */ protected function loadInNamespace(string $class) { @@ -294,21 +294,20 @@ protected function loadInNamespace(string $class) $filePath = $directory . $relativeClassPath . '.php'; $filename = $this->includeFile($filePath); - if ($filename) { + if ($filename !== false) { return $filename; } } } } - // never found a mapped file return false; } /** * A central way to include a file. Split out primarily for testing purposes. * - * @return false|string The filename on success, false if the file is not loaded + * @return false|non-empty-string The filename on success, false if the file is not loaded */ protected function includeFile(string $file) { diff --git a/system/Autoloader/FileLocator.php b/system/Autoloader/FileLocator.php index 13ecb817a56c..f67708a4e836 100644 --- a/system/Autoloader/FileLocator.php +++ b/system/Autoloader/FileLocator.php @@ -44,7 +44,7 @@ public function __construct(Autoloader $autoloader) * Attempts to locate a file by examining the name for a namespace * and looking through the PSR-4 namespaced files that we know about. * - * @param string $file The relative file path or namespaced file to + * @param non-empty-string $file The relative file path or namespaced file to * locate. If not namespaced, search in the app * folder. * @param non-empty-string|null $folder The folder within the namespace that we should @@ -53,7 +53,7 @@ public function __construct(Autoloader $autoloader) * folder. * @param string $ext The file extension the file should have. * - * @return false|string The path to the file, or false if not found. + * @return false|non-empty-string The path to the file, or false if not found. */ public function locateFile(string $file, ?string $folder = null, string $ext = 'php') { @@ -156,9 +156,11 @@ public function getClassname(string $file): string $dlm = false; } - if (($tokens[$i - 2][0] === T_CLASS || (isset($tokens[$i - 2][1]) && $tokens[$i - 2][1] === 'phpclass')) + if ( + ($tokens[$i - 2][0] === T_CLASS || (isset($tokens[$i - 2][1]) && $tokens[$i - 2][1] === 'phpclass')) && $tokens[$i - 1][0] === T_WHITESPACE - && $token[0] === T_STRING) { + && $token[0] === T_STRING + ) { $className = $token[1]; break; } @@ -184,7 +186,7 @@ public function getClassname(string $file): string * 'app/Modules/bar/Config/Routes.php', * ] * - * @return list + * @return list */ public function search(string $path, string $ext = 'php', bool $prioritizeApp = true): array { @@ -213,7 +215,6 @@ public function search(string $path, string $ext = 'php', bool $prioritizeApp = $foundPaths = [...$foundPaths, ...$appPaths]; } - // Remove any duplicates return array_values(array_unique($foundPaths)); } @@ -236,13 +237,12 @@ protected function ensureExt(string $path, string $ext): string /** * Return the namespace mappings we know about. * - * @return array> + * @return list */ protected function getNamespaces() { $namespaces = []; - // Save system for last $system = []; foreach ($this->autoloader->getNamespace() as $prefix => $paths) { @@ -263,6 +263,7 @@ protected function getNamespaces() } } + // Save system for last return array_merge($namespaces, $system); } @@ -295,19 +296,17 @@ public function findQualifiedNameFromPath(string $path) ); // Remove the file extension (.php) - /** @var class-string */ + /** @var class-string $className */ $className = mb_substr($className, 0, -4); if (in_array($className, $this->invalidClassnames, true)) { continue; } - // Check if this exists if (class_exists($className)) { return $className; } - // If the class does not exist, it is an invalid classname. $this->invalidClassnames[] = $className; } } @@ -353,11 +352,11 @@ public function listFiles(string $path): array * Scans the provided namespace, returning a list of all files * that are contained within the sub path specified by $path. * - * @return list List of file paths + * @return list List of file paths */ public function listNamespaceFiles(string $prefix, string $path): array { - if ($path === '' || ($prefix === '')) { + if ($path === '' || $prefix === '') { return []; } diff --git a/system/Autoloader/FileLocatorCached.php b/system/Autoloader/FileLocatorCached.php index 8b17399444c6..0aa267b2a84b 100644 --- a/system/Autoloader/FileLocatorCached.php +++ b/system/Autoloader/FileLocatorCached.php @@ -117,7 +117,7 @@ public function getClassname(string $file): string } /** - * @return list + * @return list */ public function search(string $path, string $ext = 'php', bool $prioritizeApp = true): array { diff --git a/system/Autoloader/FileLocatorInterface.php b/system/Autoloader/FileLocatorInterface.php index 8f6129abee17..0422ec2519db 100644 --- a/system/Autoloader/FileLocatorInterface.php +++ b/system/Autoloader/FileLocatorInterface.php @@ -23,7 +23,7 @@ interface FileLocatorInterface * Attempts to locate a file by examining the name for a namespace * and looking through the PSR-4 namespaced files that we know about. * - * @param string $file The relative file path or namespaced file to + * @param non-empty-string $file The relative file path or namespaced file to * locate. If not namespaced, search in the app * folder. * @param non-empty-string|null $folder The folder within the namespace that we should @@ -32,12 +32,14 @@ interface FileLocatorInterface * folder. * @param string $ext The file extension the file should have. * - * @return false|string The path to the file, or false if not found. + * @return false|non-empty-string The path to the file, or false if not found. */ public function locateFile(string $file, ?string $folder = null, string $ext = 'php'); /** * Examines a file and returns the fully qualified class name. + * + * @param non-empty-string $file */ public function getClassname(string $file): string; @@ -54,7 +56,7 @@ public function getClassname(string $file): string; * 'app/Modules/bar/Config/Routes.php', * ] * - * @return list + * @return list */ public function search(string $path, string $ext = 'php', bool $prioritizeApp = true): array; diff --git a/system/BaseModel.php b/system/BaseModel.php index cf8df09998f3..4f1cf3dca18f 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -532,9 +532,8 @@ abstract protected function doOnlyDeleted(); * Compiles a replace and runs the query. * This method works only with dbCalls. * - * @param array|null $row Row data - * @phpstan-param row_array|null $row - * @param bool $returnSQL Set to true to return Query String + * @param row_array|null $row Row data + * @param bool $returnSQL Set to true to return Query String * * @return BaseResult|false|Query|string */ @@ -552,8 +551,7 @@ abstract protected function doErrors(); * Public getter to return the id value using the idValue() method. * For example with SQL this will return $data->$this->primaryKey. * - * @param array|object $row Row data - * @phpstan-param row_array|object $row + * @param object|row_array $row Row data * * @return array|int|string|null */ @@ -737,8 +735,7 @@ public function first() * you must ensure that the class will provide access to the class * variables, even if through a magic method. * - * @param array|object $row Row data - * @phpstan-param row_array|object $row + * @param object|row_array $row Row data * * @throws ReflectionException */ @@ -789,12 +786,10 @@ public function getInsertID() * Inserts data into the database. If an object is provided, * it will attempt to convert it to an array. * - * @param array|object|null $row Row data - * @phpstan-param row_array|object|null $row - * @param bool $returnID Whether insert ID should be returned or not. + * @param object|row_array|null $row Row data + * @param bool $returnID Whether insert ID should be returned or not. * - * @return bool|int|string insert ID or true on success. false on failure. - * @phpstan-return ($returnID is true ? int|string|false : bool) + * @return ($returnID is true ? false|int|string : bool) * * @throws ReflectionException */ @@ -867,8 +862,8 @@ public function insert($row = null, bool $returnID = true) /** * Set datetime to created field. * - * @phpstan-param row_array $row - * @param int|string $date timestamp or datetime string + * @param row_array $row + * @param int|string $date timestamp or datetime string */ protected function setCreatedField(array $row, $date): array { @@ -882,8 +877,8 @@ protected function setCreatedField(array $row, $date): array /** * Set datetime to updated field. * - * @phpstan-param row_array $row - * @param int|string $date timestamp or datetime string + * @param row_array $row + * @param int|string $date timestamp or datetime string */ protected function setUpdatedField(array $row, $date): array { @@ -897,11 +892,10 @@ protected function setUpdatedField(array $row, $date): array /** * Compiles batch insert runs the queries, validating each row prior. * - * @param list|null $set an associative array of insert values - * @phpstan-param list|null $set - * @param bool|null $escape Whether to escape values - * @param int $batchSize The size of the batch to run - * @param bool $testing True means only number of records is returned, false will execute the query + * @param list|null $set an associative array of insert values + * @param bool|null $escape Whether to escape values + * @param int $batchSize The size of the batch to run + * @param bool $testing True means only number of records is returned, false will execute the query * * @return bool|int Number of rows inserted or FALSE on failure * @@ -1043,11 +1037,10 @@ public function update($id = null, $row = null): bool /** * Compiles an update and runs the query. * - * @param list|null $set an associative array of insert values - * @phpstan-param list|null $set - * @param string|null $index The where key - * @param int $batchSize The size of the batch to run - * @param bool $returnSQL True means SQL is returned, false will execute the query + * @param list|null $set an associative array of insert values + * @param string|null $index The where key + * @param int $batchSize The size of the batch to run + * @param bool $returnSQL True means SQL is returned, false will execute the query * * @return false|int|list Number of rows affected or FALSE on failure, SQL array when testMode * @@ -1094,9 +1087,7 @@ public function updateBatch(?array $set = null, ?string $index = null, int $batc $row = $this->doProtectFields($row); // Restore updateIndex value in case it was wiped out - if ($updateIndex !== null) { - $row[$index] = $updateIndex; - } + $row[$index] = $updateIndex; $row = $this->setUpdatedField($row, $this->setDate()); } @@ -1128,8 +1119,8 @@ public function updateBatch(?array $set = null, ?string $index = null, int $batc /** * Deletes a single record from the database where $id matches. * - * @param array|int|string|null $id The rows primary key(s) - * @param bool $purge Allows overriding the soft deletes setting. + * @param int|list|string|null $id The rows primary key(s) + * @param bool $purge Allows overriding the soft deletes setting. * * @return BaseResult|bool * @@ -1217,9 +1208,8 @@ public function onlyDeleted() /** * Compiles a replace and runs the query. * - * @param array|null $row Row data - * @phpstan-param row_array|null $row - * @param bool $returnSQL Set to true to return Query String + * @param row_array|null $row Row data + * @param bool $returnSQL Set to true to return Query String * * @return BaseResult|false|Query|string */ @@ -1813,9 +1803,8 @@ protected function objectToRawArray($object, bool $onlyChanged = true, bool $rec /** * Transform data to array. * - * @param array|object|null $row Row data - * @phpstan-param row_array|object|null $row - * @param string $type Type of data (insert|update) + * @param object|row_array|null $row Row data + * @param string $type Type of data (insert|update) * * @throws DataException * @throws InvalidArgumentException diff --git a/system/Boot.php b/system/Boot.php index 502692249b06..ba3675516b16 100644 --- a/system/Boot.php +++ b/system/Boot.php @@ -16,6 +16,7 @@ use CodeIgniter\Cache\FactoriesCache; use CodeIgniter\CLI\Console; use CodeIgniter\Config\DotEnv; +use Config\App; use Config\Autoload; use Config\Modules; use Config\Optimize; @@ -75,6 +76,34 @@ public static function bootWeb(Paths $paths): int return EXIT_SUCCESS; } + /** + * Used by command line scripts other than + * * `spark` + * * `php-cli` + * * `phpunit` + * + * @used-by `system/util_bootstrap.php` + */ + public static function bootConsole(Paths $paths): void + { + static::definePathConstants($paths); + static::loadConstants(); + static::checkMissingExtensions(); + + static::loadDotEnv($paths); + static::loadEnvironmentBootstrap($paths); + + static::loadCommonFunctions(); + static::loadAutoloader(); + static::setExceptionHandler(); + static::initializeKint(); + static::autoloadHelpers(); + + // We need to force the request to be a CLIRequest since we're in console + Services::createRequest(new App(), true); + service('routes')->loadRoutes(); + } + /** * Used by `spark` * @@ -344,9 +373,8 @@ protected static function initializeConsole(): Console $console = new Console(); // Show basic information before we do anything else. - // @phpstan-ignore-next-line if (is_int($suppress = array_search('--no-header', $_SERVER['argv'], true))) { - unset($_SERVER['argv'][$suppress]); // @phpstan-ignore-line + unset($_SERVER['argv'][$suppress]); $suppress = true; } diff --git a/system/CLI/Console.php b/system/CLI/Console.php index 325eaa7b455c..89415e265134 100644 --- a/system/CLI/Console.php +++ b/system/CLI/Console.php @@ -59,10 +59,9 @@ public function showHeader(bool $suppress = false) } CLI::write(sprintf( - 'CodeIgniter v%s Command Line Tool - Server Time: %s UTC%s', + 'CodeIgniter v%s Command Line Tool - Server Time: %s', CodeIgniter::CI_VERSION, - date('Y-m-d H:i:s'), - date('P'), + date('Y-m-d H:i:s \\U\\T\\CP'), ), 'green'); CLI::newLine(); } diff --git a/system/CLI/GeneratorTrait.php b/system/CLI/GeneratorTrait.php index 1024fa695259..62f5ceec09ae 100644 --- a/system/CLI/GeneratorTrait.php +++ b/system/CLI/GeneratorTrait.php @@ -282,22 +282,21 @@ protected function qualifyClassName(): string return $namespace . $directoryString . str_replace('/', '\\', $class); } - /** - * Normalize input classname. - */ private function normalizeInputClassName(): string { // Gets the class name from input. $class = $this->params[0] ?? CLI::getSegment(2); if ($class === null && $this->hasClassName) { - // @codeCoverageIgnoreStart - $nameLang = $this->classNameLang !== '' + $nameField = $this->classNameLang !== '' ? $this->classNameLang : 'CLI.generator.className.default'; - $class = CLI::prompt(lang($nameLang), null, 'required'); + $class = CLI::prompt(lang($nameField), null, 'required'); + + // Reassign the class name to the params array in case + // the class name is requested again + $this->params[0] = $class; CLI::newLine(); - // @codeCoverageIgnoreEnd } helper('inflector'); diff --git a/system/Cache/CacheFactory.php b/system/Cache/CacheFactory.php index fb60d73c5cd7..e84254142125 100644 --- a/system/Cache/CacheFactory.php +++ b/system/Cache/CacheFactory.php @@ -75,7 +75,7 @@ public static function getHandler(Cache $config, ?string $handler = null, ?strin } } - // If $adapter->initialization throws a CriticalError exception, we will attempt to + // If $adapter->initialize() throws a CriticalError exception, we will attempt to // use the $backup handler, if that also fails, we resort to the dummy handler. try { $adapter->initialize(); diff --git a/system/Cache/CacheInterface.php b/system/Cache/CacheInterface.php index d76d5aa15ba3..0f94cd56f930 100644 --- a/system/Cache/CacheInterface.php +++ b/system/Cache/CacheInterface.php @@ -13,9 +13,6 @@ namespace CodeIgniter\Cache; -/** - * Cache interface - */ interface CacheInterface { /** @@ -30,16 +27,16 @@ public function initialize(); * * @param string $key Cache item name * - * @return array|bool|float|int|object|string|null + * @return mixed */ public function get(string $key); /** * Saves an item to the cache store. * - * @param string $key Cache item name - * @param array|bool|float|int|object|string|null $value The data to save - * @param int $ttl Time To Live, in seconds (default 60) + * @param string $key Cache item name + * @param mixed $value The data to save + * @param int $ttl Time To Live, in seconds (default 60) * * @return bool Success or failure */ @@ -87,7 +84,7 @@ public function clean(); * The information returned and the structure of the data * varies depending on the handler. * - * @return array|false|object|null + * @return array|false|object|null */ public function getCacheInfo(); @@ -96,10 +93,9 @@ public function getCacheInfo(); * * @param string $key Cache item name. * - * @return array|false|null - * Returns null if the item does not exist, otherwise array - * with at least the 'expire' key for absolute epoch expiry (or null). - * Some handlers may return false when an item does not exist, which is deprecated. + * @return array|false|null Returns null if the item does not exist, otherwise array + * with at least the 'expire' key for absolute epoch expiry (or null). + * Some handlers may return false when an item does not exist, which is deprecated. */ public function getMetaData(string $key); diff --git a/system/Cache/Exceptions/CacheException.php b/system/Cache/Exceptions/CacheException.php index 650dad64d649..c4871a00e8b4 100644 --- a/system/Cache/Exceptions/CacheException.php +++ b/system/Cache/Exceptions/CacheException.php @@ -16,9 +16,6 @@ use CodeIgniter\Exceptions\DebugTraceableTrait; use CodeIgniter\Exceptions\RuntimeException; -/** - * CacheException - */ class CacheException extends RuntimeException { use DebugTraceableTrait; @@ -26,7 +23,7 @@ class CacheException extends RuntimeException /** * Thrown when handler has no permission to write cache. * - * @return CacheException + * @return static */ public static function forUnableToWrite(string $path) { @@ -36,7 +33,7 @@ public static function forUnableToWrite(string $path) /** * Thrown when an unrecognized handler is used. * - * @return CacheException + * @return static */ public static function forInvalidHandlers() { @@ -46,7 +43,7 @@ public static function forInvalidHandlers() /** * Thrown when no backup handler is setup in config. * - * @return CacheException + * @return static */ public static function forNoBackup() { @@ -56,7 +53,7 @@ public static function forNoBackup() /** * Thrown when specified handler was not found. * - * @return CacheException + * @return static */ public static function forHandlerNotFound() { diff --git a/system/Cache/FactoriesCache.php b/system/Cache/FactoriesCache.php index e4b7488f43df..6e49996c5d45 100644 --- a/system/Cache/FactoriesCache.php +++ b/system/Cache/FactoriesCache.php @@ -18,15 +18,9 @@ final class FactoriesCache { - /** - * @var CacheInterface|FileVarExportHandler - */ - private $cache; - - /** - * @param CacheInterface|FileVarExportHandler|null $cache - */ - public function __construct($cache = null) + private readonly CacheInterface|FileVarExportHandler $cache; + + public function __construct(CacheInterface|FileVarExportHandler|null $cache = null) { $this->cache = $cache ?? new FileVarExportHandler(); } @@ -51,7 +45,9 @@ public function load(string $component): bool { $key = $this->getCacheKey($component); - if (! $data = $this->cache->get($key)) { + $data = $this->cache->get($key); + + if (! is_array($data) || $data === []) { return false; } diff --git a/system/Cache/FactoriesCache/FileVarExportHandler.php b/system/Cache/FactoriesCache/FileVarExportHandler.php index b7c1a043c86e..092cd67ebd80 100644 --- a/system/Cache/FactoriesCache/FileVarExportHandler.php +++ b/system/Cache/FactoriesCache/FileVarExportHandler.php @@ -17,10 +17,7 @@ final class FileVarExportHandler { private string $path = WRITEPATH . 'cache'; - /** - * @param array|bool|float|int|object|string|null $val - */ - public function save(string $key, $val): void + public function save(string $key, mixed $val): void { $val = var_export($val, true); @@ -36,10 +33,7 @@ public function delete(string $key): void @unlink($this->path . "/{$key}"); } - /** - * @return array|bool|float|int|object|string|null - */ - public function get(string $key) + public function get(string $key): mixed { return @include $this->path . "/{$key}"; } diff --git a/system/Cache/Handlers/BaseHandler.php b/system/Cache/Handlers/BaseHandler.php index ebe0ca2f43ba..db482952022a 100644 --- a/system/Cache/Handlers/BaseHandler.php +++ b/system/Cache/Handlers/BaseHandler.php @@ -53,7 +53,7 @@ abstract class BaseHandler implements CacheInterface * Keys that exceed MAX_KEY_LENGTH are hashed. * From https://github.com/symfony/cache/blob/7b024c6726af21fd4984ac8d1eae2b9f3d90de88/CacheItem.php#L158 * - * @param string $key The key to validate + * @param mixed $key The key to validate * @param string $prefix Optional prefix to include in length calculations * * @throws InvalidArgumentException When $key is not valid @@ -67,7 +67,8 @@ public static function validateKey($key, $prefix = ''): string throw new InvalidArgumentException('Cache key cannot be empty.'); } - $reserved = config(Cache::class)->reservedCharacters ?? self::RESERVED_CHARACTERS; + $reserved = config(Cache::class)->reservedCharacters; + if ($reserved !== '' && strpbrk($key, $reserved) !== false) { throw new InvalidArgumentException('Cache key contains reserved characters ' . $reserved); } @@ -83,7 +84,7 @@ public static function validateKey($key, $prefix = ''): string * @param int $ttl Time to live * @param Closure(): mixed $callback Callback return value * - * @return array|bool|float|int|object|string|null + * @return mixed */ public function remember(string $key, int $ttl, Closure $callback) { @@ -103,7 +104,7 @@ public function remember(string $key, int $ttl, Closure $callback) * * @param string $pattern Cache items glob-style pattern * - * @return int|never + * @return int * * @throws Exception */ diff --git a/system/Cache/Handlers/FileHandler.php b/system/Cache/Handlers/FileHandler.php index 4fd03e7921b0..010fc86d9cdd 100644 --- a/system/Cache/Handlers/FileHandler.php +++ b/system/Cache/Handlers/FileHandler.php @@ -54,14 +54,19 @@ class FileHandler extends BaseHandler */ public function __construct(Cache $config) { - $this->path = ! empty($config->file['storePath']) ? $config->file['storePath'] : WRITEPATH . 'cache'; - $this->path = rtrim($this->path, '/') . '/'; + $options = [ + ...['storePath' => WRITEPATH . 'cache', 'mode' => 0640], + ...$config->file, + ]; + + $this->path = $options['storePath'] !== '' ? $options['storePath'] : WRITEPATH . 'cache'; + $this->path = rtrim($this->path, '\\/') . '/'; if (! is_really_writable($this->path)) { throw CacheException::forUnableToWrite($this->path); } - $this->mode = $config->file['mode'] ?? 0640; + $this->mode = $options['mode']; $this->prefix = $config->prefix; helper('filesystem'); @@ -217,7 +222,7 @@ public function isSupported(): bool /** * Does the heavy lifting of actually retrieving the file and - * verifying it's age. + * verifying its age. * * @return array{data: mixed, ttl: int, time: int}|false */ @@ -227,7 +232,17 @@ protected function getItem(string $filename) return false; } - $data = @unserialize(file_get_contents($this->path . $filename)); + $content = @file_get_contents($this->path . $filename); + + if ($content === false) { + return false; + } + + try { + $data = unserialize($content); + } catch (Throwable) { + return false; + } if (! is_array($data)) { return false; @@ -332,33 +347,46 @@ protected function deleteFiles(string $path, bool $delDir = false, bool $htdocs * @param bool $topLevelOnly Look only at the top level directory specified? * @param bool $_recursion Internal variable to determine recursion status - do not use in calls * - * @return array|false + * @return array|false */ protected function getDirFileInfo(string $sourceDir, bool $topLevelOnly = true, bool $_recursion = false) { - static $_filedata = []; - $relativePath = $sourceDir; + static $filedata = []; + + $relativePath = $sourceDir; + $filePointer = @opendir($sourceDir); - if ($fp = @opendir($sourceDir)) { + if (! is_bool($filePointer)) { // reset the array and make sure $sourceDir has a trailing slash on the initial call if ($_recursion === false) { - $_filedata = []; - $sourceDir = rtrim(realpath($sourceDir) ?: $sourceDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + $filedata = []; + + $resolvedSrc = realpath($sourceDir); + $resolvedSrc = $resolvedSrc === false ? $sourceDir : $resolvedSrc; + + $sourceDir = rtrim($resolvedSrc, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; } // Used to be foreach (scandir($sourceDir, 1) as $file), but scandir() is simply not as fast - while (false !== ($file = readdir($fp))) { + while (false !== $file = readdir($filePointer)) { if (is_dir($sourceDir . $file) && $file[0] !== '.' && $topLevelOnly === false) { $this->getDirFileInfo($sourceDir . $file . DIRECTORY_SEPARATOR, $topLevelOnly, true); } elseif (! is_dir($sourceDir . $file) && $file[0] !== '.') { - $_filedata[$file] = $this->getFileInfo($sourceDir . $file); - $_filedata[$file]['relative_path'] = $relativePath; + $filedata[$file] = $this->getFileInfo($sourceDir . $file); + + $filedata[$file]['relative_path'] = $relativePath; } } - closedir($fp); + closedir($filePointer); - return $_filedata; + return $filedata; } return false; @@ -372,10 +400,19 @@ protected function getDirFileInfo(string $sourceDir, bool $topLevelOnly = true, * * @deprecated 4.6.1 Use `get_file_info()` instead. * - * @param string $file Path to file - * @param array|string $returnedValues Array or comma separated string of information returned + * @param string $file Path to file + * @param list|string $returnedValues Array or comma separated string of information returned * - * @return array|false + * @return array{ + * name?: string, + * server_path?: string, + * size?: int, + * date?: int, + * readable?: bool, + * writable?: bool, + * executable?: bool, + * fileperms?: int + * }|false */ protected function getFileInfo(string $file, $returnedValues = ['name', 'server_path', 'size', 'date']) { diff --git a/system/Cache/Handlers/MemcachedHandler.php b/system/Cache/Handlers/MemcachedHandler.php index c6bb6ddcadce..91ac38a9427d 100644 --- a/system/Cache/Handlers/MemcachedHandler.php +++ b/system/Cache/Handlers/MemcachedHandler.php @@ -38,7 +38,7 @@ class MemcachedHandler extends BaseHandler /** * Memcached Configuration * - * @var array + * @var array{host: string, port: int, weight: int, raw: bool} */ protected $config = [ 'host' => '127.0.0.1', @@ -76,43 +76,32 @@ public function initialize() { try { if (class_exists(Memcached::class)) { - // Create new instance of Memcached $this->memcached = new Memcached(); + if ($this->config['raw']) { $this->memcached->setOption(Memcached::OPT_BINARY_PROTOCOL, true); } - // Add server $this->memcached->addServer( $this->config['host'], $this->config['port'], $this->config['weight'], ); - // attempt to get status of servers $stats = $this->memcached->getStats(); // $stats should be an associate array with a key in the format of host:port. // If it doesn't have the key, we know the server is not working as expected. - if (! isset($stats[$this->config['host'] . ':' . $this->config['port']])) { + if (! is_array($stats) || ! isset($stats[$this->config['host'] . ':' . $this->config['port']])) { throw new CriticalError('Cache: Memcached connection failed.'); } } elseif (class_exists(Memcache::class)) { - // Create new instance of Memcache $this->memcached = new Memcache(); - // Check if we can connect to the server - $canConnect = $this->memcached->connect( - $this->config['host'], - $this->config['port'], - ); - - // If we can't connect, throw a CriticalError exception - if ($canConnect === false) { + if (! $this->memcached->connect($this->config['host'], $this->config['port'])) { throw new CriticalError('Cache: Memcache connection failed.'); } - // Add server, third parameter is persistence and defaults to TRUE. $this->memcached->addServer( $this->config['host'], $this->config['port'], diff --git a/system/Cache/Handlers/PredisHandler.php b/system/Cache/Handlers/PredisHandler.php index bdf5afe0f22f..250ea74a88e0 100644 --- a/system/Cache/Handlers/PredisHandler.php +++ b/system/Cache/Handlers/PredisHandler.php @@ -31,7 +31,13 @@ class PredisHandler extends BaseHandler /** * Default config * - * @var array + * @var array{ + * scheme: string, + * host: string, + * password: string|null, + * port: int, + * timeout: int + * } */ protected $config = [ 'scheme' => 'tcp', diff --git a/system/Cache/Handlers/RedisHandler.php b/system/Cache/Handlers/RedisHandler.php index 80c21e8559e8..3a13ee07f8e4 100644 --- a/system/Cache/Handlers/RedisHandler.php +++ b/system/Cache/Handlers/RedisHandler.php @@ -29,7 +29,13 @@ class RedisHandler extends BaseHandler /** * Default config * - * @var array + * @var array{ + * host: string, + * password: string|null, + * port: int, + * timeout: int, + * database: int, + * } */ protected $config = [ 'host' => '127.0.0.1', diff --git a/system/Cache/ResponseCache.php b/system/Cache/ResponseCache.php index e984b8e3606f..2c92dc5058be 100644 --- a/system/Cache/ResponseCache.php +++ b/system/Cache/ResponseCache.php @@ -40,12 +40,10 @@ final class ResponseCache * * @var bool|list */ - private $cacheQueryString = false; + private array|bool $cacheQueryString = false; /** - * Cache time to live. - * - * @var int seconds + * Cache time to live (TTL) in seconds. */ private int $ttl = 0; @@ -54,10 +52,7 @@ public function __construct(CacheConfig $config, private readonly CacheInterface $this->cacheQueryString = $config->cacheQueryString; } - /** - * @return $this - */ - public function setTtl(int $ttl) + public function setTtl(int $ttl): self { $this->ttl = $ttl; @@ -67,11 +62,9 @@ public function setTtl(int $ttl) /** * Generates the cache key to use from the current request. * - * @param CLIRequest|IncomingRequest $request - * * @internal for testing purposes only */ - public function generateCacheKey($request): string + public function generateCacheKey(CLIRequest|IncomingRequest $request): string { if ($request instanceof CLIRequest) { return md5($request->getPath()); @@ -79,7 +72,7 @@ public function generateCacheKey($request): string $uri = clone $request->getUri(); - $query = $this->cacheQueryString + $query = (bool) $this->cacheQueryString ? $uri->getQuery(is_array($this->cacheQueryString) ? ['only' => $this->cacheQueryString] : []) : ''; @@ -88,10 +81,8 @@ public function generateCacheKey($request): string /** * Caches the response. - * - * @param CLIRequest|IncomingRequest $request */ - public function make($request, ResponseInterface $response): bool + public function make(CLIRequest|IncomingRequest $request, ResponseInterface $response): bool { if ($this->ttl === 0) { return true; @@ -118,12 +109,12 @@ public function make($request, ResponseInterface $response): bool /** * Gets the cached response for the request. - * - * @param CLIRequest|IncomingRequest $request */ - public function get($request, ResponseInterface $response): ?ResponseInterface + public function get(CLIRequest|IncomingRequest $request, ResponseInterface $response): ?ResponseInterface { - if ($cachedResponse = $this->cache->get($this->generateCacheKey($request))) { + $cachedResponse = $this->cache->get($this->generateCacheKey($request)); + + if (is_string($cachedResponse) && $cachedResponse !== '') { $cachedResponse = unserialize($cachedResponse); if ( diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 2c89a5e2e4f1..3c81f64c04cd 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -55,7 +55,7 @@ class CodeIgniter /** * The current version of CodeIgniter Framework */ - public const CI_VERSION = '4.6.1'; + public const CI_VERSION = '4.6.2'; /** * App startup time. @@ -141,7 +141,7 @@ class CodeIgniter * web: Invoked by HTTP request * php-cli: Invoked by CLI via `php public/index.php` * - * @phpstan-var 'php-cli'|'web' + * @var 'php-cli'|'web'|null */ protected ?string $context = null; @@ -1128,7 +1128,7 @@ protected function callExit($code) /** * Sets the app context. * - * @phpstan-param 'php-cli'|'web' $context + * @param 'php-cli'|'web' $context * * @return $this */ diff --git a/system/Commands/Generators/TestGenerator.php b/system/Commands/Generators/TestGenerator.php index 7ab6859ed9a9..47f71294c823 100644 --- a/system/Commands/Generators/TestGenerator.php +++ b/system/Commands/Generators/TestGenerator.php @@ -76,6 +76,9 @@ class TestGenerator extends BaseCommand */ public function run(array $params) { + // Ensure tests are always suffixed with 'Test' + $params['suffix'] = null; + $this->component = 'Test'; $this->template = 'test.tpl.php'; @@ -173,20 +176,17 @@ protected function buildPath(string $class): string /** * Returns test file path for the namespace. */ - private function searchTestFilePath(string $namespace): ?string + private function searchTestFilePath(string $testNamespace): ?string { - $bases = service('autoloader')->getNamespace($namespace); - - $base = null; - - foreach ($bases as $candidate) { - if (str_contains($candidate, '/tests/')) { - $base = $candidate; + /** @var list $testPaths */ + $testPaths = service('autoloader')->getNamespace($testNamespace); - break; + foreach ($testPaths as $candidate) { + if (str_contains($candidate, DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR)) { + return $candidate; } } - return $base; + return null; } } diff --git a/system/Commands/Translation/LocalizationFinder.php b/system/Commands/Translation/LocalizationFinder.php index 3307dab8a4e4..e6557b74d1c4 100644 --- a/system/Commands/Translation/LocalizationFinder.php +++ b/system/Commands/Translation/LocalizationFinder.php @@ -361,8 +361,7 @@ private function isSubDirectory(string $directory, string $rootDirectory): bool /** * @param list $files * - * @return array - * @phpstan-return array{'foundLanguageKeys': array>, 'badLanguageKeys': array>, 'countFiles': int} + * @return array{'foundLanguageKeys': array>, 'badLanguageKeys': array>, 'countFiles': int} */ private function findLanguageKeysInFiles(array $files): array { diff --git a/system/Common.php b/system/Common.php index 910ec9b1cc28..3f69838e0b80 100644 --- a/system/Common.php +++ b/system/Common.php @@ -69,8 +69,7 @@ function app_timezone(): string * cache()->save('foo', 'bar'); * $foo = cache('bar'); * - * @return array|bool|CacheInterface|float|int|object|string|null - * @phpstan-return ($key is null ? CacheInterface : array|bool|float|int|object|string|null) + * @return ($key is null ? CacheInterface : mixed) */ function cache(?string $key = null) { @@ -201,8 +200,7 @@ function command(string $command) * * @param class-string|string $name * - * @return ConfigTemplate|null - * @phpstan-return ($name is class-string ? ConfigTemplate : object|null) + * @return ($name is class-string ? ConfigTemplate : object|null) */ function config(string $name, bool $getShared = true) { @@ -404,11 +402,11 @@ function env(string $key, $default = null) * If $data is an array, then it loops over it, escaping each * 'value' of the key/value pairs. * - * @param array|string $data - * @phpstan-param 'html'|'js'|'css'|'url'|'attr'|'raw' $context - * @param string|null $encoding Current encoding for escaping. - * If not UTF-8, we convert strings from this encoding - * pre-escaping and back to this encoding post-escaping. + * @param array|string $data + * @param 'attr'|'css'|'html'|'js'|'raw'|'url' $context + * @param string|null $encoding Current encoding for escaping. + * If not UTF-8, we convert strings from this encoding + * pre-escaping and back to this encoding post-escaping. * * @return array|string * @@ -796,8 +794,7 @@ function log_message(string $level, string $message, array $context = []): void * * @param class-string|string $name * - * @return ModelTemplate|null - * @phpstan-return ($name is class-string ? ModelTemplate : object|null) + * @return ($name is class-string ? ModelTemplate : object|null) */ function model(string $name, bool $getShared = true, ?ConnectionInterface &$conn = null) { @@ -810,9 +807,8 @@ function model(string $name, bool $getShared = true, ?ConnectionInterface &$conn * Provides access to "old input" that was set in the session * during a redirect()->withInput(). * - * @param string|null $default - * @param false|string $escape - * @phpstan-param false|'attr'|'css'|'html'|'js'|'raw'|'url' $escape + * @param string|null $default + * @param 'attr'|'css'|'html'|'js'|'raw'|'url'|false $escape * * @return array|string|null */ @@ -965,8 +961,7 @@ function route_to(string $method, ...$params) * session()->set('foo', 'bar'); * $foo = session('bar'); * - * @return array|bool|float|int|object|Session|string|null - * @phpstan-return ($val is null ? Session : array|bool|float|int|object|string|null) + * @return ($val is null ? Session : mixed) */ function session(?string $val = null) { @@ -1123,8 +1118,7 @@ function stringify_attributes($attributes, bool $js = false): string * @param non-empty-string|null $name * @param (callable(): mixed)|null $callable * - * @return mixed|Timer - * @phpstan-return ($name is null ? Timer : ($callable is (callable(): mixed) ? mixed : Timer)) + * @return ($name is null ? Timer : ($callable is (callable(): mixed) ? mixed : Timer)) */ function timer(?string $name = null, ?callable $callable = null) { diff --git a/system/Config/AutoloadConfig.php b/system/Config/AutoloadConfig.php index 0a99cdb0bb28..b6d7f72bdd54 100644 --- a/system/Config/AutoloadConfig.php +++ b/system/Config/AutoloadConfig.php @@ -14,6 +14,7 @@ namespace CodeIgniter\Config; use Laminas\Escaper\Escaper; +use Laminas\Escaper\EscaperInterface; use Laminas\Escaper\Exception\ExceptionInterface; use Laminas\Escaper\Exception\InvalidArgumentException as EscaperInvalidArgumentException; use Laminas\Escaper\Exception\RuntimeException; @@ -119,6 +120,7 @@ class AutoloadConfig ExceptionInterface::class => SYSTEMPATH . 'ThirdParty/Escaper/Exception/ExceptionInterface.php', EscaperInvalidArgumentException::class => SYSTEMPATH . 'ThirdParty/Escaper/Exception/InvalidArgumentException.php', RuntimeException::class => SYSTEMPATH . 'ThirdParty/Escaper/Exception/RuntimeException.php', + EscaperInterface::class => SYSTEMPATH . 'ThirdParty/Escaper/EscaperInterface.php', Escaper::class => SYSTEMPATH . 'ThirdParty/Escaper/Escaper.php', ]; diff --git a/system/Config/Services.php b/system/Config/Services.php index f5c5a2db4a99..c9266a892e38 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -683,6 +683,11 @@ public static function session(?SessionConfig $config = null, bool $getShared = $driverName = MySQLiHandler::class; } elseif ($driverPlatform === 'Postgre') { $driverName = PostgreHandler::class; + } else { + throw new InvalidArgumentException(sprintf( + 'Invalid session database handler "%s" provided. Only "MySQLi" and "Postgre" are supported.', + $driverPlatform, + )); } } diff --git a/system/Cookie/Cookie.php b/system/Cookie/Cookie.php index df75c03c7bcb..ad461c721112 100644 --- a/system/Cookie/Cookie.php +++ b/system/Cookie/Cookie.php @@ -99,7 +99,16 @@ class Cookie implements ArrayAccess, CloneableCookieInterface * Default attributes for a Cookie object. The keys here are the * lowercase attribute names. Do not camelCase! * - * @var array + * @var array{ + * prefix: string, + * expires: int, + * path: string, + * domain: string, + * secure: bool, + * httponly: bool, + * samesite: string, + * raw: bool, + * } */ private static array $defaults = [ 'prefix' => '', @@ -127,9 +136,27 @@ class Cookie implements ArrayAccess, CloneableCookieInterface * * This method is called from Response::__construct(). * - * @param array|CookieConfig $config + * @param array{ + * prefix?: string, + * expires?: int, + * path?: string, + * domain?: string, + * secure?: bool, + * httponly?: bool, + * samesite?: string, + * raw?: bool, + * }|CookieConfig $config * - * @return array The old defaults array. Useful for resetting. + * @return array{ + * prefix: string, + * expires: int, + * path: string, + * domain: string, + * secure: bool, + * httponly: bool, + * samesite: string, + * raw: bool, + * } The old defaults array. Useful for resetting. */ public static function setDefaults($config = []) { @@ -198,9 +225,9 @@ public static function fromHeaderString(string $cookie, bool $raw = false) /** * Construct a new Cookie instance. * - * @param string $name The cookie's name - * @param string $value The cookie's value - * @param array $options The cookie's options + * @param string $name The cookie's name + * @param string $value The cookie's value + * @param array{prefix?: string, max-age?: int|numeric-string, expires?: DateTimeInterface|int|string, path?: string, domain?: string, secure?: bool, httponly?: bool, samesite?: string, raw?: bool} $options The cookie's options * * @throws CookieException */ diff --git a/system/Cookie/CookieInterface.php b/system/Cookie/CookieInterface.php index c848fa8884ec..b4a7ea3d7320 100644 --- a/system/Cookie/CookieInterface.php +++ b/system/Cookie/CookieInterface.php @@ -145,7 +145,14 @@ public function isRaw(): bool; * Gets the options that are passable to the `setcookie` variant * available on PHP 7.3+ * - * @return array + * @return array{ + * expires: int, + * path: string, + * domain: string, + * secure: bool, + * httponly: bool, + * samesite: string, + * } */ public function getOptions(): array; @@ -164,7 +171,18 @@ public function __toString(); /** * Returns the array representation of the Cookie object. * - * @return array + * @return array{ + * name: string, + * value: string, + * prefix: string, + * raw: bool, + * expires: int, + * path: string, + * domain: string, + * secure: bool, + * httponly: bool, + * samesite: string, + * } */ public function toArray(): array; } diff --git a/system/DataCaster/DataCaster.php b/system/DataCaster/DataCaster.php index ae3f40f4dc25..81ebe233c125 100644 --- a/system/DataCaster/DataCaster.php +++ b/system/DataCaster/DataCaster.php @@ -113,10 +113,9 @@ public function setTypes(array $types): static * Add ? at the beginning of the type (i.e. ?string) to get `null` * instead of casting $value when $value is null. * - * @param mixed $value The value to convert - * @param string $field The field name - * @param string $method Allowed to "get" and "set" - * @phpstan-param 'get'|'set' $method + * @param mixed $value The value to convert + * @param string $field The field name + * @param 'get'|'set' $method Allowed to "get" and "set" */ public function castAs(mixed $value, string $field, string $method = 'get'): mixed { diff --git a/system/DataConverter/DataConverter.php b/system/DataConverter/DataConverter.php index 43b42a08bbdf..fa8353e83abd 100644 --- a/system/DataConverter/DataConverter.php +++ b/system/DataConverter/DataConverter.php @@ -20,9 +20,9 @@ /** * PHP data <==> DataSource data converter * - * @see \CodeIgniter\DataConverter\DataConverterTest - * * @template TEntity of object + * + * @see \CodeIgniter\DataConverter\DataConverterTest */ final class DataConverter { @@ -52,14 +52,14 @@ public function __construct( * Static reconstruct method name or closure to reconstruct an object. * Used by reconstruct(). * - * @phpstan-var (Closure(array): TEntity)|string|null + * @var (Closure(array): TEntity)|string|null */ private readonly Closure|string|null $reconstructor = 'reconstruct', /** * Extract method name or closure to extract data from an object. * Used by extract(). * - * @phpstan-var (Closure(TEntity, bool, bool): array)|string|null + * @var (Closure(TEntity, bool, bool): array)|string|null */ private readonly Closure|string|null $extractor = null, ) { @@ -105,11 +105,10 @@ public function toDataSource(array $phpData): array /** * Takes database data array and creates a specified type object. * - * @param class-string $classname - * @phpstan-param class-string $classname - * @param array $row Raw data from database + * @param class-string $classname + * @param array $row Raw data from database * - * @phpstan-return TEntity + * @return TEntity * * @internal */ diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index cba7f5cbb8b4..82442fbca2b1 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -256,7 +256,7 @@ class BaseBuilder * Specifies which sql statements * support the ignore option. * - * @var array + * @var array */ protected $supportedIgnoreStatements = []; diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index b274b02aa5a8..f09175d59b46 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -206,16 +206,14 @@ abstract class BaseConnection implements ConnectionInterface /** * Connection ID * - * @var false|object|resource - * @phpstan-var false|TConnection + * @var false|TConnection */ public $connID = false; /** * Result ID * - * @var false|object|resource - * @phpstan-var false|TResult + * @var false|TResult */ public $resultID = false; @@ -498,8 +496,7 @@ abstract protected function _close(); /** * Create a persistent database connection. * - * @return false|object|resource - * @phpstan-return false|TConnection + * @return false|TConnection */ public function persistentConnect() { @@ -512,8 +509,7 @@ public function persistentConnect() * get that connection. If you pass either alias in and only a single * connection is present, it must return the sole connection. * - * @return false|object|resource - * @phpstan-return TConnection + * @return false|TConnection */ public function getConnection(?string $alias = null) { @@ -592,8 +588,7 @@ public function addTableAlias(string $alias) /** * Executes the query against the database. * - * @return false|object|resource - * @phpstan-return false|TResult + * @return false|TResult */ abstract protected function execute(string $sql); @@ -607,8 +602,7 @@ abstract protected function execute(string $sql); * * @param array|string|null $binds * - * @return BaseResult|bool|Query BaseResult when “read” type query, bool when “write” type query, Query when prepared query - * @phpstan-return BaseResult|bool|Query + * @return BaseResult|bool|Query * * @todo BC set $queryClass default as null in 4.1 */ @@ -658,9 +652,7 @@ public function query(string $sql, $binds = null, bool $setEscapeFlags = true, s $query->setDuration($startTime, $startTime); // This will trigger a rollback if transactions are being used - if ($this->transDepth !== 0) { - $this->transStatus = false; - } + $this->handleTransStatus(); if ( $this->DBDebug @@ -726,8 +718,7 @@ public function query(string $sql, $binds = null, bool $setEscapeFlags = true, s * is performed, nor are transactions handled. Simply takes a raw * query string and returns the database-specific result id. * - * @return false|object|resource - * @phpstan-return false|TResult + * @return false|TResult */ public function simpleQuery(string $sql) { @@ -911,6 +902,18 @@ public function resetTransStatus(): static return $this; } + /** + * Handle transaction status when a query fails + * + * @internal This method is for internal database component use only + */ + public function handleTransStatus(): void + { + if ($this->transDepth !== 0) { + $this->transStatus = false; + } + } + /** * Begin Transaction */ @@ -1065,8 +1068,7 @@ public function getConnectDuration(int $decimals = 6): string * @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) + * @return ($item is array ? array : string) */ public function protectIdentifiers($item, bool $prefixSingle = false, ?bool $protectIdentifiers = null, bool $fieldExists = true) { @@ -1270,8 +1272,7 @@ private function escapeTableName(TableName $tableName): string * * @param array|string $item * - * @return array|string - * @phpstan-return ($item is array ? array : string) + * @return ($item is array ? array : string) */ public function escapeIdentifiers($item) { @@ -1355,8 +1356,7 @@ abstract public function affectedRows(): int; * * @param array|bool|float|int|object|string|null $str * - * @return array|float|int|string - * @phpstan-return ($str is array ? array : float|int|string) + * @return ($str is array ? array : float|int|string) */ public function escape($str) { @@ -1787,8 +1787,7 @@ public function isWriteType($sql): bool * * Must return an array with keys 'code' and 'message': * - * @return array - * @phpstan-return array{code: int|string|null, message: string|null} + * @return array{code: int|string|null, message: string|null} */ abstract public function error(): array; diff --git a/system/Database/BasePreparedQuery.php b/system/Database/BasePreparedQuery.php index e9017bcf5ff9..d5bb40bebd0a 100644 --- a/system/Database/BasePreparedQuery.php +++ b/system/Database/BasePreparedQuery.php @@ -31,8 +31,7 @@ abstract class BasePreparedQuery implements PreparedQueryInterface /** * The prepared statement itself. * - * @var object|resource|null - * @phpstan-var TStatement|null + * @var TStatement|null */ protected $statement; @@ -61,8 +60,7 @@ abstract class BasePreparedQuery implements PreparedQueryInterface /** * A reference to the db connection to use. * - * @var BaseConnection - * @phpstan-var BaseConnection + * @var BaseConnection */ protected $db; @@ -112,8 +110,7 @@ abstract public function _prepare(string $sql, array $options = []); * Takes a new set of data and runs it against the currently * prepared query. Upon success, will return a Results object. * - * @return bool|ResultInterface - * @phpstan-return bool|ResultInterface + * @return bool|ResultInterface * * @throws DatabaseException */ @@ -137,9 +134,7 @@ public function execute(...$data) $query->setDuration($startTime, $startTime); // This will trigger a rollback if transactions are being used - if ($this->db->transDepth !== 0) { - $this->db->transStatus = false; - } + $this->db->handleTransStatus(); if ($this->db->DBDebug) { // We call this function in order to roll-back queries diff --git a/system/Database/BaseResult.php b/system/Database/BaseResult.php index efbc16722148..c0bdc2aa1025 100644 --- a/system/Database/BaseResult.php +++ b/system/Database/BaseResult.php @@ -27,16 +27,14 @@ abstract class BaseResult implements ResultInterface /** * Connection ID * - * @var object|resource - * @phpstan-var TConnection + * @var TConnection */ public $connID; /** * Result ID * - * @var false|object|resource - * @phpstan-var false|TResult + * @var false|TResult */ public $resultID; @@ -85,10 +83,8 @@ abstract class BaseResult implements ResultInterface /** * Constructor * - * @param object|resource $connID - * @param object|resource $resultID - * @phpstan-param TConnection $connID - * @phpstan-param TResult $resultID + * @param TConnection $connID + * @param TResult $resultID */ public function __construct(&$connID, &$resultID) { @@ -119,7 +115,7 @@ public function getResult(string $type = 'object'): array /** * Returns the results as an array of custom objects. * - * @phpstan-param class-string $className + * @param class-string $className * * @return array */ @@ -211,8 +207,7 @@ public function getResultArray(): array * * If no results, an empty array is returned. * - * @return array - * @phpstan-return list + * @return list */ public function getResultObject(): array { @@ -258,12 +253,10 @@ public function getResultObject(): array * * @template T of object * - * @param int|string $n The index of the results to return, or column name. - * @param string $type The type of result object. 'array', 'object' or class name. - * @phpstan-param class-string|'array'|'object' $type + * @param int|string $n The index of the results to return, or column name. + * @param 'array'|'object'|class-string $type The type of result object. 'array', 'object' or class name. * - * @return array|float|int|object|stdClass|string|null - * @phpstan-return ($n is string ? float|int|string|null : ($type is 'object' ? stdClass|null : ($type is 'array' ? array|null : T|null))) + * @return ($n is string ? float|int|string|null : ($type is 'object' ? stdClass|null : ($type is 'array' ? array|null : T|null))) */ public function getRow($n = 0, string $type = 'object') { @@ -300,11 +293,10 @@ public function getRow($n = 0, string $type = 'object') * * @template T of object * - * @param int $n The index of the results to return. - * @phpstan-param class-string $className + * @param int $n The index of the results to return. + * @param class-string $className * - * @return object|null - * @phpstan-return T|null + * @return T|null */ public function getCustomRowObject(int $n, string $className) { @@ -537,9 +529,9 @@ abstract protected function fetchAssoc(); /** * Returns the result set as an object. * - * Overridden by child classes. + * @param class-string $className * - * @return Entity|false|object|stdClass + * @return false|object */ - abstract protected function fetchObject(string $className = 'stdClass'); + abstract protected function fetchObject(string $className = stdClass::class); } diff --git a/system/Database/ConnectionInterface.php b/system/Database/ConnectionInterface.php index 2f1c932c461d..8e4834486261 100644 --- a/system/Database/ConnectionInterface.php +++ b/system/Database/ConnectionInterface.php @@ -32,16 +32,14 @@ public function initialize(); /** * Connect to the database. * - * @return false|object|resource - * @phpstan-return false|TConnection + * @return false|TConnection */ public function connect(bool $persistent = false); /** * Create a persistent database connection. * - * @return false|object|resource - * @phpstan-return false|TConnection + * @return false|TConnection */ public function persistentConnect(); @@ -59,8 +57,7 @@ public function reconnect(); * get that connection. If you pass either alias in and only a single * connection is present, it must return the sole connection. * - * @return false|object|resource - * @phpstan-return false|TConnection + * @return false|TConnection */ public function getConnection(?string $alias = null); @@ -105,8 +102,7 @@ public function getVersion(): string; * * @param array|string|null $binds * - * @return BaseResult|bool|Query - * @phpstan-return BaseResult|bool|Query + * @return BaseResult|bool|Query */ public function query(string $sql, $binds = null); @@ -115,8 +111,7 @@ public function query(string $sql, $binds = null); * is performed, nor are transactions handled. Simply takes a raw * query string and returns the database-specific result id. * - * @return false|object|resource - * @phpstan-return false|TResult + * @return false|TResult */ public function simpleQuery(string $sql); @@ -144,8 +139,7 @@ public function getLastQuery(); * * @param array|bool|float|int|object|string|null $str * - * @return array|float|int|string - * @phpstan-return ($str is array ? array : float|int|string) + * @return ($str is array ? array : float|int|string) */ public function escape($str); diff --git a/system/Database/Forge.php b/system/Database/Forge.php index 8405d9f7143f..f6eb617fd389 100644 --- a/system/Database/Forge.php +++ b/system/Database/Forge.php @@ -846,8 +846,7 @@ public function modifyColumn(string $table, $fields): bool * @param array|string $processedFields Processed column definitions * or column names to DROP * - * @return false|list|string|null SQL string - * @phpstan-return ($alterType is 'DROP' ? string : list|false|null) + * @return ($alterType is 'DROP' ? string : false|list|null) */ protected function _alterTable(string $alterType, string $table, $processedFields) { diff --git a/system/Database/MySQLi/Builder.php b/system/Database/MySQLi/Builder.php index b8d0fa545d4f..a9e248fc4584 100644 --- a/system/Database/MySQLi/Builder.php +++ b/system/Database/MySQLi/Builder.php @@ -33,7 +33,7 @@ class Builder extends BaseBuilder * Specifies which sql statements * support the ignore option. * - * @var array + * @var array */ protected $supportedIgnoreStatements = [ 'update' => 'IGNORE', diff --git a/system/Database/MySQLi/Forge.php b/system/Database/MySQLi/Forge.php index fe207f5267a5..9482f7e2329a 100644 --- a/system/Database/MySQLi/Forge.php +++ b/system/Database/MySQLi/Forge.php @@ -135,8 +135,7 @@ protected function _createTableAttributes(array $attributes): string * @param array|string $processedFields Processed column definitions * or column names to DROP * - * @return list|string SQL string - * @phpstan-return ($alterType is 'DROP' ? string : list) + * @return ($alterType is 'DROP' ? string : list) */ protected function _alterTable(string $alterType, string $table, $processedFields) { diff --git a/system/Database/OCI8/Forge.php b/system/Database/OCI8/Forge.php index f761045d9784..8f71169115ce 100644 --- a/system/Database/OCI8/Forge.php +++ b/system/Database/OCI8/Forge.php @@ -100,8 +100,7 @@ class Forge extends BaseForge * @param array|string $processedFields Processed column definitions * or column names to DROP * - * @return list|string SQL string - * @phpstan-return ($alterType is 'DROP' ? string : list) + * @return ($alterType is 'DROP' ? string : list) */ protected function _alterTable(string $alterType, string $table, $processedFields) { diff --git a/system/Database/Postgre/Builder.php b/system/Database/Postgre/Builder.php index 8d0d569d5e28..126ab5741892 100644 --- a/system/Database/Postgre/Builder.php +++ b/system/Database/Postgre/Builder.php @@ -36,7 +36,7 @@ class Builder extends BaseBuilder * Specifies which sql statements * support the ignore option. * - * @var array + * @var array */ protected $supportedIgnoreStatements = [ 'insert' => 'ON CONFLICT DO NOTHING', diff --git a/system/Database/Postgre/Connection.php b/system/Database/Postgre/Connection.php index 584c452b233d..9b480975de2e 100644 --- a/system/Database/Postgre/Connection.php +++ b/system/Database/Postgre/Connection.php @@ -242,8 +242,7 @@ public function affectedRows(): int * * @param array|bool|float|int|object|string|null $str * - * @return array|float|int|string - * @phpstan-return ($str is array ? array : float|int|string) + * @return ($str is array ? array : float|int|string) */ public function escape($str) { diff --git a/system/Database/Postgre/Forge.php b/system/Database/Postgre/Forge.php index b0a38c90ed23..8b18d7d48ca7 100644 --- a/system/Database/Postgre/Forge.php +++ b/system/Database/Postgre/Forge.php @@ -86,8 +86,7 @@ protected function _createTableAttributes(array $attributes): string * @param array|string $processedFields Processed column definitions * or column names to DROP * - * @return false|list|string SQL string or false - * @phpstan-return ($alterType is 'DROP' ? string : list|false) + * @return ($alterType is 'DROP' ? string : false|list) */ protected function _alterTable(string $alterType, string $table, $processedFields) { diff --git a/system/Database/PreparedQueryInterface.php b/system/Database/PreparedQueryInterface.php index 6ac69904e7f9..c49c2e7bb551 100644 --- a/system/Database/PreparedQueryInterface.php +++ b/system/Database/PreparedQueryInterface.php @@ -26,8 +26,7 @@ interface PreparedQueryInterface * Takes a new set of data and runs it against the currently * prepared query. Upon success, will return a Results object. * - * @return bool|ResultInterface - * @phpstan-return bool|ResultInterface + * @return bool|ResultInterface */ public function execute(...$data); diff --git a/system/Database/ResultInterface.php b/system/Database/ResultInterface.php index e6383cab6585..89febfef835c 100644 --- a/system/Database/ResultInterface.php +++ b/system/Database/ResultInterface.php @@ -61,12 +61,10 @@ public function getResultObject(): array; * * @template T of object * - * @param int|string $n The index of the results to return, or column name. - * @param string $type The type of result object. 'array', 'object' or class name. - * @phpstan-param class-string|'array'|'object' $type + * @param int|string $n The index of the results to return, or column name. + * @param 'array'|'object'|class-string $type The type of result object. 'array', 'object' or class name. * - * @return array|float|int|object|stdClass|string|null - * @phpstan-return ($n is string ? float|int|string|null : ($type is 'object' ? stdClass|null : ($type is 'array' ? array|null : T|null))) + * @return ($n is string ? float|int|string|null : ($type is 'object' ? stdClass|null : ($type is 'array' ? array|null : T|null))) */ public function getRow($n = 0, string $type = 'object'); @@ -77,11 +75,10 @@ public function getRow($n = 0, string $type = 'object'); * * @template T of object * - * @param int $n The index of the results to return. - * @phpstan-param class-string $className + * @param int $n The index of the results to return. + * @param class-string $className * - * @return object|null - * @phpstan-return T|null + * @return T|null */ public function getCustomRowObject(int $n, string $className); diff --git a/system/Database/SQLSRV/Forge.php b/system/Database/SQLSRV/Forge.php index 7b1da6e85ba7..f35d8e7bbca2 100644 --- a/system/Database/SQLSRV/Forge.php +++ b/system/Database/SQLSRV/Forge.php @@ -180,8 +180,7 @@ protected function _createTableAttributes(array $attributes): string * @param array|string $processedFields Processed column definitions * or column names to DROP * - * @return false|list|string SQL string or false - * @phpstan-return ($alterType is 'DROP' ? string : list|false) + * @return ($alterType is 'DROP' ? string : false|list) */ protected function _alterTable(string $alterType, string $table, $processedFields) { diff --git a/system/Database/SQLite3/Builder.php b/system/Database/SQLite3/Builder.php index 3aa13edeb96a..4f8dff97a0ea 100644 --- a/system/Database/SQLite3/Builder.php +++ b/system/Database/SQLite3/Builder.php @@ -49,7 +49,7 @@ class Builder extends BaseBuilder ]; /** - * @var array + * @var array */ protected $supportedIgnoreStatements = [ 'insert' => 'OR IGNORE', diff --git a/system/Database/SQLite3/Forge.php b/system/Database/SQLite3/Forge.php index 31c2a7ed58a6..2bb9386f6c9e 100644 --- a/system/Database/SQLite3/Forge.php +++ b/system/Database/SQLite3/Forge.php @@ -139,9 +139,7 @@ public function dropColumn(string $table, $columnNames): bool * @param array|string $processedFields Processed column definitions * or column names to DROP * - * @return array|string|null - * @return list|string|null SQL string or null - * @phpstan-return ($alterType is 'DROP' ? string : list|null) + * @return ($alterType is 'DROP' ? string : list|null) */ protected function _alterTable(string $alterType, string $table, $processedFields) { diff --git a/system/Database/SQLite3/Table.php b/system/Database/SQLite3/Table.php index 674d26b7d6f2..ea84e0011e3f 100644 --- a/system/Database/SQLite3/Table.php +++ b/system/Database/SQLite3/Table.php @@ -376,8 +376,7 @@ protected function copyData() * * @param array|bool $fields * - * @return mixed - * @phpstan-return ($fields is array ? array : mixed) + * @return ($fields is array ? array : mixed) */ protected function formatFields($fields) { diff --git a/system/Debug/Toolbar/Collectors/Logs.php b/system/Debug/Toolbar/Collectors/Logs.php index f36dfb1d12b5..c6e22fc0bbd2 100644 --- a/system/Debug/Toolbar/Collectors/Logs.php +++ b/system/Debug/Toolbar/Collectors/Logs.php @@ -45,12 +45,14 @@ class Logs extends BaseCollector /** * Our collected data. * - * @var array + * @var list */ protected $data; /** - * Returns the data of this collector to be formatted in the toolbar + * Returns the data of this collector to be formatted in the toolbar. + * + * @return array{logs: list} */ public function display(): array { @@ -66,7 +68,7 @@ public function isEmpty(): bool { $this->collectLogs(); - return empty($this->data); + return $this->data !== []; } /** @@ -82,14 +84,18 @@ public function icon(): string /** * Ensures the data has been collected. * - * @return array + * @return list */ protected function collectLogs() { - if (! empty($this->data)) { + if ($this->data !== []) { return $this->data; } - return $this->data = service('logger', true)->logCache ?? []; + $cache = service('logger')->logCache; + + $this->data = $cache ?? []; + + return $this->data; } } diff --git a/system/Email/Email.php b/system/Email/Email.php index 2a0facc0ca08..3e4cd5671a97 100644 --- a/system/Email/Email.php +++ b/system/Email/Email.php @@ -68,7 +68,7 @@ class Email /** * Which method to use for sending e-mails. * - * @var string 'mail', 'sendmail' or 'smtp' + * @var 'mail'|'sendmail'|'smtp' */ public $protocol = 'mail'; @@ -117,9 +117,11 @@ class Email /** * SMTP Encryption * - * @var string '', 'tls' or 'ssl'. 'tls' will issue a STARTTLS command - * to the server. 'ssl' means implicit SSL. Connection on port - * 465 should set this to ''. + * * `tls` - will issue a STARTTLS command to the server + * * `ssl` - means implicit SSL + * * `''` - for connection on port 465 + * + * @var ''|'ssl'|'tls' */ public $SMTPCrypto = ''; @@ -142,7 +144,7 @@ class Email /** * Message format. * - * @var string 'text' or 'html' + * @var 'html'|'text' */ public $mailType = 'text'; @@ -170,7 +172,7 @@ class Email /** * X-Priority header value. * - * @var int 1-5 + * @var int<1, 5> */ public $priority = 3; @@ -180,7 +182,7 @@ class Email * * @see http://www.ietf.org/rfc/rfc822.txt * - * @var string "\r\n" or "\n" + * @var "\r\n"|"n" */ public $newline = "\r\n"; @@ -195,7 +197,7 @@ class Email * * @see http://www.ietf.org/rfc/rfc822.txt * - * @var string + * @var "\r\n"|"n" */ public $CRLF = "\r\n"; @@ -268,7 +270,7 @@ class Email /** * Mail encoding * - * @var string '8bit' or '7bit' + * @var '7bit'|'8bit' */ protected $encoding = '8bit'; @@ -342,7 +344,7 @@ class Email * * @see Email::$protocol * - * @var array + * @var list */ protected $protocols = [ 'mail', @@ -368,7 +370,7 @@ class Email * * @see Email::$encoding * - * @var array + * @var list */ protected $bitDepths = [ '7bit', @@ -380,7 +382,7 @@ class Email * * Actual values to send with the X-Priority header * - * @var array + * @var array */ protected $priorities = [ 1 => '1 (Highest)', @@ -414,7 +416,7 @@ public function __construct($config = null) * * @param array|\Config\Email|null $config * - * @return Email + * @return $this */ public function initialize($config) { @@ -445,7 +447,7 @@ public function initialize($config) /** * @param bool $clearAttachments * - * @return Email + * @return $this */ public function clear($clearAttachments = false) { @@ -463,7 +465,7 @@ public function clear($clearAttachments = false) $this->setHeader('Date', $this->setDate()); - if ($clearAttachments !== false) { + if ($clearAttachments) { $this->attachments = []; } @@ -473,13 +475,13 @@ public function clear($clearAttachments = false) /** * @param string $from * @param string $name - * @param string|null $returnPath Return-Path + * @param string|null $returnPath * - * @return Email + * @return $this */ public function setFrom($from, $name = '', $returnPath = null) { - if (preg_match('/\<(.*)\>/', $from, $match)) { + if (preg_match('/\<(.*)\>/', $from, $match) === 1) { $from = $match[1]; } @@ -504,9 +506,8 @@ public function setFrom($from, $name = '', $returnPath = null) } $this->setHeader('From', $name . ' <' . $from . '>'); - if (! isset($returnPath)) { - $returnPath = $from; - } + $returnPath ??= $from; + $this->setHeader('Return-Path', '<' . $returnPath . '>'); $this->tmpArchive['returnPath'] = $returnPath; @@ -517,11 +518,11 @@ public function setFrom($from, $name = '', $returnPath = null) * @param string $replyto * @param string $name * - * @return Email + * @return $this */ public function setReplyTo($replyto, $name = '') { - if (preg_match('/\<(.*)\>/', $replyto, $match)) { + if (preg_match('/\<(.*)\>/', $replyto, $match) === 1) { $replyto = $match[1]; } @@ -550,7 +551,7 @@ public function setReplyTo($replyto, $name = '') /** * @param array|string $to * - * @return Email + * @return $this */ public function setTo($to) { @@ -573,7 +574,7 @@ public function setTo($to) /** * @param string $cc * - * @return Email + * @return $this */ public function setCC($cc) { @@ -598,7 +599,7 @@ public function setCC($cc) * @param string $bcc * @param string $limit * - * @return Email + * @return $this */ public function setBCC($bcc, $limit = '') { @@ -626,7 +627,7 @@ public function setBCC($bcc, $limit = '') /** * @param string $subject * - * @return Email + * @return $this */ public function setSubject($subject) { @@ -641,7 +642,7 @@ public function setSubject($subject) /** * @param string $body * - * @return Email + * @return $this */ public function setMessage($body) { @@ -734,7 +735,7 @@ public function setAttachmentCID($filename) * @param string $header * @param string $value * - * @return Email + * @return $this */ public function setHeader($header, $value) { @@ -744,14 +745,16 @@ public function setHeader($header, $value) } /** - * @param array|string $email + * @param list|string $email * - * @return array + * @return list */ protected function stringToArray($email) { if (! is_array($email)) { - return (str_contains($email, ',')) ? preg_split('/[\s,]/', $email, -1, PREG_SPLIT_NO_EMPTY) : (array) trim($email); + return str_contains($email, ',') + ? preg_split('/[\s,]/', $email, -1, PREG_SPLIT_NO_EMPTY) + : (array) trim($email); } return $email; @@ -760,7 +763,7 @@ protected function stringToArray($email) /** * @param string $str * - * @return Email + * @return $this */ public function setAltMessage($str) { @@ -772,11 +775,11 @@ public function setAltMessage($str) /** * @param string $type * - * @return Email + * @return $this */ public function setMailType($type = 'text') { - $this->mailType = ($type === 'html') ? 'html' : 'text'; + $this->mailType = $type === 'html' ? 'html' : 'text'; return $this; } @@ -784,7 +787,7 @@ public function setMailType($type = 'text') /** * @param bool $wordWrap * - * @return Email + * @return $this */ public function setWordWrap($wordWrap = true) { @@ -796,7 +799,7 @@ public function setWordWrap($wordWrap = true) /** * @param string $protocol * - * @return Email + * @return $this */ public function setProtocol($protocol = 'mail') { @@ -808,7 +811,7 @@ public function setProtocol($protocol = 'mail') /** * @param int $n * - * @return Email + * @return $this */ public function setPriority($n = 3) { @@ -820,7 +823,7 @@ public function setPriority($n = 3) /** * @param string $newline * - * @return Email + * @return $this */ public function setNewline($newline = "\n") { @@ -832,11 +835,11 @@ public function setNewline($newline = "\n") /** * @param string $CRLF * - * @return Email + * @return $this */ public function setCRLF($CRLF = "\n") { - $this->CRLF = ($CRLF !== "\n" && $CRLF !== "\r\n" && $CRLF !== "\r") ? "\n" : $CRLF; + $this->CRLF = ! in_array($CRLF, ["\n", "\r\n", "\r"], true) ? "\n" : $CRLF; return $this; } @@ -891,10 +894,10 @@ protected function getEncoding() protected function getContentType() { if ($this->mailType === 'html') { - return empty($this->attachments) ? 'html' : 'html-attach'; + return $this->attachments === [] ? 'html' : 'html-attach'; } - if ($this->mailType === 'text' && ! empty($this->attachments)) { + if ($this->mailType === 'text' && $this->attachments !== []) { return 'plain-attach'; } @@ -996,8 +999,8 @@ public function cleanEmail($email) */ protected function getAltMessage() { - if (! empty($this->altMessage)) { - return ($this->wordWrap) ? $this->wordWrap($this->altMessage, 76) : $this->altMessage; + if ($this->altMessage !== '') { + return $this->wordWrap ? $this->wordWrap($this->altMessage, 76) : $this->altMessage; } $body = preg_match('/\(.*)\<\/body\>/si', $this->body, $match) ? $match[1] : $this->body; @@ -1009,7 +1012,7 @@ protected function getAltMessage() $body = preg_replace('| +|', ' ', $body); - return ($this->wordWrap) ? $this->wordWrap($body, 76) : $body; + return $this->wordWrap ? $this->wordWrap($body, 76) : $body; } /** @@ -1020,8 +1023,10 @@ protected function getAltMessage() */ public function wordWrap($str, $charlim = null) { - if (empty($charlim)) { - $charlim = empty($this->wrapChars) ? 76 : $this->wrapChars; + $charlim ??= 0; + + if ($charlim === 0) { + $charlim = $this->wrapChars === 0 ? 76 : $this->wrapChars; } if (str_contains($str, "\r")) { @@ -1269,13 +1274,13 @@ protected function buildMessage() } /** - * @param mixed $type + * @param string $type * * @return bool */ protected function attachmentsHaveMultipart($type) { - foreach ($this->attachments as &$attachment) { + foreach ($this->attachments as $attachment) { if ($attachment['multipart'] === $type) { return true; } @@ -1303,14 +1308,14 @@ protected function appendAttachments(&$body, $boundary, $multipart = null) . 'Content-Type: ' . $attachment['type'] . '; name="' . $name . '"' . $this->newline . 'Content-Disposition: ' . $attachment['disposition'] . ';' . $this->newline . 'Content-Transfer-Encoding: base64' . $this->newline - . (empty($attachment['cid']) ? '' : 'Content-ID: <' . $attachment['cid'] . '>' . $this->newline) + . ($attachment['cid'] === '' ? '' : 'Content-ID: <' . $attachment['cid'] . '>' . $this->newline) . $this->newline . $attachment['content'] . $this->newline; } // $name won't be set if no attachments were appended, // and therefore a boundary wouldn't be necessary - if (! empty($name)) { + if (isset($name)) { $body .= '--' . $boundary . '--'; } } @@ -2137,12 +2142,16 @@ protected function getSMTPData() */ protected function getHostname() { - if (isset($_SERVER['SERVER_NAME'])) { - return $_SERVER['SERVER_NAME']; + $superglobals = service('superglobals'); + + $serverName = $superglobals->server('SERVER_NAME'); + if (! in_array($serverName, [null, ''], true)) { + return $serverName; } - if (isset($_SERVER['SERVER_ADDR'])) { - return '[' . $_SERVER['SERVER_ADDR'] . ']'; + $serverAddr = $superglobals->server('SERVER_ADDR'); + if (! in_array($serverAddr, [null, ''], true)) { + return '[' . $serverAddr . ']'; } $hostname = gethostname(); @@ -2218,7 +2227,7 @@ protected function mimeTypes($ext = '') public function __destruct() { - if (is_resource($this->SMTPConnect)) { + if ($this->SMTPConnect !== null) { try { $this->sendCommand('quit'); } catch (ErrorException $e) { diff --git a/system/Events/Events.php b/system/Events/Events.php index ebe13d8682c1..ae68e20573ed 100644 --- a/system/Events/Events.php +++ b/system/Events/Events.php @@ -29,7 +29,7 @@ class Events /** * The list of listeners. * - * @var array + * @var array, 2: list}> */ protected static $listeners = []; @@ -53,7 +53,7 @@ class Events * Stores information about the events * for display in the debug toolbar. * - * @var list> + * @var list */ protected static $performanceLog = []; @@ -84,15 +84,12 @@ public static function initialize() $files = service('locator')->search('Config/Events.php'); } - $files = array_filter(array_map(static function (string $file): false|string { - if (is_file($file)) { - return realpath($file) ?: $file; - } - - return false; // @codeCoverageIgnore - }, $files)); + $files = array_filter(array_map( + static fn (string $file): false|string => realpath($file), + $files, + )); - static::$files = array_unique(array_merge($files, [$events])); + static::$files = array_values(array_unique(array_merge($files, [$events]))); foreach (static::$files as $file) { include $file; @@ -110,9 +107,9 @@ public static function initialize() * Events::on('create', [$myInstance, 'myMethod']); // Method on an existing instance * Events::on('create', function() {}); // Closure * - * @param string $eventName - * @param callable $callback - * @param int $priority + * @param string $eventName + * @param callable(mixed): mixed $callback + * @param int $priority * * @return void */ @@ -138,7 +135,7 @@ public static function on($eventName, $callback, $priority = self::PRIORITY_NORM * b) a method returns false, at which point execution of subscribers stops. * * @param string $eventName - * @param mixed $arguments + * @param mixed ...$arguments */ public static function trigger($eventName, ...$arguments): bool { @@ -175,6 +172,8 @@ public static function trigger($eventName, ...$arguments): bool * sorted by priority. * * @param string $eventName + * + * @return list */ public static function listeners($eventName): array { @@ -200,7 +199,8 @@ public static function listeners($eventName): array * If the listener couldn't be found, returns FALSE, else TRUE if * it was removed. * - * @param string $eventName + * @param string $eventName + * @param callable(mixed): mixed $listener */ public static function removeListener($eventName, callable $listener): bool { @@ -244,6 +244,8 @@ public static function removeAllListeners($eventName = null) /** * Sets the path to the file that routes are read from. * + * @param list $files + * * @return void */ public static function setFiles(array $files) @@ -276,7 +278,7 @@ public static function simulate(bool $choice = true) /** * Getter for the performance log records. * - * @return list> + * @return list */ public static function getPerformanceLogs() { diff --git a/system/Files/File.php b/system/Files/File.php index 23eb324d2684..be7f84f8a20b 100644 --- a/system/Files/File.php +++ b/system/Files/File.php @@ -73,7 +73,7 @@ public function getSize() /** * Retrieve the file size by unit, calculated in IEC standards with 1024 as base value. * - * @phpstan-param positive-int $precision + * @param positive-int $precision */ public function getSizeByBinaryUnit(FileSizeUnit $unit = FileSizeUnit::B, int $precision = 3): int|string { @@ -83,7 +83,7 @@ public function getSizeByBinaryUnit(FileSizeUnit $unit = FileSizeUnit::B, int $p /** * Retrieve the file size by unit, calculated in metric standards with 1000 as base value. * - * @phpstan-param positive-int $precision + * @param positive-int $precision */ public function getSizeByMetricUnit(FileSizeUnit $unit = FileSizeUnit::B, int $precision = 3): int|string { diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index 412a9f457d53..9a253a42b359 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -206,8 +206,8 @@ public function setResponse(ResponseInterface $response) * 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 + * @param string $uri URI path relative to baseURL + * @param 'after'|'before' $position * * @return RequestInterface|ResponseInterface|string|null * @@ -310,7 +310,7 @@ private function createFilter(string $className): FilterInterface /** * Returns the "Required Filters" class list. * - * @phpstan-param 'before'|'after' $position + * @param 'after'|'before' $position * * @return list}> [[classname, arguments], ...] */ @@ -340,7 +340,7 @@ public function getRequiredClasses(string $position): array /** * Runs "Required Filters" for the specified position. * - * @phpstan-param 'before'|'after' $position + * @param 'after'|'before' $position * * @return RequestInterface|ResponseInterface|string|null * @@ -367,7 +367,7 @@ public function runRequired(string $position = 'before') /** * Returns "Required Filters" for the specified position. * - * @phpstan-param 'before'|'after' $position + * @param 'after'|'before' $position * * @internal */ @@ -544,7 +544,7 @@ public function getFiltersClass(): array * MUST be called prior to initialize(); * Intended for use within routes files. * - * @phpstan-param 'before'|'after' $position + * @param 'after'|'before' $position * * @return $this */ @@ -574,9 +574,9 @@ public function addFilter(string $class, ?string $alias = null, string $position * after the filter name, followed by a comma-separated list of arguments that * are passed to the filter when executed. * - * @param string $filter filter_name or filter_name:arguments like 'role:admin,manager' - * or filter classname. - * @phpstan-param 'before'|'after' $position + * @param string $filter filter_name or filter_name:arguments like 'role:admin,manager' + * or filter classname. + * @param 'after'|'before' $position */ private function enableFilter(string $filter, string $position = 'before'): void { @@ -824,7 +824,7 @@ protected function processFilters(?string $uri = null) /** * Maps filter aliases to the equivalent filter classes * - * @phpstan-param 'before'|'after' $position + * @param 'after'|'before' $position * * @return void * diff --git a/system/Format/Exceptions/FormatException.php b/system/Format/Exceptions/FormatException.php index 46daad558c50..cde333b1292c 100644 --- a/system/Format/Exceptions/FormatException.php +++ b/system/Format/Exceptions/FormatException.php @@ -37,7 +37,7 @@ public static function forInvalidFormatter(string $class) * Thrown in JSONFormatter when the json_encode produces * an error code other than JSON_ERROR_NONE and JSON_ERROR_RECURSION. * - * @param string $error The error message + * @param string|null $error The error message * * @return static */ diff --git a/system/Format/Format.php b/system/Format/Format.php index 4a9cb60981e6..6100b1a586a8 100644 --- a/system/Format/Format.php +++ b/system/Format/Format.php @@ -23,19 +23,8 @@ */ class Format { - /** - * Configuration instance - * - * @var FormatConfig - */ - protected $config; - - /** - * Constructor. - */ - public function __construct(FormatConfig $config) + public function __construct(protected FormatConfig $config) { - $this->config = $config; } /** diff --git a/system/Format/FormatterInterface.php b/system/Format/FormatterInterface.php index 0f005568c787..0c2492b90a77 100644 --- a/system/Format/FormatterInterface.php +++ b/system/Format/FormatterInterface.php @@ -21,9 +21,9 @@ interface FormatterInterface /** * Takes the given data and formats it. * - * @param array|object|string $data + * @param array|object|string $data * - * @return false|string + * @return false|non-empty-string */ public function format($data); } diff --git a/system/Format/JSONFormatter.php b/system/Format/JSONFormatter.php index 7d2ad5259ce3..a6ba87cd724a 100644 --- a/system/Format/JSONFormatter.php +++ b/system/Format/JSONFormatter.php @@ -26,9 +26,9 @@ class JSONFormatter implements FormatterInterface /** * Takes the given data and formats it. * - * @param array|bool|float|int|object|string|null $data + * @param array|object|string $data * - * @return false|string (JSON string | false) + * @return false|non-empty-string */ public function format($data) { @@ -37,7 +37,9 @@ public function format($data) $options = $config->formatterOptions['application/json'] ?? JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES; $options |= JSON_PARTIAL_OUTPUT_ON_ERROR; - $options = ENVIRONMENT === 'production' ? $options : $options | JSON_PRETTY_PRINT; + if (ENVIRONMENT !== 'production') { + $options |= JSON_PRETTY_PRINT; + } $result = json_encode($data, $options, 512); diff --git a/system/Format/XMLFormatter.php b/system/Format/XMLFormatter.php index c85eae5feffe..efef70cec2cb 100644 --- a/system/Format/XMLFormatter.php +++ b/system/Format/XMLFormatter.php @@ -27,9 +27,9 @@ class XMLFormatter implements FormatterInterface /** * Takes the given data and formats it. * - * @param array|bool|float|int|object|string|null $data + * @param array|object|string $data * - * @return false|string (XML string | false) + * @return false|non-empty-string */ public function format($data) { @@ -56,7 +56,8 @@ public function format($data) * * @see http://www.codexworld.com/convert-array-to-xml-in-php/ * - * @param SimpleXMLElement $output + * @param array $data + * @param SimpleXMLElement $output * * @return void */ diff --git a/system/HTTP/CLIRequest.php b/system/HTTP/CLIRequest.php index 6ef97170914e..bde4e1b8e3f8 100644 --- a/system/HTTP/CLIRequest.php +++ b/system/HTTP/CLIRequest.php @@ -314,9 +314,6 @@ public function getLocale(): string /** * Checks this request type. - * - * @param string $type HTTP verb or 'json' or 'ajax' - * @phpstan-param string|'get'|'post'|'put'|'delete'|'head'|'patch'|'options'|'json'|'ajax' $type */ public function is(string $type): bool { diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php index 8a21d77262a0..5e8c5847ca02 100644 --- a/system/HTTP/CURLRequest.php +++ b/system/HTTP/CURLRequest.php @@ -383,22 +383,8 @@ public function send(string $method, string $url) // Set the string we want to break our response from $breakString = "\r\n\r\n"; - if (isset($this->config['allow_redirects']) && $this->config['allow_redirects'] !== false) { - $output = $this->handleRedirectHeaders($output, $breakString); - } - - while (str_starts_with($output, 'HTTP/1.1 100 Continue')) { - $output = substr($output, strpos($output, $breakString) + 4); - } - - if (preg_match('/HTTP\/\d\.\d 200 Connection established/i', $output)) { - $output = substr($output, strpos($output, $breakString) + 4); - } - - // If request and response have Digest - if (isset($this->config['auth'][2]) && $this->config['auth'][2] === 'digest' && str_contains($output, 'WWW-Authenticate: Digest')) { - $output = substr($output, strpos($output, $breakString) + 4); - } + // Remove all intermediate responses + $output = $this->removeIntermediateResponses($output, $breakString); // Split out our headers and body $break = strpos($output, $breakString); @@ -696,6 +682,8 @@ protected function setCURLOptions(array $curlOptions = [], array $config = []) * Does the actual work of initializing cURL, setting the options, * and grabbing the output. * + * @param array $curlOptions + * * @codeCoverageIgnore */ protected function sendRequest(array $curlOptions = []): string @@ -716,29 +704,71 @@ protected function sendRequest(array $curlOptions = []): string return $output; } - private function handleRedirectHeaders(string $output, string $breakString): string + private function removeIntermediateResponses(string $output, string $breakString): string { - // Strip out multiple redirect header sections - while (preg_match('/^HTTP\/\d(?:\.\d)? 3\d\d/', $output)) { - $breakStringPos = strpos($output, $breakString); - $redirectHeaderSection = substr($output, 0, $breakStringPos); - $redirectHeaders = explode("\n", $redirectHeaderSection); - $locationHeaderFound = false; - - foreach ($redirectHeaders as $header) { - if (str_starts_with(strtolower($header), 'location:')) { - $locationHeaderFound = true; - break; + while (true) { + // Check if we should remove the current response + if ($this->shouldRemoveCurrentResponse($output, $breakString)) { + $breakStringPos = strpos($output, $breakString); + if ($breakStringPos !== false) { + $output = substr($output, $breakStringPos + 4); + + continue; } } - if ($locationHeaderFound) { - $output = substr($output, $breakStringPos + 4); - } else { - break; - } + // No more intermediate responses to remove + break; } return $output; } + + /** + * Check if the current response (at the beginning of output) should be removed. + */ + private function shouldRemoveCurrentResponse(string $output, string $breakString): bool + { + // HTTP/x.x 1xx responses (Continue, Processing, etc.) + if (preg_match('/^HTTP\/\d+(?:\.\d+)?\s+1\d\d\s/', $output)) { + return true; + } + + // HTTP/x.x 200 Connection established (proxy responses) + if (preg_match('/^HTTP\/\d+(?:\.\d+)?\s+200\s+Connection\s+established/i', $output)) { + return true; + } + + // HTTP/x.x 3xx responses (redirects) - only if redirects are allowed + $allowRedirects = isset($this->config['allow_redirects']) && $this->config['allow_redirects'] !== false; + if ($allowRedirects && preg_match('/^HTTP\/\d+(?:\.\d+)?\s+3\d\d\s/', $output)) { + // Check if there's a Location header + $breakStringPos = strpos($output, $breakString); + if ($breakStringPos !== false) { + $headerSection = substr($output, 0, $breakStringPos); + $headers = explode("\n", $headerSection); + + foreach ($headers as $header) { + if (str_starts_with(strtolower($header), 'location:')) { + return true; // Found location header, this is a redirect to remove + } + } + } + } + + // Digest auth challenges - only remove if there's another response after + if (isset($this->config['auth'][2]) && $this->config['auth'][2] === 'digest') { + $breakStringPos = strpos($output, $breakString); + if ($breakStringPos !== false) { + $headerSection = substr($output, 0, $breakStringPos); + if (str_contains($headerSection, 'WWW-Authenticate: Digest')) { + $nextBreakPos = strpos($output, $breakString, $breakStringPos + 4); + + return $nextBreakPos !== false; // Only remove if there's another response + } + } + } + + return false; + } } diff --git a/system/HTTP/DownloadResponse.php b/system/HTTP/DownloadResponse.php index a0da2e8ce4b9..f6861d549e9f 100644 --- a/system/HTTP/DownloadResponse.php +++ b/system/HTTP/DownloadResponse.php @@ -182,22 +182,21 @@ private function getDownloadFileName(): string } /** - * get Content-Disposition Header string. + * Get Content-Disposition Header string. */ - private function getContentDisposition(): string + private function getContentDisposition(bool $inline = false): string { - $downloadFilename = $this->getDownloadFileName(); - - $utf8Filename = $downloadFilename; + $downloadFilename = $utf8Filename = $this->getDownloadFileName(); + $disposition = $inline ? 'inline' : 'attachment'; if (strtoupper($this->charset) !== 'UTF-8') { $utf8Filename = mb_convert_encoding($downloadFilename, 'UTF-8', $this->charset); } - $result = sprintf('attachment; filename="%s"', $downloadFilename); + $result = sprintf('%s; filename="%s"', $disposition, addslashes($downloadFilename)); if ($utf8Filename !== '') { - $result .= '; filename*=UTF-8\'\'' . rawurlencode($utf8Filename); + $result .= sprintf('; filename*=UTF-8\'\'%s', rawurlencode($utf8Filename)); } return $result; @@ -341,7 +340,7 @@ private function sendBodyByBinary() */ public function inline() { - $this->setHeader('Content-Disposition', 'inline'); + $this->setHeader('Content-Disposition', $this->getContentDisposition(true)); return $this; } diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index ba6f61d7349f..72b1ce0ca5df 100644 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -348,9 +348,6 @@ public function negotiate(string $type, array $supported, bool $strictMatch = fa /** * Checks this request type. - * - * @param string $type HTTP verb or 'json' or 'ajax' - * @phpstan-param string|'get'|'post'|'put'|'delete'|'head'|'patch'|'options'|'json'|'ajax' $type */ public function is(string $type): bool { diff --git a/system/HTTP/RequestTrait.php b/system/HTTP/RequestTrait.php index 39f4269ef1df..3c3da161a3be 100644 --- a/system/HTTP/RequestTrait.php +++ b/system/HTTP/RequestTrait.php @@ -224,9 +224,8 @@ public function getEnv($index = null, $filter = null, $flags = null) /** * Allows manually setting the value of PHP global, like $_GET, $_POST, etc. * - * @param string $name Supergrlobal name (lowercase) - * @phpstan-param 'get'|'post'|'request'|'cookie'|'server' $name - * @param mixed $value + * @param 'cookie'|'get'|'post'|'request'|'server' $name Superglobal name (lowercase) + * @param mixed $value * * @return $this */ @@ -247,11 +246,10 @@ public function setGlobal(string $name, $value) * * http://php.net/manual/en/filter.filters.sanitize.php * - * @param string $name Supergrlobal name (lowercase) - * @phpstan-param 'get'|'post'|'request'|'cookie'|'server' $name - * @param array|int|string|null $index - * @param int|null $filter Filter constant - * @param array|int|null $flags Options + * @param 'cookie'|'get'|'post'|'request'|'server' $name Superglobal name (lowercase) + * @param array|int|string|null $index + * @param int|null $filter Filter constant + * @param array|int|null $flags Options * * @return array|bool|float|int|object|string|null */ @@ -341,8 +339,7 @@ public function fetchGlobal(string $name, $index = null, ?int $filter = null, $f * Saves a copy of the current state of one of several PHP globals, * so we can retrieve them later. * - * @param string $name Superglobal name (lowercase) - * @phpstan-param 'get'|'post'|'request'|'cookie'|'server' $name + * @param 'cookie'|'get'|'post'|'request'|'server' $name Superglobal name (lowercase) * * @return void */ diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index 6e5206a0d493..d6653b10f537 100644 --- a/system/HTTP/SiteURI.php +++ b/system/HTTP/SiteURI.php @@ -85,11 +85,10 @@ class SiteURI extends URI private string $routePath; /** - * @param string $relativePath URI path relative to baseURL. May include - * queries or fragments. - * @param string|null $host Optional current hostname. - * @param string|null $scheme Optional scheme. 'http' or 'https'. - * @phpstan-param 'http'|'https'|null $scheme + * @param string $relativePath URI path relative to baseURL. May include + * queries or fragments. + * @param string|null $host Optional current hostname. + * @param 'http'|'https'|null $scheme Optional scheme. 'http' or 'https'. */ public function __construct( App $configApp, diff --git a/system/HTTP/SiteURIFactory.php b/system/HTTP/SiteURIFactory.php index c87898c0470e..483c3ded5779 100644 --- a/system/HTTP/SiteURIFactory.php +++ b/system/HTTP/SiteURIFactory.php @@ -132,7 +132,8 @@ private function parseRequestURI(): string && pathinfo($this->superglobals->server('SCRIPT_NAME'), PATHINFO_EXTENSION) === 'php' ) { // Compare each segment, dropping them until there is no match - $segments = $keep = explode('/', $path); + $segments = explode('/', rawurldecode($path)); + $keep = explode('/', $path); foreach (explode('/', $this->superglobals->server('SCRIPT_NAME')) as $i => $segment) { // If these segments are not the same then we're done @@ -146,6 +147,15 @@ private function parseRequestURI(): string $path = implode('/', $keep); } + // Cleanup: if indexPage is still visible in the path, remove it + if ($this->appConfig->indexPage !== '' && str_starts_with($path, $this->appConfig->indexPage)) { + $remainingPath = substr($path, strlen($this->appConfig->indexPage)); + // Only remove if followed by '/' (route) or nothing (root) + if ($remainingPath === '' || str_starts_with($remainingPath, '/')) { + $path = ltrim($remainingPath, '/'); + } + } + // This section ensures that even on servers that require the URI to // contain the query string (Nginx) a correct URI is found, and also // fixes the QUERY_STRING Server var and $_GET array. diff --git a/system/HTTP/URI.php b/system/HTTP/URI.php index b714995ce143..5bcf11de655a 100644 --- a/system/HTTP/URI.php +++ b/system/HTTP/URI.php @@ -376,7 +376,7 @@ public function getAuthority(bool $ignorePort = false): string } // Don't add port if it's a standard port for this scheme - if ((int) $this->port !== 0 && ! $ignorePort && $this->port !== $this->defaultPorts[$this->scheme]) { + if ((int) $this->port !== 0 && ! $ignorePort && $this->port !== ($this->defaultPorts[$this->scheme] ?? null)) { $authority .= ':' . $this->port; } diff --git a/system/Helpers/filesystem_helper.php b/system/Helpers/filesystem_helper.php index aa7efbea0468..e7bd35441959 100644 --- a/system/Helpers/filesystem_helper.php +++ b/system/Helpers/filesystem_helper.php @@ -255,6 +255,14 @@ function get_filenames( * @param string $sourceDir Path to source * @param bool $topLevelOnly Look only at the top level directory specified? * @param bool $recursion Internal variable to determine recursion status - do not use in calls + * + * @return array */ function get_dir_file_info(string $sourceDir, bool $topLevelOnly = true, bool $recursion = false): array { @@ -298,10 +306,19 @@ function get_dir_file_info(string $sourceDir, bool $topLevelOnly = true, bool $r * Options are: name, server_path, size, date, readable, writable, executable, fileperms * Returns false if the file cannot be found. * - * @param string $file Path to file - * @param array|string $returnedValues Array or comma separated string of information returned + * @param string $file Path to file + * @param list|string $returnedValues Array or comma separated string of information returned * - * @return array|null + * @return array{ + * name?: string, + * server_path?: string, + * size?: int, + * date?: int, + * readable?: bool, + * writable?: bool, + * executable?: bool, + * fileperms?: int + * }|null */ function get_file_info(string $file, $returnedValues = ['name', 'server_path', 'size', 'date']) { diff --git a/system/Helpers/security_helper.php b/system/Helpers/security_helper.php index d42ac0dda777..d73fd6219b12 100644 --- a/system/Helpers/security_helper.php +++ b/system/Helpers/security_helper.php @@ -15,11 +15,69 @@ if (! function_exists('sanitize_filename')) { /** - * Sanitize a filename to use in a URI. + * Sanitize Filename + * + * Tries to sanitize filenames in order to prevent directory traversal attempts + * and other security threats, which is particularly useful for files that + * were supplied via user input. + * + * If it is acceptable for the user input to include relative paths, + * e.g. file/in/some/approved/folder.txt, you can set the second optional + * parameter, $relativePath to TRUE. + * + * @param string $filename Input file name + * @param bool $relativePath Whether to preserve paths */ - function sanitize_filename(string $filename): string + function sanitize_filename(string $filename, bool $relativePath = false): string { - return service('security')->sanitizeFilename($filename); + // List of sanitized filename strings + $bad = [ + '../', + '', + '<', + '>', + "'", + '"', + '&', + '$', + '#', + '{', + '}', + '[', + ']', + '=', + ';', + '?', + '%20', + '%22', + '%3c', + '%253c', + '%3e', + '%0e', + '%28', + '%29', + '%2528', + '%26', + '%24', + '%3f', + '%3b', + '%3d', + ]; + + if (! $relativePath) { + $bad[] = './'; + $bad[] = '/'; + } + + $filename = remove_invisible_characters($filename, false); + + do { + $old = $filename; + $filename = str_replace($bad, '', $filename); + } while ($old !== $filename); + + return stripslashes($filename); } } diff --git a/system/Images/Handlers/ImageMagickHandler.php b/system/Images/Handlers/ImageMagickHandler.php index f1448efa7f8a..233ea2d07149 100644 --- a/system/Images/Handlers/ImageMagickHandler.php +++ b/system/Images/Handlers/ImageMagickHandler.php @@ -82,8 +82,8 @@ public function _resize(bool $maintainRatio = false) } $action = $maintainRatio - ? ' -resize ' . ($this->width ?? 0) . 'x' . ($this->height ?? 0) . ' "' . $source . '" "' . $destination . '"' - : ' -resize ' . ($this->width ?? 0) . 'x' . ($this->height ?? 0) . "{$escape}! \"" . $source . '" "' . $destination . '"'; + ? ' -resize ' . ($this->width ?? 0) . 'x' . ($this->height ?? 0) . ' ' . escapeshellarg($source) . ' ' . escapeshellarg($destination) + : ' -resize ' . ($this->width ?? 0) . 'x' . ($this->height ?? 0) . "{$escape}! " . escapeshellarg($source) . ' ' . escapeshellarg($destination); $this->process($action); @@ -354,7 +354,7 @@ protected function _text(string $text, array $options = []) // Font if (! empty($options['fontPath'])) { - $cmd .= " -font '{$options['fontPath']}'"; + $cmd .= ' -font ' . escapeshellarg($options['fontPath']); } if (isset($options['hAlign'], $options['vAlign'])) { @@ -393,28 +393,28 @@ protected function _text(string $text, array $options = []) $xAxis = $xAxis >= 0 ? '+' . $xAxis : $xAxis; $yAxis = $yAxis >= 0 ? '+' . $yAxis : $yAxis; - $cmd .= " -gravity {$gravity} -geometry {$xAxis}{$yAxis}"; + $cmd .= ' -gravity ' . escapeshellarg($gravity) . ' -geometry ' . escapeshellarg("{$xAxis}{$yAxis}"); } // Color if (isset($options['color'])) { [$r, $g, $b] = sscanf("#{$options['color']}", '#%02x%02x%02x'); - $cmd .= " -fill 'rgba({$r},{$g},{$b},{$options['opacity']})'"; + $cmd .= ' -fill ' . escapeshellarg("rgba({$r},{$g},{$b},{$options['opacity']})"); } // Font Size - use points.... if (isset($options['fontSize'])) { - $cmd .= " -pointsize {$options['fontSize']}"; + $cmd .= ' -pointsize ' . escapeshellarg((string) $options['fontSize']); } // Text - $cmd .= " -annotate 0 '{$text}'"; + $cmd .= ' -annotate 0 ' . escapeshellarg($text); $source = ! empty($this->resource) ? $this->resource : $this->image()->getPathname(); $destination = $this->getResourcePath(); - $cmd = " '{$source}' {$cmd} '{$destination}'"; + $cmd = ' ' . escapeshellarg($source) . ' ' . $cmd . ' ' . escapeshellarg($destination); $this->process($cmd); } diff --git a/system/Log/Handlers/BaseHandler.php b/system/Log/Handlers/BaseHandler.php index 34f9fe2ecd6c..2b82f0f49225 100644 --- a/system/Log/Handlers/BaseHandler.php +++ b/system/Log/Handlers/BaseHandler.php @@ -21,7 +21,7 @@ abstract class BaseHandler implements HandlerInterface /** * Handles * - * @var array + * @var list */ protected $handles; @@ -33,7 +33,7 @@ abstract class BaseHandler implements HandlerInterface protected $dateFormat = 'Y-m-d H:i:s'; /** - * Constructor + * @param array{handles?: list} $config */ public function __construct(array $config) { diff --git a/system/Log/Handlers/ChromeLoggerHandler.php b/system/Log/Handlers/ChromeLoggerHandler.php index 5813f338b389..8d763399b513 100644 --- a/system/Log/Handlers/ChromeLoggerHandler.php +++ b/system/Log/Handlers/ChromeLoggerHandler.php @@ -16,8 +16,6 @@ use CodeIgniter\HTTP\ResponseInterface; /** - * Class ChromeLoggerHandler - * * Allows for logging items to the Chrome console for debugging. * Requires the ChromeLogger extension installed in your browser. * @@ -41,7 +39,16 @@ class ChromeLoggerHandler extends BaseHandler /** * The final data that is sent to the browser. * - * @var array + * @var array{ + * version: float, + * columns: list, + * rows: list, + * 1: string, + * 2: string, + * }>, + * request_uri?: string, + * } */ protected $json = [ 'version' => self::VERSION, @@ -63,7 +70,7 @@ class ChromeLoggerHandler extends BaseHandler /** * Maps the log levels to the ChromeLogger types. * - * @var array + * @var array */ protected $levels = [ 'emergency' => 'error', @@ -77,7 +84,7 @@ class ChromeLoggerHandler extends BaseHandler ]; /** - * Constructor + * @param array{handles?: list} $config */ public function __construct(array $config = []) { @@ -97,10 +104,8 @@ public function __construct(array $config = []) */ public function handle($level, $message): bool { - // Format our message $message = $this->format($message); - // Generate Backtrace info $backtrace = debug_backtrace(0, $this->backtraceLevel); $backtrace = end($backtrace); @@ -116,11 +121,7 @@ public function handle($level, $message): bool $type = $this->levels[$level]; } - $this->json['rows'][] = [ - [$message], - $backtraceMessage, - $type, - ]; + $this->json['rows'][] = [[$message], $backtraceMessage, $type]; $this->sendLogs(); @@ -130,9 +131,9 @@ public function handle($level, $message): bool /** * Converts the object to display nicely in the Chrome Logger UI. * - * @param array|int|object|string $object + * @param object|string $object * - * @return array + * @return array|string */ protected function format($object) { diff --git a/system/Log/Handlers/ErrorlogHandler.php b/system/Log/Handlers/ErrorlogHandler.php index 8eee392c6b6f..a7e820419fee 100644 --- a/system/Log/Handlers/ErrorlogHandler.php +++ b/system/Log/Handlers/ErrorlogHandler.php @@ -38,14 +38,14 @@ class ErrorlogHandler extends BaseHandler * Says where the error should go. Currently supported are * 0 (`TYPE_OS`) and 4 (`TYPE_SAPI`). * - * @var int + * @var 0|4 */ protected $messageType = 0; /** * Constructor. * - * @param list $config + * @param array{handles?: list, messageType?: int} $config */ public function __construct(array $config = []) { @@ -79,6 +79,8 @@ public function handle($level, $message): bool /** * Extracted call to `error_log()` in order to be tested. * + * @param 0|4 $messageType + * * @codeCoverageIgnore */ protected function errorLog(string $message, int $messageType): bool diff --git a/system/Log/Handlers/FileHandler.php b/system/Log/Handlers/FileHandler.php index 64d1485f0f7a..d3132bf878ac 100644 --- a/system/Log/Handlers/FileHandler.php +++ b/system/Log/Handlers/FileHandler.php @@ -45,18 +45,22 @@ class FileHandler extends BaseHandler protected $filePermissions; /** - * Constructor + * @param array{handles?: list, path?: string, fileExtension?: string, filePermissions?: int} $config */ public function __construct(array $config = []) { parent::__construct($config); - $this->path = empty($config['path']) ? WRITEPATH . 'logs/' : $config['path']; + $defaults = ['path' => WRITEPATH . 'logs/', 'fileExtension' => 'log', 'filePermissions' => 0644]; + $config = [...$defaults, ...$config]; - $this->fileExtension = empty($config['fileExtension']) ? 'log' : $config['fileExtension']; - $this->fileExtension = ltrim($this->fileExtension, '.'); + $this->path = $config['path'] === '' ? $defaults['path'] : $config['path']; - $this->filePermissions = $config['filePermissions'] ?? 0644; + $this->fileExtension = $config['fileExtension'] === '' + ? $defaults['fileExtension'] + : ltrim($config['fileExtension'], '.'); + + $this->filePermissions = $config['filePermissions']; } /** @@ -108,10 +112,8 @@ public function handle($level, $message): bool for ($written = 0, $length = strlen($msg); $written < $length; $written += $result) { if (($result = fwrite($fp, substr($msg, $written))) === false) { - // if we get this far, we'll never see this during travis-ci - // @codeCoverageIgnoreStart - break; - // @codeCoverageIgnoreEnd + // if we get this far, we'll never see this during unit testing + break; // @codeCoverageIgnore } } diff --git a/system/Log/Logger.php b/system/Log/Logger.php index 7c14fcdaeb5b..3770e3a6567f 100644 --- a/system/Log/Logger.php +++ b/system/Log/Logger.php @@ -54,11 +54,11 @@ class Logger implements LoggerInterface ]; /** - * Array of levels to be logged. - * The rest will be ignored. - * Set in Config/logger.php + * Array of levels to be logged. The rest will be ignored. * - * @var array + * Set in app/Config/Logger.php + * + * @var list */ protected $loggableLevels = []; @@ -86,7 +86,7 @@ class Logger implements LoggerInterface /** * Caches instances of the handlers. * - * @var array + * @var array, HandlerInterface> */ protected $handlers = []; @@ -96,14 +96,14 @@ class Logger implements LoggerInterface * value is an associative array of configuration * items. * - * @var array|string>> + * @var array, array|string>> */ protected $handlerConfig = []; /** * Caches logging calls for debugbar. * - * @var array + * @var list */ public $logCache; @@ -123,32 +123,34 @@ class Logger implements LoggerInterface */ public function __construct($config, bool $debug = CI_DEBUG) { - $this->loggableLevels = is_array($config->threshold) ? $config->threshold : range(1, (int) $config->threshold); + $loggableLevels = is_array($config->threshold) ? $config->threshold : range(1, (int) $config->threshold); // Now convert loggable levels to strings. // We only use numbers to make the threshold setting convenient for users. - if ($this->loggableLevels !== []) { - $temp = []; + foreach ($loggableLevels as $level) { + /** @var false|string $stringLevel */ + $stringLevel = array_search($level, $this->logLevels, true); - foreach ($this->loggableLevels as $level) { - $temp[] = array_search((int) $level, $this->logLevels, true); + if ($stringLevel === false) { + continue; } - $this->loggableLevels = $temp; - unset($temp); + $this->loggableLevels[] = $stringLevel; } - $this->dateFormat = $config->dateFormat ?? $this->dateFormat; + if (isset($config->dateFormat)) { + $this->dateFormat = $config->dateFormat; + } - if (! is_array($config->handlers) || $config->handlers === []) { + if ($config->handlers === []) { throw LogException::forNoHandlers('LoggerConfig'); } // Save the handler configuration for later. // Instances will be created on demand. $this->handlerConfig = $config->handlers; + $this->cacheLogs = $debug; - $this->cacheLogs = $debug; if ($this->cacheLogs) { $this->logCache = []; } @@ -156,8 +158,6 @@ public function __construct($config, bool $debug = CI_DEBUG) /** * System is unusable. - * - * @param string $message */ public function emergency(string|Stringable $message, array $context = []): void { @@ -169,8 +169,6 @@ public function emergency(string|Stringable $message, array $context = []): void * * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. - * - * @param string $message */ public function alert(string|Stringable $message, array $context = []): void { @@ -181,8 +179,6 @@ public function alert(string|Stringable $message, array $context = []): void * Critical conditions. * * Example: Application component unavailable, unexpected exception. - * - * @param string $message */ public function critical(string|Stringable $message, array $context = []): void { @@ -192,8 +188,6 @@ public function critical(string|Stringable $message, array $context = []): void /** * Runtime errors that do not require immediate action but should typically * be logged and monitored. - * - * @param string $message */ public function error(string|Stringable $message, array $context = []): void { @@ -205,8 +199,6 @@ public function error(string|Stringable $message, array $context = []): void * * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. - * - * @param string $message */ public function warning(string|Stringable $message, array $context = []): void { @@ -215,8 +207,6 @@ public function warning(string|Stringable $message, array $context = []): void /** * Normal but significant events. - * - * @param string $message */ public function notice(string|Stringable $message, array $context = []): void { @@ -227,8 +217,6 @@ public function notice(string|Stringable $message, array $context = []): void * Interesting events. * * Example: User logs in, SQL logs. - * - * @param string $message */ public function info(string|Stringable $message, array $context = []): void { @@ -237,8 +225,6 @@ public function info(string|Stringable $message, array $context = []): void /** * Detailed debug information. - * - * @param string $message */ public function debug(string|Stringable $message, array $context = []): void { @@ -248,8 +234,7 @@ public function debug(string|Stringable $message, array $context = []): void /** * Logs with an arbitrary level. * - * @param string $level - * @param string $message + * @param mixed $level */ public function log($level, string|Stringable $message, array $context = []): void { @@ -257,24 +242,18 @@ public function log($level, string|Stringable $message, array $context = []): vo $level = array_search((int) $level, $this->logLevels, true); } - // Is the level a valid level? if (! array_key_exists($level, $this->logLevels)) { throw LogException::forInvalidLogLevel($level); } - // Does the app want to log this right now? if (! in_array($level, $this->loggableLevels, true)) { return; } - // Parse our placeholders $message = $this->interpolate($message, $context); if ($this->cacheLogs) { - $this->logCache[] = [ - 'level' => $level, - 'msg' => $message, - ]; + $this->logCache[] = ['level' => $level, 'msg' => $message]; } foreach ($this->handlerConfig as $className => $config) { @@ -282,17 +261,13 @@ public function log($level, string|Stringable $message, array $context = []): vo $this->handlers[$className] = new $className($config); } - /** - * @var HandlerInterface $handler - */ $handler = $this->handlers[$className]; if (! $handler->canHandle($level)) { continue; } - // If the handler returns false, then we - // don't execute any other handlers. + // If the handler returns false, then we don't execute any other handlers. if (! $handler->setDateFormat($this->dateFormat)->handle($level, $message)) { break; } @@ -311,7 +286,8 @@ public function log($level, string|Stringable $message, array $context = []): vo * {file} * {line} * - * @param string $message + * @param string|Stringable $message + * @param array $context * * @return string */ @@ -321,7 +297,6 @@ protected function interpolate($message, array $context = []) return print_r($message, true); } - // build a replacement array with braces around the context keys $replace = []; foreach ($context as $key => $val) { @@ -335,7 +310,6 @@ protected function interpolate($message, array $context = []) $replace['{' . $key . '}'] = $val; } - // Add special placeholders $replace['{post_vars}'] = '$_POST: ' . print_r($_POST, true); $replace['{get_vars}'] = '$_GET: ' . print_r($_GET, true); $replace['{env}'] = ENVIRONMENT; @@ -362,7 +336,6 @@ protected function interpolate($message, array $context = []) $replace['{session_vars}'] = '$_SESSION: ' . print_r($_SESSION, true); } - // interpolate replacement values into the message and return return strtr($message, $replace); } @@ -370,6 +343,8 @@ protected function interpolate($message, array $context = []) * Determines the file and line that the logging call * was made from by analyzing the backtrace. * Find the earliest stack frame that is part of our logging system. + * + * @return array{string, int|string} */ public function determineFile(): array { @@ -386,28 +361,19 @@ public function determineFile(): array 'notice', ]; - // Generate Backtrace info - $trace = \debug_backtrace(0); + $trace = debug_backtrace(0); - // So we search from the bottom (earliest) of the stack frames - $stackFrames = \array_reverse($trace); + $stackFrames = array_reverse($trace); - // Find the first reference to a Logger class method foreach ($stackFrames as $frame) { - if (\in_array($frame['function'], $logFunctions, true)) { + if (in_array($frame['function'], $logFunctions, true)) { $file = isset($frame['file']) ? clean_path($frame['file']) : 'unknown'; $line = $frame['line'] ?? 'unknown'; - return [ - $file, - $line, - ]; + return [$file, $line]; } } - return [ - 'unknown', - 'unknown', - ]; + return ['unknown', 'unknown']; } } diff --git a/system/Model.php b/system/Model.php index 473584cb8163..44ed52a0edd0 100644 --- a/system/Model.php +++ b/system/Model.php @@ -551,9 +551,8 @@ protected function doOnlyDeleted() * Compiles a replace into string and runs the query * This method works only with dbCalls. * - * @param array|null $row Data - * @phpstan-param row_array|null $row - * @param bool $returnSQL Set to true to return Query String + * @param row_array|null $row Data + * @param bool $returnSQL Set to true to return Query String * * @return BaseResult|false|Query|string */ @@ -774,12 +773,10 @@ protected function shouldUpdate($row): bool * Inserts data into the database. If an object is provided, * it will attempt to convert it to an array. * - * @param array|object|null $row - * @phpstan-param row_array|object|null $row - * @param bool $returnID Whether insert ID should be returned or not. + * @param object|row_array|null $row + * @param bool $returnID Whether insert ID should be returned or not. * - * @return bool|int|string - * @phpstan-return ($returnID is true ? int|string|false : bool) + * @return ($returnID is true ? false|int|string : bool) * * @throws ReflectionException */ diff --git a/system/RESTful/ResourceController.php b/system/RESTful/ResourceController.php index 072b52ee8260..ed72df212b07 100644 --- a/system/RESTful/ResourceController.php +++ b/system/RESTful/ResourceController.php @@ -106,8 +106,7 @@ public function delete($id = null) /** * Set/change the expected response representation for returned objects * - * @param string $format Response format - * @phpstan-param 'json'|'xml' $format + * @param 'json'|'xml' $format Response format * * @return void */ diff --git a/system/Security/Security.php b/system/Security/Security.php index 9e87a2177cda..aa744d9ed343 100644 --- a/system/Security/Security.php +++ b/system/Security/Security.php @@ -427,59 +427,16 @@ public function shouldRedirect(): bool * e.g. file/in/some/approved/folder.txt, you can set the second optional * parameter, $relativePath to TRUE. * + * @deprecated 4.6.2 Use `sanitize_filename()` instead + * * @param string $str Input file name * @param bool $relativePath Whether to preserve paths */ public function sanitizeFilename(string $str, bool $relativePath = false): string { - // List of sanitize filename strings - $bad = [ - '../', - '', - '<', - '>', - "'", - '"', - '&', - '$', - '#', - '{', - '}', - '[', - ']', - '=', - ';', - '?', - '%20', - '%22', - '%3c', - '%253c', - '%3e', - '%0e', - '%28', - '%29', - '%2528', - '%26', - '%24', - '%3f', - '%3b', - '%3d', - ]; - - if (! $relativePath) { - $bad[] = './'; - $bad[] = '/'; - } - - $str = remove_invisible_characters($str, false); - - do { - $old = $str; - $str = str_replace($bad, '', $str); - } while ($old !== $str); + helper('security'); - return stripslashes($str); + return sanitize_filename($str, $relativePath); } /** diff --git a/system/Security/SecurityInterface.php b/system/Security/SecurityInterface.php index 03a5ba2321d0..ebd8919cd109 100644 --- a/system/Security/SecurityInterface.php +++ b/system/Security/SecurityInterface.php @@ -66,6 +66,8 @@ public function shouldRedirect(): bool; * e.g. file/in/some/approved/folder.txt, you can set the second optional * parameter, $relativePath to TRUE. * + * @deprecated 4.6.2 Use `sanitize_filename()` instead + * * @param string $str Input file name * @param bool $relativePath Whether to preserve paths */ diff --git a/system/Test/Interfaces/FabricatorModel.php b/system/Test/Interfaces/FabricatorModel.php index a5860e22fb83..57c1b16cc2dc 100644 --- a/system/Test/Interfaces/FabricatorModel.php +++ b/system/Test/Interfaces/FabricatorModel.php @@ -39,7 +39,7 @@ interface FabricatorModel * * @param int|list|string|null $id One primary key or an array of primary keys * - * @phpstan-return ($id is int|string ? row_array|object|null : list) + * @return ($id is int|string ? object|row_array|null : list) */ public function find($id = null); @@ -47,9 +47,8 @@ public function find($id = null); * Inserts data into the current table. If an object is provided, * it will attempt to convert it to an array. * - * @param array|object|null $row - * @phpstan-param row_array|object|null $row - * @param bool $returnID Whether insert ID should be returned or not. + * @param object|row_array|null $row + * @param bool $returnID Whether insert ID should be returned or not. * * @return bool|int|string * diff --git a/system/Test/Mock/MockBuilder.php b/system/Test/Mock/MockBuilder.php index e947f0ef5fb7..f95f09495a98 100644 --- a/system/Test/Mock/MockBuilder.php +++ b/system/Test/Mock/MockBuilder.php @@ -17,6 +17,9 @@ class MockBuilder extends BaseBuilder { + /** + * @var array + */ protected $supportedIgnoreStatements = [ 'update' => 'IGNORE', 'insert' => 'IGNORE', diff --git a/system/Test/Mock/MockCLIConfig.php b/system/Test/Mock/MockCLIConfig.php index c62df3d9f43a..99e3597d353a 100644 --- a/system/Test/Mock/MockCLIConfig.php +++ b/system/Test/Mock/MockCLIConfig.php @@ -17,14 +17,19 @@ class MockCLIConfig extends App { - public string $baseURL = 'http://example.com/'; - public string $uriProtocol = 'REQUEST_URI'; - public array $proxyIPs = []; - public string $CSRFTokenName = 'csrf_test_name'; - public string $CSRFCookieName = 'csrf_cookie_name'; - public int $CSRFExpire = 7200; - public bool $CSRFRegenerate = true; - public $CSRFExcludeURIs = ['http://example.com']; + public string $baseURL = 'http://example.com/'; + public string $uriProtocol = 'REQUEST_URI'; + public array $proxyIPs = []; + public string $CSRFTokenName = 'csrf_test_name'; + public string $CSRFCookieName = 'csrf_cookie_name'; + public int $CSRFExpire = 7200; + public bool $CSRFRegenerate = true; + + /** + * @var list + */ + public array $CSRFExcludeURIs = ['http://example.com']; + public string $CSRFSameSite = 'Lax'; public bool $CSPEnabled = false; public string $defaultLocale = 'en'; diff --git a/system/Test/Mock/MockCURLRequest.php b/system/Test/Mock/MockCURLRequest.php index 0f0b203a9a14..059b83114927 100644 --- a/system/Test/Mock/MockCURLRequest.php +++ b/system/Test/Mock/MockCURLRequest.php @@ -23,7 +23,14 @@ */ class MockCURLRequest extends CURLRequest { + /** + * @var array + */ public $curl_options; + + /** + * @var string + */ protected $output = ''; /** @@ -38,11 +45,13 @@ public function setOutput($output) return $this; } + /** + * @param array $curlOptions + */ protected function sendRequest(array $curlOptions = []): string { $this->response = clone $this->responseOrig; - // Save so we can access later. $this->curl_options = $curlOptions; return $this->output; diff --git a/system/Test/Mock/MockCache.php b/system/Test/Mock/MockCache.php index b410df874243..67f8aee86ad1 100644 --- a/system/Test/Mock/MockCache.php +++ b/system/Test/Mock/MockCache.php @@ -214,8 +214,9 @@ public function getCacheInfo() /** * Returns detailed information about the specific item in the cache. * - * @return array|null Returns null if the item does not exist, otherwise array - * with at least the 'expire' key for absolute epoch expiry (or null). + * @return array{expire: int|null}|null Returns null if the item does not exist, + * otherwise, array with the 'expire' key for + * absolute epoch expiry (or null). */ public function getMetaData(string $key) { diff --git a/system/Test/Mock/MockConnection.php b/system/Test/Mock/MockConnection.php index 7870d51b42cb..2801a0674ad5 100644 --- a/system/Test/Mock/MockConnection.php +++ b/system/Test/Mock/MockConnection.php @@ -18,6 +18,7 @@ use CodeIgniter\Database\BaseResult; use CodeIgniter\Database\Query; use CodeIgniter\Database\TableName; +use stdClass; /** * @extends BaseConnection @@ -25,7 +26,10 @@ class MockConnection extends BaseConnection { /** - * @var array{connect?: mixed, execute?: bool|object} + * @var array{ + * connect?: object|resource|false|list, + * execute?: object|resource|false, + * } */ protected $returnValues = []; @@ -36,11 +40,18 @@ class MockConnection extends BaseConnection */ protected $schema; + /** + * @var string + */ public $database; + + /** + * @var Query + */ public $lastQuery; /** - * @param mixed $return + * @param false|list|object|resource $return * * @return $this */ @@ -59,14 +70,15 @@ public function shouldReturn(string $method, $return) * Should automatically handle different connections for read/write * queries if needed. * - * @param mixed ...$binds + * @param mixed $binds * - * @return BaseResult|bool|Query + * @return BaseResult|bool|Query * * @todo BC set $queryClass default as null in 4.1 */ public function query(string $sql, $binds = null, bool $setEscapeFlags = true, string $queryClass = '') { + /** @var class-string $queryClass */ $queryClass = str_replace('Connection', 'Query', static::class); $query = new $queryClass($this); @@ -81,23 +93,24 @@ public function query(string $sql, $binds = null, bool $setEscapeFlags = true, s $this->lastQuery = $query; - // Run the query - if (false === ($this->resultID = $this->simpleQuery($query->getQuery()))) { + $this->resultID = $this->simpleQuery($query->getQuery()); + + if ($this->resultID === false) { $query->setDuration($startTime, $startTime); // @todo deal with errors - return false; } $query->setDuration($startTime); // resultID is not false, so it must be successful - if ($query->isWriteType($sql)) { + if ($query->isWriteType()) { return true; } // query is not write-type, so it must be read-type query; return QueryResult + /** @var class-string $resultClass */ $resultClass = str_replace('Connection', 'Result', static::class); return new $resultClass($this->connID, $this->resultID); @@ -106,7 +119,7 @@ public function query(string $sql, $binds = null, bool $setEscapeFlags = true, s /** * Connect to the database. * - * @return mixed + * @return false|object|resource */ public function connect(bool $persistent = false) { @@ -153,7 +166,7 @@ public function getVersion(): string /** * Executes the query against the database. * - * @return bool|object + * @return false|object|resource */ protected function execute(string $sql) { @@ -171,9 +184,7 @@ public function affectedRows(): int /** * Returns the last error code and message. * - * Must return an array with keys 'code' and 'message': - * - * return ['code' => null, 'message' => null); + * @return array{code: int, message: string} */ public function error(): array { @@ -183,9 +194,6 @@ public function error(): array ]; } - /** - * Insert ID - */ public function insertID(): int { return $this->connID->insert_id; @@ -211,16 +219,25 @@ protected function _listColumns($table = ''): string return ''; } + /** + * @return list + */ protected function _fieldData(string $table): array { return []; } + /** + * @return array + */ protected function _indexData(string $table): array { return []; } + /** + * @return array + */ protected function _foreignKeyData(string $table): array { return []; diff --git a/system/Test/Mock/MockEvents.php b/system/Test/Mock/MockEvents.php index bb0b4d9fb8bd..4ed1ea67ccce 100644 --- a/system/Test/Mock/MockEvents.php +++ b/system/Test/Mock/MockEvents.php @@ -18,7 +18,7 @@ class MockEvents extends Events { /** - * @return array + * @return array, 2: list}> */ public function getListeners() { @@ -26,7 +26,7 @@ public function getListeners() } /** - * @return array + * @return list */ public function getEventsFile() { diff --git a/system/Test/Mock/MockFileLogger.php b/system/Test/Mock/MockFileLogger.php index 8737e724b413..5a815b223196 100644 --- a/system/Test/Mock/MockFileLogger.php +++ b/system/Test/Mock/MockFileLogger.php @@ -16,21 +16,24 @@ use CodeIgniter\Log\Handlers\FileHandler; /** - * Class MockFileLogger - * * Extends FileHandler, exposing some inner workings */ class MockFileLogger extends FileHandler { /** * Where would the log be written? + * + * @var string */ public $destination; + /** + * @param array{handles?: list, path?: string, fileExtension?: string, filePermissions?: int} $config + */ public function __construct(array $config) { parent::__construct($config); - $this->handles = $config['handles'] ?? []; + $this->destination = $this->path . 'log-' . date('Y-m-d') . '.' . $this->fileExtension; } } diff --git a/system/Test/Mock/MockInputOutput.php b/system/Test/Mock/MockInputOutput.php index 7f4370a2d22f..6aa4779fb742 100644 --- a/system/Test/Mock/MockInputOutput.php +++ b/system/Test/Mock/MockInputOutput.php @@ -31,16 +31,14 @@ final class MockInputOutput extends InputOutput /** * Output lines. * - * @var array - * @phpstan-var list + * @var list */ private array $outputs = []; /** * Sets user inputs. * - * @param array $inputs - * @phpstan-param list $inputs + * @param list $inputs */ public function setInputs(array $inputs): void { @@ -80,6 +78,8 @@ public function getOutput(?int $index = null): string /** * Returns the outputs array. + * + * @return list */ public function getOutputs(): array { diff --git a/system/Test/Mock/MockLogger.php b/system/Test/Mock/MockLogger.php index 5130b2eabada..411c1a66a6be 100644 --- a/system/Test/Mock/MockLogger.php +++ b/system/Test/Mock/MockLogger.php @@ -13,77 +13,79 @@ namespace CodeIgniter\Test\Mock; +use CodeIgniter\Log\Handlers\HandlerInterface; +use Config\Logger; use Tests\Support\Log\Handlers\TestHandler; -class MockLogger +class MockLogger extends Logger { - /* - |-------------------------------------------------------------------------- - | Error Logging Threshold - |-------------------------------------------------------------------------- - | - | You can enable error logging by setting a threshold over zero. The - | threshold determines what gets logged. Any values below or equal to the - | threshold will be logged. Threshold options are: - | - | 0 = Disables logging, Error logging TURNED OFF - | 1 = Emergency Messages - System is unusable - | 2 = Alert Messages - Action Must Be Taken Immediately - | 3 = Critical Messages - Application component unavailable, unexpected exception. - | 4 = Runtime Errors - Don't need immediate action, but should be monitored. - | 5 = Warnings - Exceptional occurrences that are not errors. - | 6 = Notices - Normal but significant events. - | 7 = Info - Interesting events, like user logging in, etc. - | 8 = Debug - Detailed debug information. - | 9 = All Messages - | - | You can also pass an array with threshold levels to show individual error types - | - | array(1, 2, 3, 8) = Emergency, Alert, Critical, and Debug messages - | - | For a live site you'll usually enable Critical or higher (3) to be logged otherwise - | your log files will fill up very fast. - | + /** + *-------------------------------------------------------------------------- + * Error Logging Threshold + *-------------------------------------------------------------------------- + * + * You can enable error logging by setting a threshold over zero. The + * threshold determines what gets logged. Any values below or equal to the + * threshold will be logged. Threshold options are: + * + * 0 = Disables logging, Error logging TURNED OFF + * 1 = Emergency Messages - System is unusable + * 2 = Alert Messages - Action Must Be Taken Immediately + * 3 = Critical Messages - Application component unavailable, unexpected exception. + * 4 = Runtime Errors - Don't need immediate action, but should be monitored. + * 5 = Warnings - Exceptional occurrences that are not errors. + * 6 = Notices - Normal but significant events. + * 7 = Info - Interesting events, like user logging in, etc. + * 8 = Debug - Detailed debug information. + * 9 = All Messages + * + * You can also pass an array with threshold levels to show individual error types + * + * array(1, 2, 3, 8) = Emergency, Alert, Critical, and Debug messages + * + * For a live site you'll usually enable Critical or higher (3) to be logged otherwise + * your log files will fill up very fast. + * + * @var int|list */ - public $threshold = 9; - /* - |-------------------------------------------------------------------------- - | Date Format for Logs - |-------------------------------------------------------------------------- - | - | Each item that is logged has an associated date. You can use PHP date - | codes to set your own date formatting - | + /** + *-------------------------------------------------------------------------- + * Date Format for Logs + *-------------------------------------------------------------------------- + * + * Each item that is logged has an associated date. You can use PHP date + * codes to set your own date formatting */ - public $dateFormat = 'Y-m-d'; + public string $dateFormat = 'Y-m-d'; - /* - |-------------------------------------------------------------------------- - | Log Handlers - |-------------------------------------------------------------------------- - | - | The logging system supports multiple actions to be taken when something - | is logged. This is done by allowing for multiple Handlers, special classes - | designed to write the log to their chosen destinations, whether that is - | a file on the server, a cloud-based service, or even taking actions such - | as emailing the dev team. - | - | Each handler is defined by the class name used for that handler, and it - | MUST implement the CodeIgniter\Log\Handlers\HandlerInterface interface. - | - | The value of each key is an array of configuration items that are sent - | to the constructor of each handler. The only required configuration item - | is the 'handles' element, which must be an array of integer log levels. - | This is most easily handled by using the constants defined in the - | Psr\Log\LogLevel class. - | - | Handlers are executed in the order defined in this array, starting with - | the handler on top and continuing down. - | + /** + *-------------------------------------------------------------------------- + * Log Handlers + *-------------------------------------------------------------------------- + * + * The logging system supports multiple actions to be taken when something + * is logged. This is done by allowing for multiple Handlers, special classes + * designed to write the log to their chosen destinations, whether that is + * a file on the server, a cloud-based service, or even taking actions such + * as emailing the dev team. + * + * Each handler is defined by the class name used for that handler, and it + * MUST implement the CodeIgniter\Log\Handlers\HandlerInterface interface. + * + * The value of each key is an array of configuration items that are sent + * to the constructor of each handler. The only required configuration item + * is the 'handles' element, which must be an array of integer log levels. + * This is most easily handled by using the constants defined in the + * Psr\Log\LogLevel class. + * + * Handlers are executed in the order defined in this array, starting with + * the handler on top and continuing down. + * + * @var array, array|string>> */ - public $handlers = [ + public array $handlers = [ // File Handler TestHandler::class => [ // The log levels that this handler will handle. diff --git a/system/Test/Mock/MockResult.php b/system/Test/Mock/MockResult.php index 9c5f55e98fdc..bc5cd8ac2054 100644 --- a/system/Test/Mock/MockResult.php +++ b/system/Test/Mock/MockResult.php @@ -31,6 +31,8 @@ public function getFieldCount(): int /** * Generates an array of column names in the result set. + * + * @return array{} */ public function getFieldNames(): array { @@ -39,6 +41,8 @@ public function getFieldNames(): array /** * Generates an array of objects representing field meta-data. + * + * @return array{} */ public function getFieldData(): array { @@ -73,7 +77,7 @@ public function dataSeek($n = 0) * * Overridden by driver classes. * - * @return mixed + * @return array{} */ protected function fetchAssoc() { @@ -83,13 +87,11 @@ protected function fetchAssoc() /** * Returns the result set as an object. * - * Overridden by child classes. - * - * @param string $className + * @param class-string $className * - * @return object|stdClass + * @return object */ - protected function fetchObject($className = 'stdClass') + protected function fetchObject($className = stdClass::class) { return new $className(); } diff --git a/system/Test/Mock/MockServices.php b/system/Test/Mock/MockServices.php index 5d1076a6dffb..c71bb612c6a7 100644 --- a/system/Test/Mock/MockServices.php +++ b/system/Test/Mock/MockServices.php @@ -18,9 +18,16 @@ class MockServices extends BaseService { + /** + * @var array + */ public $psr4 = [ 'Tests/Support' => TESTPATH . '_support/', ]; + + /** + * @var array + */ public $classmap = []; public function __construct() diff --git a/system/Test/Mock/MockSession.php b/system/Test/Mock/MockSession.php index 13eca596c9c7..b581f81dfafb 100644 --- a/system/Test/Mock/MockSession.php +++ b/system/Test/Mock/MockSession.php @@ -30,9 +30,9 @@ class MockSession extends Session * * @var list */ - public $cookies = []; + public array $cookies = []; - public $didRegenerate = false; + public bool $didRegenerate = false; /** * Sets the driver as the session handler in PHP. diff --git a/system/ThirdParty/Escaper/Escaper.php b/system/ThirdParty/Escaper/Escaper.php index 4fce36bd0c6c..39d9b0b1cdac 100644 --- a/system/ThirdParty/Escaper/Escaper.php +++ b/system/ThirdParty/Escaper/Escaper.php @@ -30,7 +30,7 @@ * * @final */ -class Escaper +class Escaper implements EscaperInterface { /** * Entity Map mapping Unicode codepoints to any available named HTML entities. @@ -183,24 +183,13 @@ public function getEncoding() return $this->encoding; } - /** - * Escape a string for the HTML Body context where there are very few characters - * of special meaning. Internally this will use htmlspecialchars(). - * - * @return ($string is non-empty-string ? non-empty-string : string) - */ + /** @inheritDoc */ public function escapeHtml(string $string) { return htmlspecialchars($string, $this->htmlSpecialCharsFlags, $this->encoding); } - /** - * Escape a string for the HTML Attribute context. We use an extended set of characters - * 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 is non-empty-string ? non-empty-string : string) - */ + /** @inheritDoc */ public function escapeHtmlAttr(string $string) { $string = $this->toUtf8($string); @@ -214,17 +203,7 @@ public function escapeHtmlAttr(string $string) return $this->fromUtf8($result); } - /** - * Escape a string for the Javascript context. This does not use json_encode(). An extended - * set of characters are escaped beyond ECMAScript's rules for Javascript literal string - * escaping in order to prevent misinterpretation of Javascript as HTML leading to the - * injection of special characters and entities. The escaping used should be tolerant - * of cases where HTML escaping was not applied on top of Javascript escaping correctly. - * 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 is non-empty-string ? non-empty-string : string) - */ + /** @inheritDoc */ public function escapeJs(string $string) { $string = $this->toUtf8($string); @@ -238,24 +217,13 @@ public function escapeJs(string $string) return $this->fromUtf8($result); } - /** - * Escape a string for the URI or Parameter contexts. This should not be used to escape - * 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 is non-empty-string ? non-empty-string : string) - */ + /** @inheritDoc */ public function escapeUrl(string $string) { return rawurlencode($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 is non-empty-string ? non-empty-string : string) - */ + /** @inheritDoc */ public function escapeCss(string $string) { $string = $this->toUtf8($string); diff --git a/system/ThirdParty/Escaper/EscaperInterface.php b/system/ThirdParty/Escaper/EscaperInterface.php new file mode 100644 index 000000000000..3930db88ac01 --- /dev/null +++ b/system/ThirdParty/Escaper/EscaperInterface.php @@ -0,0 +1,58 @@ +cache->get($cacheName)) { + $output = $this->cache->get($cacheName); + + if (is_string($output) && $output !== '') { return $output; } @@ -116,8 +118,7 @@ public function render(string $library, $params = null, int $ttl = 0, ?string $c * If a string, it should be in the format "key1=value key2=value". * It will be split and returned as an array. * - * @param array|string|null $params - * @phpstan-param array|string|float|null $params + * @param array|float|string|null $params * * @return array */ diff --git a/system/View/Filters.php b/system/View/Filters.php index 0be48b6de1b4..c74e385cdb96 100644 --- a/system/View/Filters.php +++ b/system/View/Filters.php @@ -77,8 +77,8 @@ public static function default($value, string $default): string /** * Escapes the given value with our `esc()` helper function. * - * @param string $value - * @phpstan-param 'html'|'js'|'css'|'url'|'attr'|'raw' $context + * @param string $value + * @param 'attr'|'css'|'html'|'js'|'raw'|'url' $context */ public static function esc($value, string $context = 'html'): string { diff --git a/system/View/Parser.php b/system/View/Parser.php index a2393bb4b7e5..85d0ff8468db 100644 --- a/system/View/Parser.php +++ b/system/View/Parser.php @@ -117,10 +117,14 @@ public function render(string $view, ?array $options = null, ?bool $saveData = n $cacheName = $options['cache_name'] ?? str_replace('.php', '', $view); // Was it cached? - if (isset($options['cache']) && ($output = cache($cacheName))) { - $this->logPerformance($start, microtime(true), $view); + if (isset($options['cache'])) { + $output = cache($cacheName); + + if (is_string($output) && $output !== '') { + $this->logPerformance($start, microtime(true), $view); - return $output; + return $output; + } } $file = $this->viewPath . $view; @@ -198,10 +202,9 @@ public function renderString(string $template, ?array $options = null, ?bool $sa * so that the variable is correctly handled within the * parsing itself, and contexts (including raw) are respected. * - * @param array $data - * @param non-empty-string|null $context The context to escape it for. - * If 'raw', no escaping will happen. - * @phpstan-param null|'html'|'js'|'css'|'url'|'attr'|'raw' $context + * @param array $data + * @param 'attr'|'css'|'html'|'js'|'raw'|'url'|null $context The context to escape it for. + * If 'raw', no escaping will happen. */ public function setData(array $data = [], ?string $context = null): RendererInterface { diff --git a/system/View/RendererInterface.php b/system/View/RendererInterface.php index 5fdf8794f12d..d42dcee88442 100644 --- a/system/View/RendererInterface.php +++ b/system/View/RendererInterface.php @@ -46,10 +46,9 @@ public function renderString(string $view, ?array $options = null, bool $saveDat /** * Sets several pieces of view data at once. * - * @param array $data - * @param non-empty-string|null $context The context to escape it for. - * If 'raw', no escaping will happen. - * @phpstan-param null|'html'|'js'|'css'|'url'|'attr'|'raw' $context + * @param array $data + * @param 'attr'|'css'|'html'|'js'|'raw'|'url'|null $context The context to escape it for. + * If 'raw', no escaping will happen. * * @return RendererInterface */ @@ -58,10 +57,9 @@ public function setData(array $data = [], ?string $context = null); /** * Sets a single piece of view data. * - * @param mixed $value - * @param non-empty-string|null $context The context to escape it for. - * If 'raw', no escaping will happen. - * @phpstan-param null|'html'|'js'|'css'|'url'|'attr'|'raw' $context + * @param mixed $value + * @param 'attr'|'css'|'html'|'js'|'raw'|'url'|null $context The context to escape it for. + * If 'raw', no escaping will happen. * * @return RendererInterface */ diff --git a/system/View/Table.php b/system/View/Table.php index a063e71c5a85..df4b870b8ba9 100644 --- a/system/View/Table.php +++ b/system/View/Table.php @@ -108,8 +108,7 @@ public function __construct($config = []) /** * Set the template * - * @param array $template - * @phpstan-param array|string $template + * @param array|string $template * * @return bool */ diff --git a/system/View/View.php b/system/View/View.php index 6a282096ccb4..184f2528c32c 100644 --- a/system/View/View.php +++ b/system/View/View.php @@ -186,7 +186,9 @@ public function render(string $view, ?array $options = null, ?bool $saveData = n $this->renderVars['cacheName'] = $cacheName; - if ($output = cache($this->renderVars['cacheName'])) { + $output = cache($this->renderVars['cacheName']); + + if (is_string($output) && $output !== '') { $this->logPerformance( $this->renderVars['start'], microtime(true), @@ -335,9 +337,8 @@ public function excerpt(string $string, int $length = 20): string /** * Sets several pieces of view data at once. * - * @param non-empty-string|null $context The context to escape it for. - * If 'raw', no escaping will happen. - * @phpstan-param null|'html'|'js'|'css'|'url'|'attr'|'raw' $context + * @param 'attr'|'css'|'html'|'js'|'raw'|'url'|null $context The context to escape it for. + * If 'raw', no escaping will happen. */ public function setData(array $data = [], ?string $context = null): RendererInterface { @@ -354,10 +355,9 @@ public function setData(array $data = [], ?string $context = null): RendererInte /** * Sets a single piece of view data. * - * @param mixed $value - * @param non-empty-string|null $context The context to escape it for. - * If 'raw', no escaping will happen. - * @phpstan-param null|'html'|'js'|'css'|'url'|'attr'|'raw' $context + * @param mixed $value + * @param 'attr'|'css'|'html'|'js'|'raw'|'url'|null $context The context to escape it for. + * If 'raw', no escaping will happen. */ public function setVar(string $name, $value = null, ?string $context = null): RendererInterface { diff --git a/system/util_bootstrap.php b/system/util_bootstrap.php new file mode 100644 index 000000000000..5423954deb59 --- /dev/null +++ b/system/util_bootstrap.php @@ -0,0 +1,84 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +use CodeIgniter\Boot; +use Config\Paths; + +error_reporting(E_ALL); +ini_set('display_errors', '1'); +ini_set('display_startup_errors', '1'); + +/* + * --------------------------------------------------------------- + * DEFINE ENVIRONMENT + * --------------------------------------------------------------- + * + * As this bootstrap file is primarily used by internal scripts + * across the framework and other CodeIgniter projects, we need + * to make sure it recognizes that we're in development. + */ + +$_SERVER['CI_ENVIRONMENT'] = 'development'; +define('ENVIRONMENT', 'development'); +defined('CI_DEBUG') || define('CI_DEBUG', true); + +/* + * --------------------------------------------------------------- + * SET UP OUR PATH CONSTANTS + * --------------------------------------------------------------- + * + * The path constants provide convenient access to the folders + * throughout the application. We have to set them up here + * so they are available in the config files that are loaded. + */ + +defined('HOMEPATH') || define('HOMEPATH', realpath(rtrim(getcwd(), '\\/ ')) . DIRECTORY_SEPARATOR); + +$source = match (true) { + is_dir(HOMEPATH . 'app/') => HOMEPATH, + is_dir('vendor/codeigniter4/framework/') => 'vendor/codeigniter4/framework/', + is_dir('vendor/codeigniter4/codeigniter4/') => 'vendor/codeigniter4/codeigniter4/', + default => throw new RuntimeException('Unable to determine the source directory.'), +}; + +defined('CONFIGPATH') || define('CONFIGPATH', realpath($source . 'app/Config') . DIRECTORY_SEPARATOR); +defined('PUBLICPATH') || define('PUBLICPATH', realpath($source . 'public') . DIRECTORY_SEPARATOR); +unset($source); + +require CONFIGPATH . 'Paths.php'; +$paths = new Paths(); + +defined('CIPATH') || define('CIPATH', realpath($paths->systemDirectory . '/../') . DIRECTORY_SEPARATOR); +defined('FCPATH') || define('FCPATH', PUBLICPATH); + +if (is_dir($paths->testsDirectory . '/_support/') && ! defined('SUPPORTPATH')) { + define('SUPPORTPATH', realpath($paths->testsDirectory . '/_support/') . DIRECTORY_SEPARATOR); +} + +if (is_dir(HOMEPATH . 'vendor/')) { + define('VENDORPATH', realpath(HOMEPATH . 'vendor/') . DIRECTORY_SEPARATOR); + define('COMPOSER_PATH', (string) realpath(HOMEPATH . 'vendor/autoload.php')); +} + +/* + *--------------------------------------------------------------- + * BOOTSTRAP THE APPLICATION + *--------------------------------------------------------------- + * + * This process sets up the path constants, loads and registers + * our autoloader, along with Composer's, loads our constants + * and fires up an environment-specific bootstrapping. + */ + +require $paths->systemDirectory . '/Boot.php'; +Boot::bootConsole($paths); diff --git a/tests/README.md b/tests/README.md index c0c58040917b..3848a83540dd 100644 --- a/tests/README.md +++ b/tests/README.md @@ -105,7 +105,7 @@ Individual tests can be run by including the relative path to the test file. You can run the tests without running the live database and the live cache tests. ```console -./phpunit --exclude-group DatabaseLive,CacheLive +./phpunit --exclude-group DatabaseLive --exclude-group CacheLive ``` ## Generating Code Coverage diff --git a/tests/_support/Autoloader/FatalLocator.php b/tests/_support/Autoloader/FatalLocator.php index 0b98fddc6eca..438c41480f01 100644 --- a/tests/_support/Autoloader/FatalLocator.php +++ b/tests/_support/Autoloader/FatalLocator.php @@ -34,7 +34,7 @@ class FatalLocator extends FileLocator * * @return false|string The path to the file, or false if not found. */ - public function locateFile(string $file, ?string $folder = null, string $ext = 'php') + public function locateFile(string $file, ?string $folder = null, string $ext = 'php'): false|string { $folder ??= 'null'; diff --git a/tests/_support/Config/Services.php b/tests/_support/Config/Services.php index 712c79004a0b..3cb7c53388c3 100644 --- a/tests/_support/Config/Services.php +++ b/tests/_support/Config/Services.php @@ -31,10 +31,8 @@ class Services extends BaseServices * The URI class provides a way to model and manipulate URIs. * * @param string|null $uri The URI string - * - * @return URI */ - public static function uri(?string $uri = null, bool $getShared = true) + public static function uri(?string $uri = null, bool $getShared = true): URI { // Intercept our test case if ($uri === 'testCanReplaceFrameworkServices') { diff --git a/tests/_support/Entity/Cast/CastPassParameters.php b/tests/_support/Entity/Cast/CastPassParameters.php index 2cbfce0f324b..3f304e3622a0 100644 --- a/tests/_support/Entity/Cast/CastPassParameters.php +++ b/tests/_support/Entity/Cast/CastPassParameters.php @@ -22,10 +22,8 @@ class CastPassParameters extends BaseCast * * @param mixed $value Data * @param array $params Additional param - * - * @return mixed */ - public static function set($value, array $params = []) + public static function set($value, array $params = []): string { return $value . ':' . json_encode($params); } diff --git a/tests/_support/Models/UserModel.php b/tests/_support/Models/UserModel.php index f940b5874ba5..8e3a1b34e13f 100644 --- a/tests/_support/Models/UserModel.php +++ b/tests/_support/Models/UserModel.php @@ -15,6 +15,9 @@ use CodeIgniter\Model; +/** + * @method int affectedRows() + */ class UserModel extends Model { protected $table = 'user'; diff --git a/tests/system/API/ResponseTraitTest.php b/tests/system/API/ResponseTraitTest.php index bc61dd06c81d..0443d90dc391 100644 --- a/tests/system/API/ResponseTraitTest.php +++ b/tests/system/API/ResponseTraitTest.php @@ -17,6 +17,8 @@ use CodeIgniter\Format\FormatterInterface; use CodeIgniter\Format\JSONFormatter; use CodeIgniter\Format\XMLFormatter; +use CodeIgniter\HTTP\RequestInterface; +use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\HTTP\SiteURI; use CodeIgniter\HTTP\UserAgent; use CodeIgniter\Test\CIUnitTestCase; @@ -24,6 +26,7 @@ use CodeIgniter\Test\Mock\MockResponse; use Config\App; use Config\Cookie; +use Config\Services; use PHPUnit\Framework\Attributes\Group; use stdClass; @@ -82,6 +85,12 @@ private function createCookieConfig(): Cookie return $cookie; } + /** + * @param array $userHeaders + * + * @phpstan-assert RequestInterface $this->request + * @phpstan-assert ResponseInterface $this->response + */ private function createRequestAndResponse(string $routePath = '', array $userHeaders = []): void { $config = $this->createAppConfig(); @@ -97,29 +106,28 @@ private function createRequestAndResponse(string $routePath = '', array $userHea $this->response = new MockResponse($config); } - // Insert headers into request. - $headers = [ - 'Accept' => 'text/html', - ]; - $headers = array_merge($headers, $userHeaders); + $headers = array_merge(['Accept' => 'text/html'], $userHeaders); foreach ($headers as $key => $value) { $this->request->setHeader($key, $value); } } + /** + * @param array $userHeaders + */ protected function makeController(string $routePath = '', array $userHeaders = []): object { $this->createRequestAndResponse($routePath, $userHeaders); - // Create the controller class finally. return new class ($this->request, $this->response, $this->formatter) { use ResponseTrait; - protected $formatter; - - public function __construct(protected $request, protected $response, $formatter) - { + public function __construct( + protected RequestInterface $request, + protected ResponseInterface $response, + ?FormatterInterface $formatter, + ) { $this->formatter = $formatter; } @@ -173,11 +181,13 @@ public function testNoFormatterWithStringAsHtmlTrue(): void $controller = new class ($this->request, $this->response, $this->formatter) { use ResponseTrait; - protected $formatter; protected bool $stringAsHtml = true; - public function __construct(protected $request, protected $response, $formatter) - { + public function __construct( + protected RequestInterface $request, + protected ResponseInterface $response, + ?FormatterInterface $formatter, + ) { $this->formatter = $formatter; } }; @@ -291,11 +301,13 @@ public function testRespondSetsCorrectBodyAndStatusWithStringAsHtmlTrue(): void $controller = new class ($this->request, $this->response, $this->formatter) { use ResponseTrait; - protected $formatter; protected bool $stringAsHtml = true; - public function __construct(protected $request, protected $response, $formatter) - { + public function __construct( + protected RequestInterface $request, + protected ResponseInterface $response, + ?FormatterInterface $formatter, + ) { $this->formatter = $formatter; } }; @@ -546,8 +558,8 @@ public function testValidContentTypes(): void private function tryValidContentType(string $mimeType, string $contentType): void { - $original = $_SERVER; - $_SERVER['CONTENT_TYPE'] = $mimeType; + $originalContentType = Services::superglobals()->server('CONTENT_TYPE') ?? ''; + Services::superglobals()->setServer('CONTENT_TYPE', $mimeType); $this->makeController('', ['Accept' => $mimeType]); $this->assertSame( @@ -563,7 +575,7 @@ private function tryValidContentType(string $mimeType, string $contentType): voi 'Response header pre-response...', ); - $_SERVER = $original; + Services::superglobals()->setServer('CONTENT_TYPE', $originalContentType); } public function testValidResponses(): void @@ -609,9 +621,11 @@ public function testFormatByRequestNegotiateIfFormatIsNotJsonOrXML(): void $controller = new class ($request, $response) { use ResponseTrait; - public function __construct(protected $request, protected $response) - { - $this->format = 'txt'; + public function __construct( + protected RequestInterface $request, + protected ResponseInterface $response, + ) { + $this->format = 'txt'; // @phpstan-ignore assign.propertyType (needed for testing) } }; @@ -659,6 +673,9 @@ public function testXMLResponseFormat(): void $this->assertSame($xmlFormatter->format($data), $this->response->getXML()); } + /** + * @param list $args + */ private function invoke(object $controller, string $method, array $args = []): object { $method = self::getPrivateMethodInvoker($controller, $method); diff --git a/tests/system/Autoloader/AutoloaderTest.php b/tests/system/Autoloader/AutoloaderTest.php index af5340400160..cd73356958fe 100644 --- a/tests/system/Autoloader/AutoloaderTest.php +++ b/tests/system/Autoloader/AutoloaderTest.php @@ -394,7 +394,7 @@ public function testAutoloaderLoadsNonClassFiles(): void $this->assertTrue(function_exists('autoload_foo')); $this->assertSame('I am autoloaded by Autoloader through $files!', autoload_foo()); - $this->assertTrue(defined('AUTOLOAD_CONSTANT')); + $this->assertTrue(defined('AUTOLOAD_CONSTANT')); // @phpstan-ignore method.alreadyNarrowedType $this->assertSame('foo', AUTOLOAD_CONSTANT); $loader->unregister(); diff --git a/tests/system/Cache/AbstractFactoriesCacheHandlerTestCase.php b/tests/system/Cache/AbstractFactoriesCacheHandlerTestCase.php new file mode 100644 index 000000000000..7a75239c5b03 --- /dev/null +++ b/tests/system/Cache/AbstractFactoriesCacheHandlerTestCase.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Cache; + +use CodeIgniter\Cache\FactoriesCache\FileVarExportHandler; +use CodeIgniter\Config\Factories; +use CodeIgniter\Test\CIUnitTestCase; +use Config\App; + +/** + * @internal + */ +abstract class AbstractFactoriesCacheHandlerTestCase extends CIUnitTestCase +{ + protected FactoriesCache $cache; + protected CacheInterface|FileVarExportHandler $handler; + + abstract protected function createFactoriesCache(): void; + + public function testInstantiate(): void + { + $this->createFactoriesCache(); + $this->assertInstanceOf(FactoriesCache::class, $this->cache); + } + + public function testSave(): void + { + Factories::reset(); + Factories::config('App'); + + $this->createFactoriesCache(); + + $this->cache->save('config'); + + $cachedData = $this->handler->get('FactoriesCache_config'); + + $this->assertIsArray($cachedData); + $this->assertArrayHasKey('aliases', $cachedData); + $this->assertArrayHasKey('instances', $cachedData); + $this->assertArrayHasKey('App', $cachedData['aliases']); + } + + public function testLoad(): void + { + Factories::reset(); + + /** @var App $appConfig */ + $appConfig = Factories::config('App'); + + $appConfig->baseURL = 'http://test.example.jp/this-is-test/'; + + $this->createFactoriesCache(); + $this->cache->save('config'); + + Factories::reset(); + $this->cache->load('config'); + + /** @var App $appConfig */ + $appConfig = Factories::config('App'); + $this->assertSame('http://test.example.jp/this-is-test/', $appConfig->baseURL); + } + + public function testDelete(): void + { + $this->createFactoriesCache(); + + $this->cache->delete('config'); + + $this->assertFalse($this->cache->load('config')); + } +} diff --git a/tests/system/Cache/CacheFactoryTest.php b/tests/system/Cache/CacheFactoryTest.php index 8a76462b2cea..c51dce6830c3 100644 --- a/tests/system/Cache/CacheFactoryTest.php +++ b/tests/system/Cache/CacheFactoryTest.php @@ -26,15 +26,12 @@ final class CacheFactoryTest extends CIUnitTestCase { private static string $directory = 'CacheFactory'; - private CacheFactory $cacheFactory; private Cache $config; protected function setUp(): void { parent::setUp(); - $this->cacheFactory = new CacheFactory(); - // Initialize path $this->config = new Cache(); $this->config->file['storePath'] .= self::$directory; @@ -48,11 +45,6 @@ protected function tearDown(): void } } - public function testNew(): void - { - $this->assertInstanceOf(CacheFactory::class, $this->cacheFactory); - } - public function testGetHandlerExceptionCacheInvalidHandlers(): void { $this->expectException(CacheException::class); @@ -60,7 +52,7 @@ public function testGetHandlerExceptionCacheInvalidHandlers(): void $this->config->validHandlers = []; - $this->cacheFactory->getHandler($this->config); + CacheFactory::getHandler($this->config); } public function testGetHandlerExceptionCacheHandlerNotFound(): void @@ -70,7 +62,7 @@ public function testGetHandlerExceptionCacheHandlerNotFound(): void unset($this->config->validHandlers[$this->config->handler]); - $this->cacheFactory->getHandler($this->config); + CacheFactory::getHandler($this->config); } public function testGetDummyHandler(): void @@ -81,7 +73,7 @@ public function testGetDummyHandler(): void $this->config->handler = 'dummy'; - $this->assertInstanceOf(DummyHandler::class, $this->cacheFactory->getHandler($this->config)); + $this->assertInstanceOf(DummyHandler::class, CacheFactory::getHandler($this->config)); // Initialize path $this->config = new Cache(); @@ -99,7 +91,7 @@ public function testHandlesBadHandler(): void if (is_windows()) { $this->markTestSkipped('Cannot test this properly on Windows.'); } else { - $this->assertInstanceOf(DummyHandler::class, $this->cacheFactory->getHandler($this->config, 'wincache', 'wincache')); + $this->assertInstanceOf(DummyHandler::class, CacheFactory::getHandler($this->config, 'wincache', 'wincache')); } // Initialize path diff --git a/tests/system/Cache/CacheMockTest.php b/tests/system/Cache/CacheMockTest.php index c867bebc4249..9fe4538c2760 100644 --- a/tests/system/Cache/CacheMockTest.php +++ b/tests/system/Cache/CacheMockTest.php @@ -35,6 +35,7 @@ public function testMockReturnsMockCacheClass(): void public function testMockCaching(): void { + /** @var MockCache $mock */ $mock = mock(CacheFactory::class); // Ensure it stores the value normally diff --git a/tests/system/Cache/FactoriesCacheFileHandlerTest.php b/tests/system/Cache/FactoriesCacheFileHandlerTest.php index 01e3276733e3..660c476cc799 100644 --- a/tests/system/Cache/FactoriesCacheFileHandlerTest.php +++ b/tests/system/Cache/FactoriesCacheFileHandlerTest.php @@ -13,24 +13,18 @@ namespace CodeIgniter\Cache; -use CodeIgniter\Cache\FactoriesCache\FileVarExportHandler; -use Config\Cache as CacheConfig; +use Config\Cache; use PHPUnit\Framework\Attributes\Group; /** * @internal */ #[Group('Others')] -final class FactoriesCacheFileHandlerTest extends FactoriesCacheFileVarExportHandlerTest +final class FactoriesCacheFileHandlerTest extends AbstractFactoriesCacheHandlerTestCase { - /** - * @var CacheInterface|FileVarExportHandler - */ - protected $handler; - protected function createFactoriesCache(): void { - $this->handler = CacheFactory::getHandler(new CacheConfig(), 'file'); + $this->handler = CacheFactory::getHandler(new Cache(), 'file'); $this->cache = new FactoriesCache($this->handler); } } diff --git a/tests/system/Cache/FactoriesCacheFileVarExportHandlerTest.php b/tests/system/Cache/FactoriesCacheFileVarExportHandlerTest.php index 8bf5c79d9209..c71c756f5cdd 100644 --- a/tests/system/Cache/FactoriesCacheFileVarExportHandlerTest.php +++ b/tests/system/Cache/FactoriesCacheFileVarExportHandlerTest.php @@ -14,78 +14,17 @@ namespace CodeIgniter\Cache; use CodeIgniter\Cache\FactoriesCache\FileVarExportHandler; -use CodeIgniter\Config\Factories; -use CodeIgniter\Test\CIUnitTestCase; -use Config\App; use PHPUnit\Framework\Attributes\Group; /** * @internal - * @no-final */ #[Group('Others')] -class FactoriesCacheFileVarExportHandlerTest extends CIUnitTestCase +final class FactoriesCacheFileVarExportHandlerTest extends AbstractFactoriesCacheHandlerTestCase { - protected FactoriesCache $cache; - - /** - * @var CacheInterface|FileVarExportHandler - */ - protected $handler; - protected function createFactoriesCache(): void { $this->handler = new FileVarExportHandler(); $this->cache = new FactoriesCache($this->handler); } - - public function testInstantiate(): void - { - $this->createFactoriesCache(); - - $this->assertInstanceOf(FactoriesCache::class, $this->cache); - } - - public function testSave(): void - { - Factories::reset(); - Factories::config('App'); - - $this->createFactoriesCache(); - - $this->cache->save('config'); - - $cachedData = $this->handler->get('FactoriesCache_config'); - - $this->assertArrayHasKey('aliases', $cachedData); - $this->assertArrayHasKey('instances', $cachedData); - $this->assertArrayHasKey('App', $cachedData['aliases']); - } - - public function testLoad(): void - { - Factories::reset(); - /** @var App $appConfig */ - $appConfig = Factories::config('App'); - $appConfig->baseURL = 'http://test.example.jp/this-is-test/'; - - $this->createFactoriesCache(); - $this->cache->save('config'); - - Factories::reset(); - - $this->cache->load('config'); - - $appConfig = Factories::config('App'); - $this->assertSame('http://test.example.jp/this-is-test/', $appConfig->baseURL); - } - - public function testDelete(): void - { - $this->createFactoriesCache(); - - $this->cache->delete('config'); - - $this->assertFalse($this->cache->load('config')); - } } diff --git a/tests/system/Cache/Handlers/BaseHandlerTest.php b/tests/system/Cache/Handlers/BaseHandlerTest.php index 06d3c4a98f8e..4cba697e5e00 100644 --- a/tests/system/Cache/Handlers/BaseHandlerTest.php +++ b/tests/system/Cache/Handlers/BaseHandlerTest.php @@ -25,11 +25,8 @@ #[Group('Others')] final class BaseHandlerTest extends CIUnitTestCase { - /** - * @param mixed $input - */ #[DataProvider('provideValidateKeyInvalidType')] - public function testValidateKeyInvalidType($input): void + public function testValidateKeyInvalidType(mixed $input): void { $this->expectException('InvalidArgumentException'); $this->expectExceptionMessage('Cache key must be a string'); @@ -37,14 +34,17 @@ public function testValidateKeyInvalidType($input): void BaseHandler::validateKey($input); } + /** + * @return iterable + */ public static function provideValidateKeyInvalidType(): iterable { - return [ - [true], - [false], - [null], - [42], - [new stdClass()], + yield from [ + 'true' => [true], + 'false' => [false], + 'null' => [null], + 'int' => [42], + 'object' => [new stdClass()], ]; } diff --git a/tests/system/Cache/Handlers/DummyHandlerTest.php b/tests/system/Cache/Handlers/DummyHandlerTest.php index c9c8a0fa0937..ab3b3ccd643d 100644 --- a/tests/system/Cache/Handlers/DummyHandlerTest.php +++ b/tests/system/Cache/Handlers/DummyHandlerTest.php @@ -13,7 +13,9 @@ namespace CodeIgniter\Cache\Handlers; +use CodeIgniter\Cache\CacheFactory; use CodeIgniter\Test\CIUnitTestCase; +use Config\Cache; use PHPUnit\Framework\Attributes\Group; /** @@ -26,8 +28,7 @@ final class DummyHandlerTest extends CIUnitTestCase protected function setUp(): void { - $this->handler = new DummyHandler(); - $this->handler->initialize(); + $this->handler = CacheFactory::getHandler(new Cache(), 'dummy'); } public function testNew(): void diff --git a/tests/system/Cache/Handlers/FileHandlerTest.php b/tests/system/Cache/Handlers/FileHandlerTest.php index 4c45a0712f64..0819a5a9df06 100644 --- a/tests/system/Cache/Handlers/FileHandlerTest.php +++ b/tests/system/Cache/Handlers/FileHandlerTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Cache\Handlers; +use CodeIgniter\Cache\CacheFactory; use CodeIgniter\Cache\Exceptions\CacheException; use CodeIgniter\CLI\CLI; use CodeIgniter\I18n\Time; @@ -58,8 +59,7 @@ protected function setUp(): void mkdir($this->config->file['storePath'], 0777, true); } - $this->handler = new FileHandler($this->config); - $this->handler->initialize(); + $this->handler = CacheFactory::getHandler($this->config, 'file'); } protected function tearDown(): void @@ -98,19 +98,16 @@ public function testNewWithNonWritablePath(): void $this->expectException(CacheException::class); chmod($this->config->file['storePath'], 0444); - new FileHandler($this->config); + CacheFactory::getHandler($this->config, 'file'); } public function testSetDefaultPath(): void { // Initialize path - $config = new Cache(); - $config->file['storePath'] = null; + $config = new Cache(); + unset($config->file['storePath']); - $this->handler = new FileHandler($config); - $this->handler->initialize(); - - $this->assertInstanceOf(FileHandler::class, $this->handler); + $this->assertInstanceOf(FileHandler::class, CacheFactory::getHandler($config, 'file')); } /** @@ -245,9 +242,8 @@ public function testIncrement(): void public function testIncrementWithDefaultPrefix(): void { $this->config->prefix = 'test_'; - $this->handler = new FileHandler($this->config); - $this->handler->initialize(); + $this->handler = CacheFactory::getHandler($this->config, 'file'); $this->handler->save(self::$key1, 1); $this->handler->save(self::$key2, 'value'); @@ -320,9 +316,7 @@ public function testSaveMode(int $int, string $string): void $config = new Cache(); $config->file['mode'] = $int; - $this->handler = new FileHandler($config); - $this->handler->initialize(); - + $this->handler = CacheFactory::getHandler($config, 'file'); $this->handler->save(self::$key1, 'value'); $file = $config->file['storePath'] . DIRECTORY_SEPARATOR . self::$key1; @@ -346,10 +340,17 @@ public static function provideSaveMode(): iterable public function testFileHandler(): void { - $fileHandler = new BaseTestFileHandler(); + $cache = new Cache(); - $actual = $fileHandler->getFileInfoTest(); + $cache->validHandlers['file'] = BaseTestFileHandler::class; + /** @var BaseTestFileHandler $fileHandler */ + $fileHandler = CacheFactory::getHandler($cache, 'file'); + $this->assertInstanceOf(BaseTestFileHandler::class, $fileHandler); + + $actual = $fileHandler->getFileInfoTest(); + $this->assertIsArray($actual); + $this->assertArrayHasKey('name', $actual); $this->assertArrayHasKey('server_path', $actual); $this->assertArrayHasKey('size', $actual); $this->assertArrayHasKey('date', $actual); @@ -363,8 +364,35 @@ public function testGetMetaDataMiss(): void { $this->assertFalse($this->handler->getMetaData(self::$dummy)); } + + #[RequiresOperatingSystem('Linux|Darwin')] + public function testGetUnreadableFile(): void + { + $this->handler->save(self::$key1, 'value'); + + $filePath = $this->config->file['storePath'] . DIRECTORY_SEPARATOR . $this->config->prefix . self::$key1; + + // Make the file unreadable + chmod($filePath, 0000); + + $this->assertNull($this->handler->get(self::$key1)); + } + + public function testGetItemWithCorruptedData(): void + { + $filePath = $this->config->file['storePath'] . DIRECTORY_SEPARATOR . $this->config->prefix . self::$key1; + + file_put_contents($filePath, 'corrupted_serialized_data_that_cannot_be_unserialized'); + + $this->assertFileExists($filePath); + + $this->assertNull($this->handler->get(self::$key1)); + } } +/** + * @internal + */ final class BaseTestFileHandler extends FileHandler { private static string $directory = 'FileHandler'; @@ -381,7 +409,16 @@ public function __construct() } /** - * @return array|null + * @return array{ + * name: string, + * server_path: string, + * size: int, + * date: int, + * readable: bool, + * writable: bool, + * executable: bool, + * fileperms: int, + * }|null */ public function getFileInfoTest(): ?array { diff --git a/tests/system/Cache/Handlers/MemcachedHandlerTest.php b/tests/system/Cache/Handlers/MemcachedHandlerTest.php index c6cfdb8543ee..f4114d4742e7 100644 --- a/tests/system/Cache/Handlers/MemcachedHandlerTest.php +++ b/tests/system/Cache/Handlers/MemcachedHandlerTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Cache\Handlers; +use CodeIgniter\Cache\CacheFactory; use CodeIgniter\CLI\CLI; use CodeIgniter\Exceptions\BadMethodCallException; use CodeIgniter\I18n\Time; @@ -45,11 +46,7 @@ protected function setUp(): void $this->markTestSkipped('Memcached extension not loaded.'); } - $config = new Cache(); - - $this->handler = new MemcachedHandler($config); - - $this->handler->initialize(); + $this->handler = CacheFactory::getHandler(new Cache(), 'memcached'); } protected function tearDown(): void @@ -139,9 +136,8 @@ public function testIncrement(): void $config = new Cache(); $config->memcached['raw'] = true; - $memcachedHandler = new MemcachedHandler($config); - $memcachedHandler->initialize(); + $memcachedHandler = CacheFactory::getHandler($config, 'memcached'); $memcachedHandler->save(self::$key1, 1); $memcachedHandler->save(self::$key2, 'value'); @@ -158,9 +154,8 @@ public function testDecrement(): void $config = new Cache(); $config->memcached['raw'] = true; - $memcachedHandler = new MemcachedHandler($config); - $memcachedHandler->initialize(); + $memcachedHandler = CacheFactory::getHandler($config, 'memcached'); $memcachedHandler->save(self::$key1, 10); $memcachedHandler->save(self::$key2, 'value'); diff --git a/tests/system/Cache/Handlers/PredisHandlerTest.php b/tests/system/Cache/Handlers/PredisHandlerTest.php index 4b009582cd55..eec292f280ad 100644 --- a/tests/system/Cache/Handlers/PredisHandlerTest.php +++ b/tests/system/Cache/Handlers/PredisHandlerTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Cache\Handlers; +use CodeIgniter\Cache\CacheFactory; use CodeIgniter\CLI\CLI; use CodeIgniter\I18n\Time; use Config\Cache; @@ -42,11 +43,8 @@ protected function setUp(): void { parent::setUp(); - $this->config = new Cache(); - - $this->handler = new PredisHandler($this->config); - - $this->handler->initialize(); + $this->config = new Cache(); + $this->handler = CacheFactory::getHandler($this->config, 'predis'); } protected function tearDown(): void @@ -63,9 +61,7 @@ public function testNew(): void public function testDestruct(): void { - $this->handler = new PredisHandler($this->config); - $this->handler->initialize(); - + $this->handler = CacheFactory::getHandler($this->config, 'predis'); $this->assertInstanceOf(PredisHandler::class, $this->handler); } diff --git a/tests/system/Cache/Handlers/RedisHandlerTest.php b/tests/system/Cache/Handlers/RedisHandlerTest.php index 812985e65e8d..467abc675372 100644 --- a/tests/system/Cache/Handlers/RedisHandlerTest.php +++ b/tests/system/Cache/Handlers/RedisHandlerTest.php @@ -48,11 +48,8 @@ protected function setUp(): void $this->markTestSkipped('redis extension not loaded.'); } - $this->config = new Cache(); - - $this->handler = new RedisHandler($this->config); - - $this->handler->initialize(); + $this->config = new Cache(); + $this->handler = CacheFactory::getHandler($this->config, 'redis'); } protected function tearDown(): void @@ -69,9 +66,7 @@ public function testNew(): void public function testDestruct(): void { - $this->handler = new RedisHandler($this->config); - $this->handler->initialize(); - + $this->handler = CacheFactory::getHandler($this->config, 'redis'); $this->assertInstanceOf(RedisHandler::class, $this->handler); } diff --git a/tests/system/Cache/ResponseCacheTest.php b/tests/system/Cache/ResponseCacheTest.php index 11c82db71247..b3f0ec832905 100644 --- a/tests/system/Cache/ResponseCacheTest.php +++ b/tests/system/Cache/ResponseCacheTest.php @@ -21,8 +21,9 @@ use CodeIgniter\HTTP\SiteURI; use CodeIgniter\HTTP\UserAgent; use CodeIgniter\Test\CIUnitTestCase; -use Config\App as AppConfig; -use Config\Cache as CacheConfig; +use CodeIgniter\Test\Mock\MockCache; +use Config\App; +use Config\Cache; use ErrorException; use PHPUnit\Framework\Attributes\BackupGlobals; use PHPUnit\Framework\Attributes\Group; @@ -34,138 +35,112 @@ #[Group('Others')] final class ResponseCacheTest extends CIUnitTestCase { - private AppConfig $appConfig; - - protected function setUp(): void + /** + * @param array $query + */ + private function createIncomingRequest(string $uri = '', array $query = [], App $app = new App()): IncomingRequest { - parent::setUp(); - - $this->appConfig = new AppConfig(); - } + $superglobals = service('superglobals'); + $superglobals->setServer('REQUEST_URI', sprintf('/%s%s', $uri, $query !== [] ? '?' . http_build_query($query) : '')); + $superglobals->setServer('SCRIPT_NAME', '/index.php'); - private function createIncomingRequest( - string $uri = '', - array $query = [], - ?AppConfig $appConfig = null, - ): IncomingRequest { - $_POST = $_GET = $_SERVER = $_REQUEST = $_ENV = $_COOKIE = $_SESSION = []; + $siteUri = new SiteURI($app, $uri); - $_SERVER['REQUEST_URI'] = '/' . $uri . ($query !== [] ? '?' . http_build_query($query) : ''); - $_SERVER['SCRIPT_NAME'] = '/index.php'; - - $appConfig ??= $this->appConfig; - - $siteUri = new SiteURI($appConfig, $uri); if ($query !== []) { - $_GET = $_REQUEST = $query; $siteUri->setQueryArray($query); } - return new IncomingRequest( - $appConfig, - $siteUri, - null, - new UserAgent(), - ); + return new IncomingRequest($app, $siteUri, null, new UserAgent()); } /** * @param list $params */ - private function createCLIRequest(array $params = [], ?AppConfig $appConfig = null): CLIRequest + private function createCLIRequest(array $params = [], App $app = new App()): CLIRequest { - $_POST = $_GET = $_SERVER = $_REQUEST = $_ENV = $_COOKIE = $_SESSION = []; + $_SERVER['argv'] = ['public/index.php', ...$params]; - $_SERVER['argv'] = ['public/index.php', ...$params]; - $_SERVER['SCRIPT_NAME'] = 'public/index.php'; + $superglobals = service('superglobals'); + $superglobals->setServer('SCRIPT_NAME', 'public/index.php'); - $appConfig ??= $this->appConfig; - - return new CLIRequest($appConfig); + return new CLIRequest($app); } - private function createResponseCache(?CacheConfig $cacheConfig = null): ResponseCache + private function createResponseCache(Cache $cache = new Cache()): ResponseCache { - $cache = mock(CacheFactory::class); - - $cacheConfig ??= new CacheConfig(); + /** @var MockCache $mockCache */ + $mockCache = mock(CacheFactory::class); - return (new ResponseCache($cacheConfig, $cache))->setTtl(300); + return (new ResponseCache($cache, $mockCache))->setTtl(300); } public function testCachePageIncomingRequest(): void { $pageCache = $this->createResponseCache(); - $request = $this->createIncomingRequest('foo/bar'); - - $response = new Response($this->appConfig); + $response = new Response(new App()); $response->setHeader('ETag', 'abcd1234'); $response->setBody('The response body.'); - $return = $pageCache->make($request, $response); - - $this->assertTrue($return); + $this->assertTrue($pageCache->make( + $this->createIncomingRequest('foo/bar'), + $response, + )); // Check cache with a request with the same URI path. - $request = $this->createIncomingRequest('foo/bar'); - $cachedResponse = $pageCache->get($request, new Response($this->appConfig)); - + $cachedResponse = $pageCache->get($this->createIncomingRequest('foo/bar'), new Response(new App())); $this->assertInstanceOf(ResponseInterface::class, $cachedResponse); $this->assertSame('The response body.', $cachedResponse->getBody()); $this->assertSame('abcd1234', $cachedResponse->getHeaderLine('ETag')); // Check cache with a request with the same URI path and different query string. - $request = $this->createIncomingRequest('foo/bar', ['foo' => 'bar', 'bar' => 'baz']); - $cachedResponse = $pageCache->get($request, new Response($this->appConfig)); - + $cachedResponse = $pageCache->get( + $this->createIncomingRequest('foo/bar', ['foo' => 'bar', 'bar' => 'baz']), + new Response(new App()), + ); $this->assertInstanceOf(ResponseInterface::class, $cachedResponse); $this->assertSame('The response body.', $cachedResponse->getBody()); $this->assertSame('abcd1234', $cachedResponse->getHeaderLine('ETag')); // Check cache with another request with the different URI path. - $request = $this->createIncomingRequest('another'); - - $cachedResponse = $pageCache->get($request, new Response($this->appConfig)); - + $cachedResponse = $pageCache->get($this->createIncomingRequest('another'), new Response(new App())); $this->assertNotInstanceOf(ResponseInterface::class, $cachedResponse); } public function testCachePageIncomingRequestWithCacheQueryString(): void { - $cacheConfig = new CacheConfig(); - $cacheConfig->cacheQueryString = true; - $pageCache = $this->createResponseCache($cacheConfig); + $cache = new Cache(); + + $cache->cacheQueryString = true; + + $pageCache = $this->createResponseCache($cache); $request = $this->createIncomingRequest('foo/bar', ['foo' => 'bar', 'bar' => 'baz']); - $response = new Response($this->appConfig); + $response = new Response(new App()); $response->setHeader('ETag', 'abcd1234'); $response->setBody('The response body.'); - $return = $pageCache->make($request, $response); - - $this->assertTrue($return); + $this->assertTrue($pageCache->make($request, $response)); // Check cache with a request with the same URI path and same query string. - $this->createIncomingRequest('foo/bar', ['foo' => 'bar', 'bar' => 'baz']); - $cachedResponse = $pageCache->get($request, new Response($this->appConfig)); - + $cachedResponse = $pageCache->get( + $this->createIncomingRequest('foo/bar', ['foo' => 'bar', 'bar' => 'baz']), + new Response(new App()), + ); $this->assertInstanceOf(ResponseInterface::class, $cachedResponse); $this->assertSame('The response body.', $cachedResponse->getBody()); $this->assertSame('abcd1234', $cachedResponse->getHeaderLine('ETag')); // Check cache with a request with the same URI path and different query string. - $request = $this->createIncomingRequest('foo/bar', ['xfoo' => 'bar', 'bar' => 'baz']); - $cachedResponse = $pageCache->get($request, new Response($this->appConfig)); - + $cachedResponse = $pageCache->get( + $this->createIncomingRequest('foo/bar', ['xfoo' => 'bar', 'bar' => 'baz']), + new Response(new App()), + ); $this->assertNotInstanceOf(ResponseInterface::class, $cachedResponse); // Check cache with another request with the different URI path. - $request = $this->createIncomingRequest('another'); - - $cachedResponse = $pageCache->get($request, new Response($this->appConfig)); - + $cachedResponse = $pageCache->get($this->createIncomingRequest('another'), new Response(new App())); $this->assertNotInstanceOf(ResponseInterface::class, $cachedResponse); } @@ -173,19 +148,16 @@ public function testCachePageIncomingRequestWithHttpMethods(): void { $pageCache = $this->createResponseCache(); - $request = $this->createIncomingRequest('foo/bar'); - - $response = new Response($this->appConfig); + $response = new Response(new App()); $response->setBody('The response body.'); - $return = $pageCache->make($request, $response); - - $this->assertTrue($return); + $this->assertTrue($pageCache->make($this->createIncomingRequest('foo/bar'), $response)); // Check cache with a request with the same URI path and different HTTP method - $request = $this->createIncomingRequest('foo/bar')->withMethod('POST'); - $cachedResponse = $pageCache->get($request, new Response($this->appConfig)); - + $cachedResponse = $pageCache->get( + $this->createIncomingRequest('foo/bar')->withMethod('POST'), + new Response(new App()), + ); $this->assertNotInstanceOf(ResponseInterface::class, $cachedResponse); } @@ -193,27 +165,18 @@ public function testCachePageCLIRequest(): void { $pageCache = $this->createResponseCache(); - $request = $this->createCLIRequest(['foo', 'bar']); - - $response = new Response($this->appConfig); + $response = new Response(new App()); $response->setBody('The response body.'); - $return = $pageCache->make($request, $response); - - $this->assertTrue($return); + $this->assertTrue($pageCache->make($this->createCLIRequest(['foo', 'bar']), $response)); // Check cache with a request with the same params. - $request = $this->createCLIRequest(['foo', 'bar']); - $cachedResponse = $pageCache->get($request, new Response($this->appConfig)); - + $cachedResponse = $pageCache->get($this->createCLIRequest(['foo', 'bar']), new Response(new App())); $this->assertInstanceOf(ResponseInterface::class, $cachedResponse); $this->assertSame('The response body.', $cachedResponse->getBody()); // Check cache with another request with the different params. - $request = $this->createCLIRequest(['baz']); - - $cachedResponse = $pageCache->get($request, new Response($this->appConfig)); - + $cachedResponse = $pageCache->get($this->createCLIRequest(['baz']), new Response(new App())); $this->assertNotInstanceOf(ResponseInterface::class, $cachedResponse); } @@ -222,13 +185,13 @@ public function testUnserializeError(): void $this->expectException(ErrorException::class); $this->expectExceptionMessage('unserialize(): Error at offset 0 of 12 bytes'); - $cache = mock(CacheFactory::class); - $cacheConfig = new CacheConfig(); - $pageCache = new ResponseCache($cacheConfig, $cache); + /** @var MockCache $mockCache */ + $mockCache = mock(CacheFactory::class); + $pageCache = new ResponseCache(new Cache(), $mockCache); $request = $this->createIncomingRequest('foo/bar'); - $response = new Response($this->appConfig); + $response = new Response(new App()); $response->setHeader('ETag', 'abcd1234'); $response->setBody('The response body.'); @@ -237,10 +200,10 @@ public function testUnserializeError(): void $cacheKey = $pageCache->generateCacheKey($request); // Save invalid data. - $cache->save($cacheKey, 'Invalid data'); + $mockCache->save($cacheKey, 'Invalid data'); // Check cache with a request with the same URI path. - $pageCache->get($request, new Response($this->appConfig)); + $pageCache->get($request, new Response(new App())); } public function testInvalidCacheError(): void @@ -248,13 +211,13 @@ public function testInvalidCacheError(): void $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Error unserializing page cache'); - $cache = mock(CacheFactory::class); - $cacheConfig = new CacheConfig(); - $pageCache = new ResponseCache($cacheConfig, $cache); + /** @var MockCache $mockCache */ + $mockCache = mock(CacheFactory::class); + $pageCache = new ResponseCache(new Cache(), $mockCache); $request = $this->createIncomingRequest('foo/bar'); - $response = new Response($this->appConfig); + $response = new Response(new App()); $response->setHeader('ETag', 'abcd1234'); $response->setBody('The response body.'); @@ -263,9 +226,9 @@ public function testInvalidCacheError(): void $cacheKey = $pageCache->generateCacheKey($request); // Save invalid data. - $cache->save($cacheKey, serialize(['a' => '1'])); + $mockCache->save($cacheKey, serialize(['a' => '1'])); // Check cache with a request with the same URI path. - $pageCache->get($request, new Response($this->appConfig)); + $pageCache->get($request, new Response(new App())); } } diff --git a/tests/system/CodeIgniterTest.php b/tests/system/CodeIgniterTest.php index df2f217eb68a..a4216391b7ee 100644 --- a/tests/system/CodeIgniterTest.php +++ b/tests/system/CodeIgniterTest.php @@ -77,7 +77,7 @@ public function testRunEmptyDefaultRoute(): void $this->codeigniter->run(); $output = ob_get_clean(); - $this->assertStringContainsString('Welcome to CodeIgniter', $output); + $this->assertStringContainsString('Welcome to CodeIgniter', (string) $output); } public function testOutputBufferingControl(): void @@ -98,7 +98,7 @@ public function testRunEmptyDefaultRouteReturnResponse(): void $response = $this->codeigniter->run(null, true); $this->assertInstanceOf(ResponseInterface::class, $response); - $this->assertStringContainsString('Welcome to CodeIgniter', $response->getBody()); + $this->assertStringContainsString('Welcome to CodeIgniter', (string) $response->getBody()); } public function testRunClosureRoute(): void @@ -121,7 +121,7 @@ public function testRunClosureRoute(): void $this->codeigniter->run(); $output = ob_get_clean(); - $this->assertStringContainsString('You want to see "about" page.', $output); + $this->assertStringContainsString('You want to see "about" page.', (string) $output); } /** @@ -144,7 +144,7 @@ public function testRun404Override(): void $this->codeigniter->run($routes); $output = ob_get_clean(); - $this->assertStringContainsString("Can't find a route for 'GET: pages/about'.", $output); + $this->assertStringContainsString("Can't find a route for 'GET: pages/about'.", (string) $output); $this->assertSame(404, response()->getStatusCode()); } @@ -163,7 +163,7 @@ public function testRun404OverrideControllerReturnsResponse(): void $response = $this->codeigniter->run($routes, true); $this->assertInstanceOf(ResponseInterface::class, $response); - $this->assertStringContainsString('Oops', $response->getBody()); + $this->assertStringContainsString('Oops', (string) $response->getBody()); $this->assertSame(567, $response->getStatusCode()); } @@ -182,7 +182,7 @@ public function testRun404OverrideReturnResponse(): void $response = $this->codeigniter->run($routes, true); $this->assertInstanceOf(ResponseInterface::class, $response); - $this->assertStringContainsString('Oops', $response->getBody()); + $this->assertStringContainsString('Oops', (string) $response->getBody()); } public function testRun404OverrideByClosure(): void @@ -203,7 +203,7 @@ public function testRun404OverrideByClosure(): void $this->codeigniter->run($routes); $output = ob_get_clean(); - $this->assertStringContainsString('404 Override by Closure.', $output); + $this->assertStringContainsString('404 Override by Closure.', (string) $output); $this->assertSame(404, response()->getStatusCode()); } @@ -228,7 +228,7 @@ public function testControllersCanReturnString(): void $this->codeigniter->run(); $output = ob_get_clean(); - $this->assertStringContainsString('You want to see "about" page.', $output); + $this->assertStringContainsString('You want to see "about" page.', (string) $output); } public function testControllersCanReturnResponseObject(): void @@ -254,7 +254,7 @@ public function testControllersCanReturnResponseObject(): void $this->codeigniter->run(); $output = ob_get_clean(); - $this->assertStringContainsString("You want to see 'about' page.", $output); + $this->assertStringContainsString("You want to see 'about' page.", (string) $output); } /** @@ -308,7 +308,7 @@ public function testRunExecuteFilterByClassName(): void $this->codeigniter->run(); $output = ob_get_clean(); - $this->assertStringContainsString('http://hellowworld.com', $output); + $this->assertStringContainsString('http://hellowworld.com', (string) $output); $this->resetServices(); } @@ -346,7 +346,7 @@ public function testRegisterSameFilterTwiceWithDifferentArgument(): void $this->codeigniter->run(); $output = ob_get_clean(); - $this->assertStringContainsString('http://hellowworld.comhttp://hellowworld.com', $output); + $this->assertStringContainsString('http://hellowworld.comhttp://hellowworld.com', (string) $output); $this->resetServices(); } @@ -374,7 +374,7 @@ public function testDisableControllerFilters(): void $this->codeigniter->run(); $output = ob_get_clean(); - $this->assertStringContainsString('', $output); + $this->assertStringContainsString('', (string) $output); $this->resetServices(); } @@ -402,7 +402,7 @@ public function testRoutesIsEmpty(): void $this->codeigniter->run(); $output = ob_get_clean(); - $this->assertStringContainsString('Welcome to CodeIgniter', $output); + $this->assertStringContainsString('Welcome to CodeIgniter', (string) $output); } public function testTransfersCorrectHTTPVersion(): void @@ -447,7 +447,7 @@ public function testIgnoringErrorSuppressedByAt(): void $this->codeigniter->run(); $output = ob_get_clean(); - $this->assertStringContainsString('Welcome to CodeIgniter', $output); + $this->assertStringContainsString('Welcome to CodeIgniter', (string) $output); } public function testRunForceSecure(): void @@ -684,7 +684,7 @@ public function testRunDefaultRoute(): void $this->codeigniter->run(); $output = ob_get_clean(); - $this->assertStringContainsString('Welcome to CodeIgniter', $output); + $this->assertStringContainsString('Welcome to CodeIgniter', (string) $output); } public function testRunCLIRoute(): void @@ -704,7 +704,7 @@ public function testRunCLIRoute(): void $this->codeigniter->run(); $output = ob_get_clean(); - $this->assertStringContainsString('Method Not Allowed', $output); + $this->assertStringContainsString('Method Not Allowed', (string) $output); } public function testSpoofRequestMethodCanUsePUT(): void @@ -795,7 +795,7 @@ public function testPageCacheSendSecureHeaders(): void $this->codeigniter->run(); $output = ob_get_clean(); - $this->assertStringContainsString('This is a test page', $output); + $this->assertStringContainsString('This is a test page', (string) $output); $response = service('response'); $headers = $response->headers(); $this->assertArrayHasKey('X-Frame-Options', $headers); @@ -805,7 +805,7 @@ public function testPageCacheSendSecureHeaders(): void $this->codeigniter->run(); $output = ob_get_clean(); - $this->assertStringContainsString('This is a test page', $output); + $this->assertStringContainsString('This is a test page', (string) $output); $response = service('response'); $headers = $response->headers(); $this->assertArrayHasKey('X-Frame-Options', $headers); diff --git a/tests/system/Commands/Database/ShowTableInfoTest.php b/tests/system/Commands/Database/ShowTableInfoTest.php index a707f519a858..a2749c812d8b 100644 --- a/tests/system/Commands/Database/ShowTableInfoTest.php +++ b/tests/system/Commands/Database/ShowTableInfoTest.php @@ -116,7 +116,7 @@ public function testDbTableMetadata(): void $expected = <<<'EOL' | Field Name | Type | Max Length | Nullable | Default | Primary Key | EOL; - $this->assertStringContainsString($expected, $result); + $this->assertStringContainsString($expected, (string) $result); } public function testDbTableDesc(): void diff --git a/tests/system/Commands/EnvironmentCommandTest.php b/tests/system/Commands/EnvironmentCommandTest.php index 8bab8db12cc0..b62ade32c827 100644 --- a/tests/system/Commands/EnvironmentCommandTest.php +++ b/tests/system/Commands/EnvironmentCommandTest.php @@ -94,6 +94,6 @@ public function testSettingNewEnvIsSuccess(): void command('env development'); $this->assertStringContainsString('Environment is successfully changed to', $this->getStreamFilterBuffer()); - $this->assertStringContainsString('CI_ENVIRONMENT = development', file_get_contents($this->envPath)); + $this->assertStringContainsString('CI_ENVIRONMENT = development', (string) file_get_contents($this->envPath)); } } diff --git a/tests/system/Commands/FilterCheckTest.php b/tests/system/Commands/FilterCheckTest.php index 6bc594ba1831..c6644e02e94e 100644 --- a/tests/system/Commands/FilterCheckTest.php +++ b/tests/system/Commands/FilterCheckTest.php @@ -48,14 +48,14 @@ public function testFilterCheckDefinedRoute(): void $this->assertStringContainsString( '| GET | / | forcehttps pagecache | pagecache performance toolbar |', - preg_replace('/\033\[.+?m/u', '', $this->getBuffer()), + (string) preg_replace('/\033\[.+?m/u', '', $this->getBuffer()), ); $this->assertStringContainsString( 'Before Filter Classes: CodeIgniter\Filters\ForceHTTPS → CodeIgniter\Filters\PageCache After Filter Classes: CodeIgniter\Filters\PageCache → CodeIgniter\Filters\PerformanceMetrics → CodeIgniter\Filters\DebugToolbar', - preg_replace('/\033\[.+?m/u', '', $this->getBuffer()), + (string) preg_replace('/\033\[.+?m/u', '', $this->getBuffer()), ); } diff --git a/tests/system/Commands/GenerateKeyTest.php b/tests/system/Commands/GenerateKeyTest.php index 01e2b2c750df..6dd7a7330c36 100644 --- a/tests/system/Commands/GenerateKeyTest.php +++ b/tests/system/Commands/GenerateKeyTest.php @@ -92,18 +92,18 @@ public function testGenerateKeyCreatesNewKey(): void { command('key:generate'); $this->assertStringContainsString('successfully set.', $this->getBuffer()); - $this->assertStringContainsString(env('encryption.key'), file_get_contents($this->envPath)); - $this->assertStringContainsString('hex2bin:', file_get_contents($this->envPath)); + $this->assertStringContainsString(env('encryption.key'), (string) file_get_contents($this->envPath)); + $this->assertStringContainsString('hex2bin:', (string) file_get_contents($this->envPath)); command('key:generate --prefix base64 --force'); $this->assertStringContainsString('successfully set.', $this->getBuffer()); - $this->assertStringContainsString(env('encryption.key'), file_get_contents($this->envPath)); - $this->assertStringContainsString('base64:', file_get_contents($this->envPath)); + $this->assertStringContainsString(env('encryption.key'), (string) file_get_contents($this->envPath)); + $this->assertStringContainsString('base64:', (string) file_get_contents($this->envPath)); command('key:generate --prefix hex2bin --force'); $this->assertStringContainsString('successfully set.', $this->getBuffer()); - $this->assertStringContainsString(env('encryption.key'), file_get_contents($this->envPath)); - $this->assertStringContainsString('hex2bin:', file_get_contents($this->envPath)); + $this->assertStringContainsString(env('encryption.key'), (string) file_get_contents($this->envPath)); + $this->assertStringContainsString('hex2bin:', (string) file_get_contents($this->envPath)); } public function testDefaultShippedEnvIsMissing(): void diff --git a/tests/system/Commands/RoutesTest.php b/tests/system/Commands/RoutesTest.php index 94455b826826..a28c0cc02e1a 100644 --- a/tests/system/Commands/RoutesTest.php +++ b/tests/system/Commands/RoutesTest.php @@ -86,7 +86,7 @@ public function testRoutesCommand(): void EOL; $this->assertStringContainsString( $expected, - preg_replace('/\033\[.+?m/u', '', $this->getBuffer()), + (string) preg_replace('/\033\[.+?m/u', '', $this->getBuffer()), ); } diff --git a/tests/system/Commands/TestGeneratorTest.php b/tests/system/Commands/TestGeneratorTest.php index b449afd76f05..b67c136a4009 100644 --- a/tests/system/Commands/TestGeneratorTest.php +++ b/tests/system/Commands/TestGeneratorTest.php @@ -13,8 +13,11 @@ namespace CodeIgniter\Commands; +use CodeIgniter\CLI\CLI; use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\Mock\MockInputOutput; use CodeIgniter\Test\StreamFilterTrait; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; /** @@ -25,22 +28,92 @@ final class TestGeneratorTest extends CIUnitTestCase { use StreamFilterTrait; + protected function setUp(): void + { + parent::setUp(); + + $this->resetStreamFilterBuffer(); + + putenv('NO_COLOR=1'); + CLI::init(); + } + protected function tearDown(): void { - $result = str_replace(["\033[0;32m", "\033[0m", "\n"], '', $this->getStreamFilterBuffer()); - $file = str_replace('ROOTPATH' . DIRECTORY_SEPARATOR, ROOTPATH, trim(substr($result, 14))); - $dir = dirname($file); + parent::tearDown(); + + $this->clearTestFiles(); + $this->resetStreamFilterBuffer(); + + putenv('NO_COLOR'); + CLI::init(); + } + + private function clearTestFiles(): void + { + preg_match('/File created: (.*)/', $this->getStreamFilterBuffer(), $result); + + $file = str_replace('ROOTPATH' . DIRECTORY_SEPARATOR, ROOTPATH, $result[1] ?? ''); if (is_file($file)) { unlink($file); } - if (is_dir($dir)) { + + $dir = dirname($file) . DIRECTORY_SEPARATOR; + if (is_dir($dir) && ! in_array($dir, ['/', TESTPATH, TESTPATH . 'system/', TESTPATH . '_support/'], true)) { rmdir($dir); } } - public function testGenerateTest(): void + #[DataProvider('provideGenerateTestFiles')] + public function testGenerateTestFiles(string $name, string $expectedClass): void { - command('make:test Foo/Bar'); - $this->assertFileExists(ROOTPATH . 'tests/Foo/Bar.php'); + command(sprintf('make:test %s', $name)); + + $expectedTestFile = str_replace('/', DIRECTORY_SEPARATOR, sprintf('%stests/%s.php', ROOTPATH, $expectedClass)); + $expectedMessage = sprintf('File created: %s', str_replace(ROOTPATH, 'ROOTPATH' . DIRECTORY_SEPARATOR, $expectedTestFile)); + $this->assertStringContainsString($expectedMessage, $this->getStreamFilterBuffer()); + $this->assertFileExists($expectedTestFile); + } + + /** + * @return iterable + */ + public static function provideGenerateTestFiles(): iterable + { + yield 'simple class name' => ['Foo', 'FooTest']; + + yield 'namespaced class name' => ['Foo/Bar', 'Foo/BarTest']; + + yield 'class with suffix' => ['Foo/BarTest', 'Foo/BarTest']; + + // the 4 slashes are needed to escape here and in the command + yield 'namespace style class name' => ['Foo\\\\Bar', 'Foo/BarTest']; + } + + public function testGenerateTestWithEmptyClassName(): void + { + $expectedFile = ROOTPATH . 'tests/FooTest.php'; + + try { + $io = new MockInputOutput(); + CLI::setInputOutput($io); + + // Simulate running `make:test` with no input followed by entering `Foo` + $io->setInputs(['', 'Foo']); + command('make:test'); + + $expectedOutput = 'Test class name : ' . PHP_EOL; + $expectedOutput .= 'The "Test class name" field is required.' . PHP_EOL; + $expectedOutput .= 'Test class name : Foo' . PHP_EOL . PHP_EOL; + $expectedOutput .= 'File created: ROOTPATH/tests/FooTest.php' . PHP_EOL . PHP_EOL; + $this->assertSame($expectedOutput, $io->getOutput()); + $this->assertFileExists($expectedFile); + } finally { + if (is_file($expectedFile)) { + unlink($expectedFile); + } + + CLI::resetInputOutput(); + } } } diff --git a/tests/system/CommonFunctionsTest.php b/tests/system/CommonFunctionsTest.php index f87a1f44860c..715ea3051735 100644 --- a/tests/system/CommonFunctionsTest.php +++ b/tests/system/CommonFunctionsTest.php @@ -612,7 +612,7 @@ public function testTrace(): void trace(); $content = ob_get_clean(); - $this->assertStringContainsString('Debug Backtrace', $content); + $this->assertStringContainsString('Debug Backtrace', (string) $content); } public function testViewNotSaveData(): void diff --git a/tests/system/CommonSingleServiceTest.php b/tests/system/CommonSingleServiceTest.php index 2a27c9b8486f..f3e3bb57b40b 100644 --- a/tests/system/CommonSingleServiceTest.php +++ b/tests/system/CommonSingleServiceTest.php @@ -75,29 +75,6 @@ public function testSingleServiceWithAtLeastOneParamSupplied(string $service): v } } - public function testSingleServiceWithAllParamsSupplied(): void - { - $cache1 = single_service('cache', null, true); - $cache2 = single_service('cache', null, true); - - assert($cache1 !== null); - assert($cache2 !== null); - - // Assert that even passing true as last param this will - // not create a shared instance. - $this->assertInstanceOf($cache1::class, $cache2); - $this->assertNotSame($cache1, $cache2); - } - - public function testSingleServiceWithGibberishGiven(): void - { - $this->assertNull(single_service('foo')); // @phpstan-ignore codeigniter.unknownServiceMethod - $this->assertNull(single_service('bar')); // @phpstan-ignore codeigniter.unknownServiceMethod - $this->assertNull(single_service('baz')); // @phpstan-ignore codeigniter.unknownServiceMethod - $this->assertNull(single_service('caches')); // @phpstan-ignore codeigniter.unknownServiceMethod - $this->assertNull(single_service('timers')); // @phpstan-ignore codeigniter.unknownServiceMethod - } - /** * @return iterable */ @@ -137,4 +114,27 @@ public static function provideServiceNames(): iterable yield from $services; } + + public function testSingleServiceWithAllParamsSupplied(): void + { + $cache1 = single_service('cache', null, true); + $cache2 = single_service('cache', null, true); + + assert($cache1 !== null); + assert($cache2 !== null); + + // Assert that even passing true as last param this will + // not create a shared instance. + $this->assertInstanceOf($cache1::class, $cache2); + $this->assertNotSame($cache1, $cache2); + } + + public function testSingleServiceWithGibberishGiven(): void + { + $this->assertNull(single_service('foo')); // @phpstan-ignore codeigniter.unknownServiceMethod + $this->assertNull(single_service('bar')); // @phpstan-ignore codeigniter.unknownServiceMethod + $this->assertNull(single_service('baz')); // @phpstan-ignore codeigniter.unknownServiceMethod + $this->assertNull(single_service('caches')); // @phpstan-ignore codeigniter.unknownServiceMethod + $this->assertNull(single_service('timers')); // @phpstan-ignore codeigniter.unknownServiceMethod + } } diff --git a/tests/system/Config/MimesTest.php b/tests/system/Config/MimesTest.php index bcf5923f5b9d..0b22a45e5735 100644 --- a/tests/system/Config/MimesTest.php +++ b/tests/system/Config/MimesTest.php @@ -24,6 +24,12 @@ #[Group('Others')] final class MimesTest extends CIUnitTestCase { + #[DataProvider('provideGuessExtensionFromType')] + public function testGuessExtensionFromType(?string $expected, string $mime): void + { + $this->assertSame($expected, Mimes::guessExtensionFromType($mime)); + } + public static function provideGuessExtensionFromType(): iterable { return [ @@ -50,10 +56,10 @@ public static function provideGuessExtensionFromType(): iterable ]; } - #[DataProvider('provideGuessExtensionFromType')] - public function testGuessExtensionFromType(?string $expected, string $mime): void + #[DataProvider('provideGuessTypeFromExtension')] + public function testGuessTypeFromExtension(?string $expected, string $ext): void { - $this->assertSame($expected, Mimes::guessExtensionFromType($mime)); + $this->assertSame($expected, Mimes::guessTypeFromExtension($ext)); } public static function provideGuessTypeFromExtension(): iterable @@ -81,10 +87,4 @@ public static function provideGuessTypeFromExtension(): iterable ], ]; } - - #[DataProvider('provideGuessTypeFromExtension')] - public function testGuessTypeFromExtension(?string $expected, string $ext): void - { - $this->assertSame($expected, Mimes::guessTypeFromExtension($ext)); - } } diff --git a/tests/system/Config/ServicesTest.php b/tests/system/Config/ServicesTest.php index 0e1c80afc75f..250474cbc0ea 100644 --- a/tests/system/Config/ServicesTest.php +++ b/tests/system/Config/ServicesTest.php @@ -36,6 +36,7 @@ use CodeIgniter\Router\RouteCollection; use CodeIgniter\Router\Router; use CodeIgniter\Security\Security; +use CodeIgniter\Session\Handlers\DatabaseHandler; use CodeIgniter\Session\Session; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Mock\MockResponse; @@ -46,6 +47,7 @@ use CodeIgniter\View\Cell; use CodeIgniter\View\Parser; use Config\App; +use Config\Database as DatabaseConfig; use Config\Exceptions; use Config\Security as SecurityConfig; use Config\Session as ConfigSession; @@ -261,7 +263,7 @@ public function testNewSessionWithNullConfig(): void $this->assertInstanceOf(Session::class, $actual); } - #[DataProvider('provideNewSessionInvalid')] + #[DataProvider('provideNewSessionWithInvalidHandler')] #[PreserveGlobalState(false)] #[RunInSeparateProcess] public function testNewSessionWithInvalidHandler(string $driver): void @@ -278,7 +280,7 @@ public function testNewSessionWithInvalidHandler(string $driver): void /** * @return iterable */ - public static function provideNewSessionInvalid(): iterable + public static function provideNewSessionWithInvalidHandler(): iterable { yield 'just a string' => ['file']; @@ -287,6 +289,25 @@ public static function provideNewSessionInvalid(): iterable yield 'other class' => [self::class]; } + #[PreserveGlobalState(false)] + #[RunInSeparateProcess] + public function testNewSessionWithInvalidDatabaseHandler(): void + { + $driver = config(DatabaseConfig::class)->tests['DBDriver']; + + if (in_array($driver, ['MySQLi', 'Postgre'], true)) { + $this->markTestSkipped('This test case does not work with MySQLi and Postgre'); + } + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage(sprintf('Invalid session database handler "%s" provided. Only "MySQLi" and "Postgre" are supported.', $driver)); + + $config = new ConfigSession(); + + $config->driver = DatabaseHandler::class; + Services::session($config, false); + } + #[PreserveGlobalState(false)] #[RunInSeparateProcess] public function testCallStatic(): void diff --git a/tests/system/Cookie/CookieStoreTest.php b/tests/system/Cookie/CookieStoreTest.php index 7e752fde5da5..60498052ee93 100644 --- a/tests/system/Cookie/CookieStoreTest.php +++ b/tests/system/Cookie/CookieStoreTest.php @@ -78,7 +78,7 @@ public function testCookieStoreInitViaHeaders(): void public function testInvalidCookieStored(): void { $this->expectException(CookieException::class); - new CookieStore([new DateTimeImmutable('now')]); + new CookieStore([new DateTimeImmutable('now')]); // @phpstan-ignore argument.type } public function testPutRemoveCookiesInStore(): void diff --git a/tests/system/DataConverter/DataConverterTest.php b/tests/system/DataConverter/DataConverterTest.php index 0e6e85a488c9..10a35e6909fc 100644 --- a/tests/system/DataConverter/DataConverterTest.php +++ b/tests/system/DataConverter/DataConverterTest.php @@ -50,16 +50,6 @@ public function testConvertDataFromDB(array $types, array $dbData, array $expect $this->assertSame($expected, $data); } - #[DataProvider('provideConvertDataToDB')] - public function testConvertDataToDB(array $types, array $phpData, array $expected): void - { - $converter = $this->createDataConverter($types); - - $data = $converter->toDataSource($phpData); - - $this->assertSame($expected, $data); - } - public static function provideConvertDataFromDB(): iterable { yield from [ @@ -206,6 +196,16 @@ public static function provideConvertDataFromDB(): iterable ]; } + #[DataProvider('provideConvertDataToDB')] + public function testConvertDataToDB(array $types, array $phpData, array $expected): void + { + $converter = $this->createDataConverter($types); + + $data = $converter->toDataSource($phpData); + + $this->assertSame($expected, $data); + } + public static function provideConvertDataToDB(): iterable { yield from [ diff --git a/tests/system/Database/BaseConnectionTest.php b/tests/system/Database/BaseConnectionTest.php index ba55129cc723..a9fcab80622b 100644 --- a/tests/system/Database/BaseConnectionTest.php +++ b/tests/system/Database/BaseConnectionTest.php @@ -108,24 +108,39 @@ public function testConnectionThrowExceptionWhenCannotConnect(): void public function testCanConnectAndStoreConnection(): void { + $conn = new class () {}; + $db = new MockConnection($this->options); - $db->shouldReturn('connect', 123)->initialize(); + $db->shouldReturn('connect', $conn)->initialize(); - $this->assertSame(123, $db->getConnection()); + $this->assertSame($conn, $db->getConnection()); } public function testCanConnectToFailoverWhenNoConnectionAvailable(): void { - $options = $this->options; - $options['failover'] = [$this->failoverOptions]; + $options = [ + ...$this->options, + ...['failover' => [$this->failoverOptions]], + ]; - $db = new class ($options) extends MockConnection { - protected $returnValues = [ - 'connect' => [false, 345], - ]; + $conn = new class () {}; + + $db = new class ($options, $conn) extends MockConnection { + /** + * @param array $params + */ + public function __construct(array $params, object $return) + { + // need to call it here before any initialization + // we cannot do it directly in the property as objects + // cannot be set directly in properties + $this->shouldReturn('connect', [false, $return]); + + parent::__construct($params); + } }; - $this->assertSame(345, $db->getConnection()); + $this->assertSame($conn, $db->getConnection()); $this->assertSame('failover', $db->username); } diff --git a/tests/system/Database/BaseQueryTest.php b/tests/system/Database/BaseQueryTest.php index 3c48e2526652..c5265b85dfa7 100644 --- a/tests/system/Database/BaseQueryTest.php +++ b/tests/system/Database/BaseQueryTest.php @@ -107,6 +107,19 @@ public function testSwapPrefix(): void $this->assertSame($newSQL, $query->getQuery()); } + /** + * @param mixed $expected + * @param mixed $sql + */ + #[DataProvider('provideIsWriteType')] + public function testIsWriteType($expected, $sql): void + { + $query = new Query($this->db); + + $query->setQuery($sql); + $this->assertSame($expected, $query->isWriteType()); + } + public static function provideIsWriteType(): iterable { return [ @@ -185,19 +198,6 @@ public static function provideIsWriteType(): iterable ]; } - /** - * @param mixed $expected - * @param mixed $sql - */ - #[DataProvider('provideIsWriteType')] - public function testIsWriteType($expected, $sql): void - { - $query = new Query($this->db); - - $query->setQuery($sql); - $this->assertSame($expected, $query->isWriteType()); - } - public function testSingleBindingOutsideOfArray(): void { $query = new Query($this->db); @@ -579,6 +579,19 @@ public function testSwapPrefixAfterGetQuery(): void $this->assertSame($expected, $query->getQuery()); } + /** + * @param mixed $expected + * @param mixed $sql + */ + #[DataProvider('provideHighlightQueryKeywords')] + public function testHighlightQueryKeywords($expected, $sql): void + { + $query = new Query($this->db); + $query->setQuery($sql); + + $this->assertSame($expected, $query->debugToolbarDisplay()); + } + public static function provideHighlightQueryKeywords(): iterable { return [ @@ -596,17 +609,4 @@ public static function provideHighlightQueryKeywords(): iterable ], ]; } - - /** - * @param mixed $expected - * @param mixed $sql - */ - #[DataProvider('provideHighlightQueryKeywords')] - public function testHighlightQueryKeywords($expected, $sql): void - { - $query = new Query($this->db); - $query->setQuery($sql); - - $this->assertSame($expected, $query->debugToolbarDisplay()); - } } diff --git a/tests/system/Database/Builder/InsertTest.php b/tests/system/Database/Builder/InsertTest.php index 4a5d7913e4ab..e8761b9de52b 100644 --- a/tests/system/Database/Builder/InsertTest.php +++ b/tests/system/Database/Builder/InsertTest.php @@ -162,7 +162,7 @@ public function testInsertBatch(): void ], ]; - $this->db->shouldReturn('execute', 1)->shouldReturn('affectedRows', 1); + $this->db->shouldReturn('execute', new class () {}); $builder->insertBatch($insertData, true); $query = $this->db->getLastQuery(); @@ -197,7 +197,7 @@ public function testInsertBatchIgnore(): void ], ]; - $this->db->shouldReturn('execute', 1)->shouldReturn('affectedRows', 1); + $this->db->shouldReturn('execute', new class () {}); $builder->ignore()->insertBatch($insertData, true, 1); $query = $this->db->getLastQuery(); @@ -231,7 +231,7 @@ public function testInsertBatchWithoutEscape(): void ], ]; - $this->db->shouldReturn('execute', 1)->shouldReturn('affectedRows', 1); + $this->db->shouldReturn('execute', new class () {}); $builder->insertBatch($insertData, false); $query = $this->db->getLastQuery(); @@ -255,7 +255,7 @@ public function testInsertBatchWithFieldsEndingInNumbers(): void ['ip' => '4.4.4.0', 'ip2' => '4.4.4.2'], ]; - $this->db->shouldReturn('execute', 1)->shouldReturn('affectedRows', 1); + $this->db->shouldReturn('execute', new class () {}); $builder->insertBatch($data, true); $query = $this->db->getLastQuery(); diff --git a/tests/system/Database/Builder/UpdateTest.php b/tests/system/Database/Builder/UpdateTest.php index b13739dfbd58..eb1cfc7d9c92 100644 --- a/tests/system/Database/Builder/UpdateTest.php +++ b/tests/system/Database/Builder/UpdateTest.php @@ -229,7 +229,7 @@ public function testUpdateBatch(): void ], ]; - $this->db->shouldReturn('execute', 1)->shouldReturn('affectedRows', 1); + $this->db->shouldReturn('execute', new class () {}); $builder->updateBatch($updateData, 'id'); $query = $this->db->getLastQuery(); @@ -268,7 +268,7 @@ public function testSetUpdateBatchWithoutEscape(): void ], ], 'id', $escape); - $this->db->shouldReturn('execute', 1)->shouldReturn('affectedRows', 1); + $this->db->shouldReturn('execute', new class () {}); $builder->updateBatch(null, 'id'); $query = $this->db->getLastQuery(); diff --git a/tests/system/Database/Builder/WhenTest.php b/tests/system/Database/Builder/WhenTest.php index a7fede7879d3..2fc4966accb8 100644 --- a/tests/system/Database/Builder/WhenTest.php +++ b/tests/system/Database/Builder/WhenTest.php @@ -15,7 +15,9 @@ use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Mock\MockConnection; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; +use stdClass; /** * @internal @@ -101,6 +103,23 @@ public function testWhenPassesParemeters(): void $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); } + #[DataProvider('provideConditionValues')] + public function testWhenRunsDefaultCallbackBasedOnCondition(mixed $condition, bool $expectDefault): void + { + $builder = $this->db->table('jobs'); + + $builder = $builder->when($condition, static function ($query): void { + $query->select('id'); + }, static function ($query): void { + $query->select('name'); + }); + + $expected = $expectDefault ? 'name' : 'id'; + $expectedSQL = 'SELECT "' . $expected . '" FROM "jobs"'; + + $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); + } + public function testWhenNotFalse(): void { $builder = $this->db->table('jobs'); @@ -166,4 +185,43 @@ public function testWhenNotPassesParemeters(): void $expectedSQL = 'SELECT * FROM "jobs" WHERE "name" = \'0\''; $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); } + + #[DataProvider('provideConditionValues')] + public function testWhenNotRunsDefaultCallbackBasedOnCondition(mixed $condition, bool $expectDefault): void + { + $builder = $this->db->table('jobs'); + + $builder = $builder->whenNot($condition, static function ($query): void { + $query->select('id'); + }, static function ($query): void { + $query->select('name'); + }); + + $expected = $expectDefault ? 'id' : 'name'; + $expectedSQL = 'SELECT "' . $expected . '" FROM "jobs"'; + + $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); + } + + /** + * @return array + */ + public static function provideConditionValues(): iterable + { + return [ + 'false' => [false, true], // [condition, expectedDefaultCallbackRuns] + 'int 0' => [0, true], + 'float 0.0' => [0.0, true], + 'empty string' => ['', true], + 'string 0' => ['0', true], + 'empty array' => [[], true], + 'null' => [null, true], + 'true' => [true, false], + 'int 1' => [1, false], + 'float 1.1' => [1.1, false], + 'non-empty string' => ['foo', false], + 'non-empty array' => [[1], false], + 'object' => [new stdClass(), false], + ]; + } } diff --git a/tests/system/Database/Builder/WhereTest.php b/tests/system/Database/Builder/WhereTest.php index 57980f28ee8c..2d0c2a3e1233 100644 --- a/tests/system/Database/Builder/WhereTest.php +++ b/tests/system/Database/Builder/WhereTest.php @@ -395,14 +395,6 @@ public function testWhereInSubQuery(): void $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); } - public static function provideWhereInvalidKeyThrowInvalidArgumentException(): iterable - { - return [ - 'null' => [null], - 'empty string' => [''], - ]; - } - /** * @param mixed $key */ @@ -415,12 +407,11 @@ public function testWhereInvalidKeyThrowInvalidArgumentException($key): void $builder->whereIn($key, ['Politician', 'Accountant']); } - public static function provideWhereInEmptyValuesThrowInvalidArgumentException(): iterable + public static function provideWhereInvalidKeyThrowInvalidArgumentException(): iterable { return [ - 'null' => [null], - 'not array' => ['not array'], - 'not instanceof \Closure' => [new stdClass()], + 'null' => [null], + 'empty string' => [''], ]; } @@ -436,6 +427,15 @@ public function testWhereInEmptyValuesThrowInvalidArgumentException($values): vo $builder->whereIn('name', $values); } + public static function provideWhereInEmptyValuesThrowInvalidArgumentException(): iterable + { + return [ + 'null' => [null], + 'not array' => ['not array'], + 'not instanceof \Closure' => [new stdClass()], + ]; + } + public function testWhereNotIn(): void { $builder = $this->db->table('jobs'); diff --git a/tests/system/Database/Live/LikeTest.php b/tests/system/Database/Live/LikeTest.php index 9591eef72ea8..c03dcd8d096c 100644 --- a/tests/system/Database/Live/LikeTest.php +++ b/tests/system/Database/Live/LikeTest.php @@ -76,7 +76,7 @@ public function testLikeCaseInsensitive(): void $this->assertSame('Developer', $job->name); } - #[DataProvider('provideMultibyteCharacters')] + #[DataProvider('provideLikeCaseInsensitiveWithMultibyteCharacter')] public function testLikeCaseInsensitiveWithMultibyteCharacter(string $match, string $result): void { $wai = $this->db->table('without_auto_increment')->like('value', $match, 'both', null, true)->get(); @@ -88,7 +88,7 @@ public function testLikeCaseInsensitiveWithMultibyteCharacter(string $match, str /** * @return iterable */ - public static function provideMultibyteCharacters(): iterable + public static function provideLikeCaseInsensitiveWithMultibyteCharacter(): iterable { yield from [ 'polish' => ['ŁĄ', 'multibyte characters pl'], diff --git a/tests/system/Database/Live/PreparedQueryTest.php b/tests/system/Database/Live/PreparedQueryTest.php index f5fe6115b111..7549e0b75ee0 100644 --- a/tests/system/Database/Live/PreparedQueryTest.php +++ b/tests/system/Database/Live/PreparedQueryTest.php @@ -304,4 +304,44 @@ public function testInsertBinaryData(): void $this->assertSame(strlen($fileContent), strlen($file)); } + + public function testHandleTransStatusMarksTransactionFailedDuringTransaction(): void + { + $this->db->transStart(); + + // Verify we're in a transaction + $this->assertSame(1, $this->db->transDepth); + + // Prepare a query that will fail (duplicate key) + $this->query = $this->db->prepare(static fn ($db) => $db->table('without_auto_increment')->insert([ + 'key' => 'a', + 'value' => 'b', + ])); + + $this->disableDBDebug(); + + $this->assertTrue($this->query->execute('test_key', 'test_value')); + $this->assertTrue($this->db->transStatus()); + + $this->seeInDatabase($this->db->DBPrefix . 'without_auto_increment', [ + 'key' => 'test_key', + 'value' => 'test_value' + ]); + + $this->assertFalse($this->query->execute('test_key', 'different_value')); + $this->assertFalse($this->db->transStatus()); + + $this->enableDBDebug(); + + // Complete the transaction - should rollback due to failed status + $this->assertFalse($this->db->transComplete()); + + // Verify the first insert was rolled back + $this->dontSeeInDatabase($this->db->DBPrefix . 'without_auto_increment', [ + 'key' => 'test_key', + 'value' => 'test_value' + ]); + + $this->db->resetTransStatus(); + } } diff --git a/tests/system/Debug/ExceptionHandlerTest.php b/tests/system/Debug/ExceptionHandlerTest.php index 36279dd58f80..a278ade8be7e 100644 --- a/tests/system/Debug/ExceptionHandlerTest.php +++ b/tests/system/Debug/ExceptionHandlerTest.php @@ -135,7 +135,7 @@ public function testHandleWebPageNotFoundExceptionAcceptHTML(): void $this->handler->handle($exception, $request, $response, 404, EXIT_ERROR); $output = ob_get_clean(); - $this->assertStringContainsString('404 - Page Not Found', $output); + $this->assertStringContainsString('404 - Page Not Found', (string) $output); } public function testHandleCLIPageNotFoundException(): void diff --git a/tests/system/Email/EmailTest.php b/tests/system/Email/EmailTest.php index ca26743c3ddc..8d865b58ddeb 100644 --- a/tests/system/Email/EmailTest.php +++ b/tests/system/Email/EmailTest.php @@ -16,9 +16,11 @@ use CodeIgniter\Events\Events; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Mock\MockEmail; +use CodeIgniter\Test\ReflectionHelper; use ErrorException; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; +use ReflectionException; /** * @internal @@ -26,6 +28,8 @@ #[Group('Others')] final class EmailTest extends CIUnitTestCase { + use ReflectionHelper; + public function testEmailValidation(): void { $config = config('Email'); @@ -35,14 +39,6 @@ public function testEmailValidation(): void $this->assertStringContainsString('Invalid email address: "invalid"', $email->printDebugger()); } - public static function provideEmailSendWithClearance(): iterable - { - return [ - 'autoclear' => [true], - 'not autoclear' => [false], - ]; - } - /** * @param bool $autoClear */ @@ -60,6 +56,14 @@ public function testEmailSendWithClearance($autoClear): void } } + public static function provideEmailSendWithClearance(): iterable + { + return [ + 'autoclear' => [true], + 'not autoclear' => [false], + ]; + } + public function testEmailSendStoresArchive(): void { $email = $this->createMockEmail(); @@ -215,4 +219,51 @@ public function testSetAttachmentCIDBufferString(): void $email->archive['body'], ); } + + /** + * @throws ReflectionException + */ + public function testGetHostnameUsesServerName(): void + { + $email = $this->createMockEmail(); + + $superglobals = service('superglobals'); + $superglobals->setServer('SERVER_NAME', 'example.test'); + + $getHostname = self::getPrivateMethodInvoker($email, 'getHostname'); + + $this->assertSame('example.test', $getHostname()); + } + + /** + * @throws ReflectionException + */ + public function testGetHostnameUsesServerAddr(): void + { + $email = $this->createMockEmail(); + + $superglobals = service('superglobals'); + $superglobals->setServer('SERVER_NAME', ''); + $superglobals->setServer('SERVER_ADDR', '192.168.1.10'); + + $getHostname = self::getPrivateMethodInvoker($email, 'getHostname'); + + $this->assertSame('[192.168.1.10]', $getHostname()); + } + + /** + * @throws ReflectionException + */ + public function testGetHostnameFallsBackToGethostnameFunction(): void + { + $email = $this->createMockEmail(); + + $superglobals = service('superglobals'); + $superglobals->setServer('SERVER_NAME', ''); + $superglobals->setServer('SERVER_ADDR', ''); + + $getHostname = self::getPrivateMethodInvoker($email, 'getHostname'); + + $this->assertSame(gethostname(), $getHostname()); + } } diff --git a/tests/system/Files/FileTest.php b/tests/system/Files/FileTest.php index b8e82edd02fd..8975ba0a6fd2 100644 --- a/tests/system/Files/FileTest.php +++ b/tests/system/Files/FileTest.php @@ -139,6 +139,27 @@ public function testGetSizeMetric(FileSizeUnit $unit): void $this->assertSame($size, $file->getSizeByMetricUnit($unit)); } + /** + * @return array> + */ + public static function provideGetSizeData(): iterable + { + return [ + 'returns KB binary' => [ + FileSizeUnit::KB, + ], + 'returns MB binary' => [ + FileSizeUnit::MB, + ], + 'returns GB binary' => [ + FileSizeUnit::GB, + ], + 'returns TB binary' => [ + FileSizeUnit::TB, + ], + ]; + } + public function testGetSizeMetricBytes(): void { $file = new File(SYSTEMPATH . 'Common.php'); @@ -168,25 +189,4 @@ public function testGetDestination(): void unlink(SYSTEMPATH . 'Common_Copy.php'); unlink(SYSTEMPATH . 'Common_Copy_5.php'); } - - /** - * @return array> - */ - public static function provideGetSizeData() - { - return [ - 'returns KB binary' => [ - FileSizeUnit::KB, - ], - 'returns MB binary' => [ - FileSizeUnit::MB, - ], - 'returns GB binary' => [ - FileSizeUnit::GB, - ], - 'returns TB binary' => [ - FileSizeUnit::TB, - ], - ]; - } } diff --git a/tests/system/Filters/FiltersTest.php b/tests/system/Filters/FiltersTest.php index a6ef8abede3e..b2daf0ec2b07 100644 --- a/tests/system/Filters/FiltersTest.php +++ b/tests/system/Filters/FiltersTest.php @@ -210,24 +210,6 @@ public function testProcessMethodProcessGlobals(): void $this->assertSame($expected, $filters->initialize()->getFilters()); } - public static function provideProcessMethodProcessGlobalsWithExcept(): iterable - { - return [ - [ - ['admin/*'], - ], - [ - ['admin/*', 'foo/*'], - ], - [ - ['*'], - ], - [ - 'admin/*', - ], - ]; - } - /** * @param array|string $except */ @@ -265,6 +247,24 @@ public function testProcessMethodProcessGlobalsWithExcept($except): void $this->assertSame($expected, $filters->initialize($uri)->getFilters()); } + public static function provideProcessMethodProcessGlobalsWithExcept(): iterable + { + return [ + [ + ['admin/*'], + ], + [ + ['admin/*', 'foo/*'], + ], + [ + ['*'], + ], + [ + 'admin/*', + ], + ]; + } + public function testProcessMethodProcessesFiltersBefore(): void { $_SERVER['REQUEST_METHOD'] = 'GET'; diff --git a/tests/system/Filters/HoneypotTest.php b/tests/system/Filters/HoneypotTest.php index 2d45b35a20b0..7e29177f7d3f 100644 --- a/tests/system/Filters/HoneypotTest.php +++ b/tests/system/Filters/HoneypotTest.php @@ -109,7 +109,7 @@ public function testAfter(): void $this->response->setBody('
'); $this->response = $filters->run($uri, 'after'); - $this->assertStringContainsString($this->honey->name, $this->response->getBody()); + $this->assertStringContainsString($this->honey->name, (string) $this->response->getBody()); } #[PreserveGlobalState(false)] @@ -129,6 +129,6 @@ public function testAfterNotApplicable(): void $this->response->setBody('
'); $this->response = $filters->run($uri, 'after'); - $this->assertStringNotContainsString($this->honey->name, $this->response->getBody()); + $this->assertStringNotContainsString($this->honey->name, (string) $this->response->getBody()); } } diff --git a/tests/system/Format/JSONFormatterTest.php b/tests/system/Format/JSONFormatterTest.php index 7ac97091a4c7..027c618dc4f0 100644 --- a/tests/system/Format/JSONFormatterTest.php +++ b/tests/system/Format/JSONFormatterTest.php @@ -13,8 +13,9 @@ namespace CodeIgniter\Format; -use CodeIgniter\Exceptions\RuntimeException; +use CodeIgniter\Format\Exceptions\FormatException; use CodeIgniter\Test\CIUnitTestCase; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; /** @@ -31,51 +32,34 @@ protected function setUp(): void $this->jsonFormatter = new JSONFormatter(); } - public function testBasicJSON(): void + /** + * @param array $data + */ + #[DataProvider('provideFormattingToJson')] + public function testFormattingToJson(array $data, string $expected): void { - $data = [ - 'foo' => 'bar', - ]; - - $expected = '{ - "foo": "bar" -}'; - $this->assertSame($expected, $this->jsonFormatter->format($data)); } - public function testUnicodeOutput(): void + /** + * @return iterable, 1: string}> + */ + public static function provideFormattingToJson(): iterable { - $data = [ - 'foo' => 'База данни грешка', - ]; + yield 'empty array' => [[], '[]']; - $expected = '{ - "foo": "База данни грешка" -}'; + yield 'simple array' => [['foo' => 'bar'], "{\n \"foo\": \"bar\"\n}"]; - $this->assertSame($expected, $this->jsonFormatter->format($data)); - } + yield 'unicode array' => [['foo' => 'База данни грешка'], "{\n \"foo\": \"База данни грешка\"\n}"]; - public function testKeepsURLs(): void - { - $data = [ - 'foo' => 'https://www.example.com/foo/bar', - ]; - - $expected = '{ - "foo": "https://www.example.com/foo/bar" -}'; - - $this->assertSame($expected, $this->jsonFormatter->format($data)); + yield 'url array' => [['foo' => 'https://www.example.com/foo/bar'], "{\n \"foo\": \"https://www.example.com/foo/bar\"\n}"]; } - public function testJSONError(): void + public function testJSONFormatterThrowsError(): void { - $this->expectException(RuntimeException::class); + $this->expectException(FormatException::class); + $this->expectExceptionMessage('Malformed UTF-8 characters, possibly incorrectly encoded'); - $data = ["\xB1\x31"]; - $expected = 'Boom'; - $this->assertSame($expected, $this->jsonFormatter->format($data)); + $this->assertSame('Boom', $this->jsonFormatter->format(["\xB1\x31"])); } } diff --git a/tests/system/Format/XMLFormatterTest.php b/tests/system/Format/XMLFormatterTest.php index 0615a1eafd91..107418a5d68c 100644 --- a/tests/system/Format/XMLFormatterTest.php +++ b/tests/system/Format/XMLFormatterTest.php @@ -104,6 +104,9 @@ public function testValidatingXmlTags(): void $this->assertSame($expected, $this->xmlFormatter->format($data)); } + /** + * @param array $input + */ #[DataProvider('provideValidatingInvalidTags')] public function testValidatingInvalidTags(string $expected, array $input): void { @@ -116,6 +119,9 @@ public function testValidatingInvalidTags(string $expected, array $input): void $this->assertSame($expectedXML, $this->xmlFormatter->format($input)); } + /** + * @return iterable}> + */ public static function provideValidatingInvalidTags(): iterable { return [ diff --git a/tests/system/HTTP/CURLRequestTest.php b/tests/system/HTTP/CURLRequestTest.php index e72a1d37d17c..6d1b328c349b 100644 --- a/tests/system/HTTP/CURLRequestTest.php +++ b/tests/system/HTTP/CURLRequestTest.php @@ -1381,4 +1381,34 @@ public function testNotRemoveMultipleRedirectHeaderSectionsWithoutLocationHeader $this->assertSame($testBody, $response->getBody()); } + + public function testProxyAndContinueResponses(): void + { + $testBody = '{"Id":"83589c7e-bd86-4101-8d93-3f2e7954e48e"}'; + + $output = "HTTP/1.1 200 Connection established\r\n\r\nHTTP/1.1 100 Continue +Connection: keep-alive\r\n\r\nHTTP/1.1 202 Accepted +Vary: Origin,Access-Control-Request-Method,Access-Control-Request-Headers, Accept-Encoding +x-content-type-options: nosniff +x-xss-protection: 1; mode=block +Cache-Control: no-cache, no-store, max-age=0, must-revalidate +Pragma: no-cache +Expires: 0 +strict-transport-security: max-age=31536000 ; includeSubDomains +x-frame-options: DENY +Content-Type: application/json +Content-Length: 56 +Date: Wed, 02 Jul 2025 18:37:21 GMT +Connection: keep-alive\r\n\r\n" . $testBody; + + $this->request->setOutput($output); + + $response = $this->request->request('GET', 'http://example.com', [ + 'allow_redirects' => false, + ]); + + $this->assertSame(202, $response->getStatusCode()); + + $this->assertSame($testBody, $response->getBody()); + } } diff --git a/tests/system/HTTP/ContentSecurityPolicyTest.php b/tests/system/HTTP/ContentSecurityPolicyTest.php index a9657804f489..06dde464a4a4 100644 --- a/tests/system/HTTP/ContentSecurityPolicyTest.php +++ b/tests/system/HTTP/ContentSecurityPolicyTest.php @@ -100,12 +100,12 @@ public function testDefaults(): void $result = $this->work(); $result = $this->getHeaderEmitted('Content-Security-Policy'); - $this->assertStringContainsString("base-uri 'self';", $result); - $this->assertStringContainsString("connect-src 'self';", $result); - $this->assertStringContainsString("default-src 'self';", $result); - $this->assertStringContainsString("img-src 'self';", $result); - $this->assertStringContainsString("script-src 'self';", $result); - $this->assertStringContainsString("style-src 'self';", $result); + $this->assertStringContainsString("base-uri 'self';", (string) $result); + $this->assertStringContainsString("connect-src 'self';", (string) $result); + $this->assertStringContainsString("default-src 'self';", (string) $result); + $this->assertStringContainsString("img-src 'self';", (string) $result); + $this->assertStringContainsString("script-src 'self';", (string) $result); + $this->assertStringContainsString("style-src 'self';", (string) $result); } #[PreserveGlobalState(false)] @@ -118,9 +118,9 @@ public function testChildSrc(): void $result = $this->work(); $result = $this->getHeaderEmitted('Content-Security-Policy-Report-Only'); - $this->assertStringContainsString('child-src evil.com;', $result); + $this->assertStringContainsString('child-src evil.com;', (string) $result); $result = $this->getHeaderEmitted('Content-Security-Policy'); - $this->assertStringContainsString("child-src 'self' good.com;", $result); + $this->assertStringContainsString("child-src 'self' good.com;", (string) $result); } #[PreserveGlobalState(false)] @@ -134,7 +134,7 @@ public function testConnectSrc(): void $result = $this->work(); $result = $this->getHeaderEmitted('Content-Security-Policy-Report-Only'); - $this->assertStringContainsString("connect-src 'self' iffy.com maybe.com;", $result); + $this->assertStringContainsString("connect-src 'self' iffy.com maybe.com;", (string) $result); } #[PreserveGlobalState(false)] @@ -148,9 +148,9 @@ public function testFontSrc(): void $result = $this->work(); $result = $this->getHeaderEmitted('Content-Security-Policy-Report-Only'); - $this->assertStringContainsString('font-src iffy.com;', $result); + $this->assertStringContainsString('font-src iffy.com;', (string) $result); $result = $this->getHeaderEmitted('Content-Security-Policy'); - $this->assertStringContainsString('font-src fontsrus.com;', $result); + $this->assertStringContainsString('font-src fontsrus.com;', (string) $result); } #[PreserveGlobalState(false)] @@ -163,10 +163,10 @@ public function testFormAction(): void $result = $this->work(); $result = $this->getHeaderEmitted('Content-Security-Policy-Report-Only'); - $this->assertStringContainsString("form-action 'self' surveysrus.com;", $result); + $this->assertStringContainsString("form-action 'self' surveysrus.com;", (string) $result); $result = $this->getHeaderEmitted('Content-Security-Policy'); - $this->assertStringNotContainsString("form-action 'self';", $result); + $this->assertStringNotContainsString("form-action 'self';", (string) $result); } #[PreserveGlobalState(false)] @@ -179,9 +179,9 @@ public function testFrameAncestor(): void $result = $this->work(); $result = $this->getHeaderEmitted('Content-Security-Policy-Report-Only'); - $this->assertStringContainsString('frame-ancestors them.com;', $result); + $this->assertStringContainsString('frame-ancestors them.com;', (string) $result); $result = $this->getHeaderEmitted('Content-Security-Policy'); - $this->assertStringContainsString("frame-ancestors 'self';", $result); + $this->assertStringContainsString("frame-ancestors 'self';", (string) $result); } #[PreserveGlobalState(false)] @@ -194,9 +194,9 @@ public function testFrameSrc(): void $result = $this->work(); $result = $this->getHeaderEmitted('Content-Security-Policy-Report-Only'); - $this->assertStringContainsString('frame-src them.com;', $result); + $this->assertStringContainsString('frame-src them.com;', (string) $result); $result = $this->getHeaderEmitted('Content-Security-Policy'); - $this->assertStringContainsString("frame-src 'self';", $result); + $this->assertStringContainsString("frame-src 'self';", (string) $result); } #[PreserveGlobalState(false)] @@ -209,9 +209,9 @@ public function testImageSrc(): void $result = $this->work(); $result = $this->getHeaderEmitted('Content-Security-Policy-Report-Only'); - $this->assertStringContainsString('img-src them.com;', $result); + $this->assertStringContainsString('img-src them.com;', (string) $result); $result = $this->getHeaderEmitted('Content-Security-Policy'); - $this->assertStringContainsString("img-src 'self' cdn.cloudy.com;", $result); + $this->assertStringContainsString("img-src 'self' cdn.cloudy.com;", (string) $result); } #[PreserveGlobalState(false)] @@ -224,9 +224,9 @@ public function testMediaSrc(): void $result = $this->work(); $result = $this->getHeaderEmitted('Content-Security-Policy-Report-Only'); - $this->assertStringContainsString('media-src them.com;', $result); + $this->assertStringContainsString('media-src them.com;', (string) $result); $result = $this->getHeaderEmitted('Content-Security-Policy'); - $this->assertStringContainsString("media-src 'self';", $result); + $this->assertStringContainsString("media-src 'self';", (string) $result); } #[PreserveGlobalState(false)] @@ -239,9 +239,9 @@ public function testManifestSrc(): void $result = $this->work(); $result = $this->getHeaderEmitted('Content-Security-Policy-Report-Only'); - $this->assertStringContainsString('manifest-src them.com;', $result); + $this->assertStringContainsString('manifest-src them.com;', (string) $result); $result = $this->getHeaderEmitted('Content-Security-Policy'); - $this->assertStringContainsString('manifest-src cdn.cloudy.com;', $result); + $this->assertStringContainsString('manifest-src cdn.cloudy.com;', (string) $result); } #[PreserveGlobalState(false)] @@ -254,9 +254,9 @@ public function testPluginType(): void $result = $this->work(); $result = $this->getHeaderEmitted('Content-Security-Policy-Report-Only'); - $this->assertStringContainsString('plugin-types application/x-shockwave-flash;', $result); + $this->assertStringContainsString('plugin-types application/x-shockwave-flash;', (string) $result); $result = $this->getHeaderEmitted('Content-Security-Policy'); - $this->assertStringContainsString("plugin-types 'self';", $result); + $this->assertStringContainsString("plugin-types 'self';", (string) $result); } #[PreserveGlobalState(false)] @@ -269,7 +269,7 @@ public function testPluginArray(): void $result = $this->work(); $result = $this->getHeaderEmitted('Content-Security-Policy'); - $this->assertStringContainsString('plugin-types application/x-shockwave-flash application/wacky-hacky;', $result); + $this->assertStringContainsString('plugin-types application/x-shockwave-flash application/wacky-hacky;', (string) $result); } #[PreserveGlobalState(false)] @@ -282,9 +282,9 @@ public function testObjectSrc(): void $result = $this->work(); $result = $this->getHeaderEmitted('Content-Security-Policy-Report-Only'); - $this->assertStringContainsString('object-src them.com;', $result); + $this->assertStringContainsString('object-src them.com;', (string) $result); $result = $this->getHeaderEmitted('Content-Security-Policy'); - $this->assertStringContainsString("object-src 'self' cdn.cloudy.com;", $result); + $this->assertStringContainsString("object-src 'self' cdn.cloudy.com;", (string) $result); } #[PreserveGlobalState(false)] @@ -297,9 +297,9 @@ public function testScriptSrc(): void $result = $this->work(); $result = $this->getHeaderEmitted('Content-Security-Policy-Report-Only'); - $this->assertStringContainsString('script-src them.com;', $result); + $this->assertStringContainsString('script-src them.com;', (string) $result); $result = $this->getHeaderEmitted('Content-Security-Policy'); - $this->assertStringContainsString("script-src 'self' cdn.cloudy.com;", $result); + $this->assertStringContainsString("script-src 'self' cdn.cloudy.com;", (string) $result); } #[PreserveGlobalState(false)] @@ -312,9 +312,9 @@ public function testStyleSrc(): void $result = $this->work(); $result = $this->getHeaderEmitted('Content-Security-Policy-Report-Only'); - $this->assertStringContainsString('style-src them.com;', $result); + $this->assertStringContainsString('style-src them.com;', (string) $result); $result = $this->getHeaderEmitted('Content-Security-Policy'); - $this->assertStringContainsString("style-src 'self' cdn.cloudy.com;", $result); + $this->assertStringContainsString("style-src 'self' cdn.cloudy.com;", (string) $result); } #[PreserveGlobalState(false)] @@ -325,7 +325,7 @@ public function testBaseURIDefault(): void $result = $this->work(); $result = $this->getHeaderEmitted('Content-Security-Policy'); - $this->assertStringContainsString("base-uri 'self';", $result); + $this->assertStringContainsString("base-uri 'self';", (string) $result); } #[PreserveGlobalState(false)] @@ -337,7 +337,7 @@ public function testBaseURI(): void $result = $this->work(); $result = $this->getHeaderEmitted('Content-Security-Policy'); - $this->assertStringContainsString('base-uri example.com;', $result); + $this->assertStringContainsString('base-uri example.com;', (string) $result); } #[PreserveGlobalState(false)] @@ -349,7 +349,7 @@ public function testBaseURIRich(): void $result = $this->work(); $result = $this->getHeaderEmitted('Content-Security-Policy'); - $this->assertStringContainsString("base-uri 'self' example.com;", $result); + $this->assertStringContainsString("base-uri 'self' example.com;", (string) $result); } #[PreserveGlobalState(false)] @@ -363,7 +363,7 @@ public function testDefaultSrc(): void $result = $this->work(); $result = $this->getHeaderEmitted('Content-Security-Policy'); - $this->assertStringContainsString('default-src iffy.com;', $result); + $this->assertStringContainsString('default-src iffy.com;', (string) $result); } #[PreserveGlobalState(false)] @@ -376,7 +376,7 @@ public function testReportURI(): void $result = $this->work(); $result = $this->getHeaderEmitted('Content-Security-Policy'); - $this->assertStringContainsString('report-uri http://example.com/csptracker;', $result); + $this->assertStringContainsString('report-uri http://example.com/csptracker;', (string) $result); } #[PreserveGlobalState(false)] @@ -389,7 +389,7 @@ public function testRemoveReportURI(): void $this->work(); $result = $this->getHeaderEmitted('Content-Security-Policy'); - $this->assertStringNotContainsString('report-uri ', $result); + $this->assertStringNotContainsString('report-uri ', (string) $result); } #[PreserveGlobalState(false)] @@ -403,7 +403,7 @@ public function testSandboxFlags(): void $result = $this->work(); $result = $this->getHeaderEmitted('Content-Security-Policy'); - $this->assertStringContainsString('sandbox allow-popups allow-top-navigation;', $result); + $this->assertStringContainsString('sandbox allow-popups allow-top-navigation;', (string) $result); } #[PreserveGlobalState(false)] @@ -415,7 +415,7 @@ public function testUpgradeInsecureRequests(): void $result = $this->work(); $result = $this->getHeaderEmitted('Content-Security-Policy'); - $this->assertStringContainsString('upgrade-insecure-requests;', $result); + $this->assertStringContainsString('upgrade-insecure-requests;', (string) $result); } #[PreserveGlobalState(false)] @@ -444,9 +444,9 @@ public function testBodyScriptNonce(): void static fn ($value): bool => str_starts_with($value, 'nonce-'), ); - $this->assertStringContainsString('nonce=', $this->response->getBody()); + $this->assertStringContainsString('nonce=', (string) $this->response->getBody()); $result = $this->getHeaderEmitted('Content-Security-Policy'); - $this->assertStringContainsString('nonce-', $result); + $this->assertStringContainsString('nonce-', (string) $result); $this->assertSame([], $nonceStyle); } @@ -463,7 +463,7 @@ public function testBodyScriptNonceCustomScriptTag(): void $csp->finalize($response); - $this->assertStringContainsString('nonce=', $response->getBody()); + $this->assertStringContainsString('nonce=', (string) $response->getBody()); } public function testBodyScriptNonceDisableAutoNonce(): void @@ -479,7 +479,7 @@ public function testBodyScriptNonceDisableAutoNonce(): void $csp->finalize($response); - $this->assertStringContainsString('{csp-script-nonce}', $response->getBody()); + $this->assertStringContainsString('{csp-script-nonce}', (string) $response->getBody()); $result = new TestResponse($response); $result->assertHeader('Content-Security-Policy'); @@ -498,7 +498,7 @@ public function testBodyStyleNonceDisableAutoNonce(): void $csp->finalize($response); - $this->assertStringContainsString('{csp-style-nonce}', $response->getBody()); + $this->assertStringContainsString('{csp-style-nonce}', (string) $response->getBody()); $result = new TestResponse($response); $result->assertHeader('Content-Security-Policy'); @@ -519,9 +519,9 @@ public function testBodyStyleNonce(): void static fn ($value): bool => str_starts_with($value, 'nonce-'), ); - $this->assertStringContainsString('nonce=', $this->response->getBody()); + $this->assertStringContainsString('nonce=', (string) $this->response->getBody()); $result = $this->getHeaderEmitted('Content-Security-Policy'); - $this->assertStringContainsString('nonce-', $result); + $this->assertStringContainsString('nonce-', (string) $result); $this->assertSame([], $nonceScript); } @@ -538,7 +538,7 @@ public function testBodyStyleNonceCustomStyleTag(): void $csp->finalize($response); - $this->assertStringContainsString('nonce=', $response->getBody()); + $this->assertStringContainsString('nonce=', (string) $response->getBody()); } #[PreserveGlobalState(false)] @@ -560,7 +560,7 @@ public function testHeaderIgnoreCase(): void $result = $this->work(); $result = $this->getHeaderEmitted('content-security-policy', true); - $this->assertStringContainsString("base-uri 'self';", $result); + $this->assertStringContainsString("base-uri 'self';", (string) $result); } #[PreserveGlobalState(false)] @@ -602,7 +602,7 @@ public function testHeaderScriptNonceEmittedOnceGetScriptNonceCalled(): void $this->work(); $result = $this->getHeaderEmitted('Content-Security-Policy'); - $this->assertStringContainsString("script-src 'self' 'nonce-", $result); + $this->assertStringContainsString("script-src 'self' 'nonce-", (string) $result); } public function testClearDirective(): void diff --git a/tests/system/HTTP/DownloadResponseTest.php b/tests/system/HTTP/DownloadResponseTest.php index 5897a1f58750..7813a08def49 100644 --- a/tests/system/HTTP/DownloadResponseTest.php +++ b/tests/system/HTTP/DownloadResponseTest.php @@ -136,7 +136,16 @@ public function testDispositionInline(): void $response = new DownloadResponse('unit-test.txt', true); $response->inline(); $response->buildHeaders(); - $this->assertSame('inline', $response->getHeaderLine('Content-Disposition')); + $this->assertSame('inline; filename="unit-test.txt"; filename*=UTF-8\'\'unit-test.txt', $response->getHeaderLine('Content-Disposition')); + } + + public function testDispositionInlineWithSetFileName(): void + { + $response = new DownloadResponse('unit-test.txt', true); + $response->setFileName('my"quoted"File.txt'); + $response->inline(); + $response->buildHeaders(); + $this->assertSame('inline; filename="my\"quoted\"File.txt"; filename*=UTF-8\'\'my%22quoted%22File.txt', $response->getHeaderLine('Content-Disposition')); } public function testNoCache(): void diff --git a/tests/system/HTTP/HeaderTest.php b/tests/system/HTTP/HeaderTest.php index 3dab6faa3784..99ee23708780 100644 --- a/tests/system/HTTP/HeaderTest.php +++ b/tests/system/HTTP/HeaderTest.php @@ -240,7 +240,7 @@ public function testHeaderToStringShowsEntireHeader(): void /** * @param string $name */ - #[DataProvider('invalidNamesProvider')] + #[DataProvider('provideInvalidHeaderNames')] public function testInvalidHeaderNames($name): void { $this->expectException(InvalidArgumentException::class); @@ -251,7 +251,7 @@ public function testInvalidHeaderNames($name): void /** * @return list> */ - public static function invalidNamesProvider(): array + public static function provideInvalidHeaderNames(): iterable { return [ ["Content-Type\r\n\r\n"], @@ -277,7 +277,7 @@ public static function invalidNamesProvider(): array /** * @param array|string>|string|null $value */ - #[DataProvider('invalidValuesProvider')] + #[DataProvider('provideInvalidHeaderValues')] public function testInvalidHeaderValues($value): void { $this->expectException(InvalidArgumentException::class); @@ -288,7 +288,7 @@ public function testInvalidHeaderValues($value): void /** * @return list|string>> */ - public static function invalidValuesProvider(): array + public static function provideInvalidHeaderValues(): iterable { return [ ["Header\n Value"], diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index 337fc24384f9..1240d0885b23 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -579,6 +579,24 @@ public function testCanGrabGetRawInput(): void $this->assertSame($expected, $request->getRawInput()); } + /** + * @param string $rawstring + * @param mixed $var + * @param mixed $expected + * @param mixed $filter + * @param mixed $flag + */ + #[DataProvider('provideCanGrabGetRawInputVar')] + public function testCanGrabGetRawInputVar($rawstring, $var, $expected, $filter, $flag): void + { + $config = new App(); + $config->baseURL = 'http://example.com/'; + + $request = $this->createRequest($config, $rawstring); + + $this->assertSame($expected, $request->getRawInputVar($var, $filter, $flag)); + } + public static function provideCanGrabGetRawInputVar(): iterable { return [ @@ -658,24 +676,6 @@ public static function provideCanGrabGetRawInputVar(): iterable ]; } - /** - * @param string $rawstring - * @param mixed $var - * @param mixed $expected - * @param mixed $filter - * @param mixed $flag - */ - #[DataProvider('provideCanGrabGetRawInputVar')] - public function testCanGrabGetRawInputVar($rawstring, $var, $expected, $filter, $flag): void - { - $config = new App(); - $config->baseURL = 'http://example.com/'; - - $request = $this->createRequest($config, $rawstring); - - $this->assertSame($expected, $request->getRawInputVar($var, $filter, $flag)); - } - #[DataProvider('provideIsHTTPMethods')] public function testIsHTTPMethodLowerCase(string $value): void { @@ -902,20 +902,6 @@ public function testGetPostIndexNotExists(): void $this->assertNull($this->request->getGetPost('gc')); } - public static function provideExtensionPHP(): iterable - { - return [ - 'not /index.php' => [ - '/test.php', - '/', - ], - '/index.php' => [ - '/index.php', - '/', - ], - ]; - } - /** * @param mixed $path * @param mixed $detectPath @@ -932,6 +918,20 @@ public function testExtensionPHP($path, $detectPath): void $this->assertSame($detectPath, $request->detectPath()); } + public static function provideExtensionPHP(): iterable + { + return [ + 'not /index.php' => [ + '/test.php', + '/', + ], + '/index.php' => [ + '/index.php', + '/', + ], + ]; + } + public function testGetPath(): void { $request = $this->createRequest(null, null, 'fruits/banana'); diff --git a/tests/system/HTTP/MessageTest.php b/tests/system/HTTP/MessageTest.php index 5e3343416b12..28483674fee9 100644 --- a/tests/system/HTTP/MessageTest.php +++ b/tests/system/HTTP/MessageTest.php @@ -189,25 +189,6 @@ public function testSetHeaderArrayValues(): void $this->assertSame('json, html, xml', $this->message->getHeaderLine('Accept')); } - public static function provideArrayHeaderValue(): iterable - { - return [ - 'existing for next not append' => [ - [ - 'json', - 'html', - 'xml', - ], - ], - 'existing for next append' => [ - [ - 'json', - 'html', - ], - ], - ]; - } - /** * @param array $arrayHeaderValue */ @@ -232,6 +213,25 @@ public function testSetHeaderWithExistingArrayValuesAppendArrayValue($arrayHeade $this->assertSame('json, html, xml', $this->message->getHeaderLine('Accept')); } + public static function provideArrayHeaderValue(): iterable + { + return [ + 'existing for next not append' => [ + [ + 'json', + 'html', + 'xml', + ], + ], + 'existing for next append' => [ + [ + 'json', + 'html', + ], + ], + ]; + } + public function testSetHeaderWithExistingArrayValuesAppendNullValue(): void { $this->message->setHeader('Accept', ['json', 'html', 'xml']); diff --git a/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php b/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php index d14a6e5e4151..cbd309dffa7c 100644 --- a/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php +++ b/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php @@ -311,4 +311,109 @@ public static function provideExtensionPHP(): iterable ], ]; } + + #[DataProvider('provideRequestURIRewrite')] + public function testRequestURIRewrite( + string $requestUri, + string $scriptName, + string $indexPage, + string $expected, + ): void { + $server = []; + $server['REQUEST_URI'] = $requestUri; + $server['SCRIPT_NAME'] = $scriptName; + + $appConfig = new App(); + $appConfig->indexPage = $indexPage; + + $factory = $this->createSiteURIFactory($server, $appConfig); + + $this->assertSame($expected, $factory->detectRoutePath('REQUEST_URI')); + } + + /** + * @return iterable + */ + public static function provideRequestURIRewrite(): iterable + { + return [ + 'rewrite_with_route' => [ + 'requestUri' => '/ci/index.php/sample/method', + 'scriptName' => '/ci/public/index.php', + 'indexPage' => 'index.php', + 'expected' => 'sample/method', + ], + 'rewrite_root' => [ + 'requestUri' => '/ci/index.php', + 'scriptName' => '/ci/public/index.php', + 'indexPage' => 'index.php', + 'expected' => '/', + ], + 'rewrite_no_index_page' => [ + 'requestUri' => '/ci/sample/method', + 'scriptName' => '/ci/public/index.php', + 'indexPage' => '', + 'expected' => 'sample/method', + ], + 'rewrite_nested_subfolder' => [ + 'requestUri' => '/projects/index.php/api/users/list', + 'scriptName' => '/projects/myapp/public/index.php', + 'indexPage' => 'index.php', + 'expected' => 'api/users/list', + ], + 'rewrite_multiple_public_folders' => [ + 'requestUri' => '/public-sites/myapp/index.php/content/view', + 'scriptName' => '/public-sites/myapp/public/index.php', + 'indexPage' => 'index.php', + 'expected' => 'content/view', + ], + 'rewrite_custom_app_folder' => [ + 'requestUri' => '/myapp/index.php/products/category/electronics', + 'scriptName' => '/myapp/web/index.php', + 'indexPage' => 'index.php', + 'expected' => 'products/category/electronics', + ], + 'multiple_index_php_in_path' => [ + 'requestUri' => '/app/index.php/user/index.php/profile', + 'scriptName' => '/app/public/index.php', + 'indexPage' => 'index.php', + 'expected' => 'user/index.php/profile', + ], + 'custom_index_page_name' => [ + 'requestUri' => '/ci/app.php/users/list', + 'scriptName' => '/ci/public/app.php', + 'indexPage' => 'app.php', + 'expected' => 'users/list', + ], + 'custom_index_page_root' => [ + 'requestUri' => '/project/main.php', + 'scriptName' => '/project/web/main.php', + 'indexPage' => 'main.php', + 'expected' => '/', + ], + 'partial_match_should_not_remove' => [ + 'requestUri' => '/app/myindex.php/route', + 'scriptName' => '/app/public/index.php', + 'indexPage' => 'index.php', + 'expected' => 'myindex.php/route', + ], + 'multibyte_characters' => [ + 'requestUri' => '/%ED%85%8C%EC%8A%A4%ED%8A%B81/index.php/route', + 'scriptName' => '/테스트1/public/index.php', + 'indexPage' => 'index.php', + 'expected' => 'route', + ], + 'multibyte_characters_with_nested_subfolder' => [ + 'requestUri' => '/%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82/%D1%82%D0%B5%D1%81%D1%821/index.php/route', + 'scriptName' => '/проект/тест1/public/index.php', + 'indexPage' => 'index.php', + 'expected' => 'route', + ], + ]; + } } diff --git a/tests/system/HTTP/SiteURITest.php b/tests/system/HTTP/SiteURITest.php index 26d0017d7c34..1208edaf05b9 100644 --- a/tests/system/HTTP/SiteURITest.php +++ b/tests/system/HTTP/SiteURITest.php @@ -66,6 +66,149 @@ public static function provideConstructor(): iterable return array_merge(self::provideSetPath(), self::provideRelativePathWithQueryOrFragment()); } + public static function provideRelativePathWithQueryOrFragment(): iterable + { + return [ + 'one/two?foo=1&bar=2' => [ + 'http://example.com/', // $baseURL + 'index.php', // $indexPage + 'one/two?foo=1&bar=2', // $relativePath + 'http://example.com/index.php/one/two?foo=1&bar=2', // $expectedURI + 'one/two', // $expectedRoutePath + '/index.php/one/two', // $expectedPath + 'foo=1&bar=2', // $expectedQuery + '', // $expectedFragment + ['one', 'two'], // $expectedSegments + 2, // $expectedTotalSegments + ], + 'one/two#sec1' => [ + 'http://example.com/', + 'index.php', + 'one/two#sec1', + 'http://example.com/index.php/one/two#sec1', + 'one/two', + '/index.php/one/two', + '', + 'sec1', + ['one', 'two'], + 2, + ], + 'one/two?foo=1&bar=2#sec1' => [ + 'http://example.com/', + 'index.php', + 'one/two?foo=1&bar=2#sec1', + 'http://example.com/index.php/one/two?foo=1&bar=2#sec1', + 'one/two', + '/index.php/one/two', + 'foo=1&bar=2', + 'sec1', + ['one', 'two'], + 2, + ], + 'Subfolder: one/two?foo=1&bar=2' => [ + 'http://example.com/ci4/', + 'index.php', + 'one/two?foo=1&bar=2', + 'http://example.com/ci4/index.php/one/two?foo=1&bar=2', + 'one/two', + '/ci4/index.php/one/two', + 'foo=1&bar=2', + '', + ['one', 'two'], + 2, + ], + ]; + } + + public function testConstructorHost(): void + { + $config = new App(); + $config->allowedHostnames = ['sub.example.com']; + + $uri = new SiteURI($config, '', 'sub.example.com'); + + $this->assertInstanceOf(SiteURI::class, $uri); + $this->assertSame('http://sub.example.com/index.php', (string) $uri); + $this->assertSame('', $uri->getRoutePath()); + $this->assertSame('/index.php', $uri->getPath()); + $this->assertSame('http://sub.example.com/', $uri->getBaseURL()); + } + + public function testConstructorScheme(): void + { + $config = new App(); + + $uri = new SiteURI($config, '', null, 'https'); + + $this->assertInstanceOf(SiteURI::class, $uri); + $this->assertSame('https://example.com/index.php', (string) $uri); + $this->assertSame('https://example.com/', $uri->getBaseURL()); + } + + public function testConstructorEmptyScheme(): void + { + $config = new App(); + + $uri = new SiteURI($config, '', null, ''); + + $this->assertInstanceOf(SiteURI::class, $uri); + $this->assertSame('http://example.com/index.php', (string) $uri); + $this->assertSame('http://example.com/', $uri->getBaseURL()); + } + + public function testConstructorForceGlobalSecureRequests(): void + { + $config = new App(); + $config->forceGlobalSecureRequests = true; + + $uri = new SiteURI($config); + + $this->assertSame('https://example.com/index.php', (string) $uri); + $this->assertSame('https://example.com/', $uri->getBaseURL()); + } + + public function testConstructorInvalidBaseURL(): void + { + $this->expectException(ConfigException::class); + + $config = new App(); + $config->baseURL = 'invalid'; + + new SiteURI($config); + } + + #[DataProvider('provideSetPath')] + public function testSetPath( + string $baseURL, + string $indexPage, + string $relativePath, + string $expectedURI, + string $expectedRoutePath, + string $expectedPath, + string $expectedQuery, + string $expectedFragment, + array $expectedSegments, + int $expectedTotalSegments, + ): void { + $config = new App(); + $config->indexPage = $indexPage; + $config->baseURL = $baseURL; + + $uri = new SiteURI($config); + + $uri->setPath($relativePath); + + $this->assertSame($expectedURI, (string) $uri); + $this->assertSame($expectedRoutePath, $uri->getRoutePath()); + $this->assertSame($expectedPath, $uri->getPath()); + $this->assertSame($expectedQuery, $uri->getQuery()); + $this->assertSame($expectedFragment, $uri->getFragment()); + $this->assertSame($baseURL, $uri->getBaseURL()); + + $this->assertSame($expectedSegments, $uri->getSegments()); + $this->assertSame($expectedTotalSegments, $uri->getTotalSegments()); + } + public static function provideSetPath(): iterable { return [ @@ -215,149 +358,6 @@ public static function provideSetPath(): iterable ]; } - public static function provideRelativePathWithQueryOrFragment(): iterable - { - return [ - 'one/two?foo=1&bar=2' => [ - 'http://example.com/', // $baseURL - 'index.php', // $indexPage - 'one/two?foo=1&bar=2', // $relativePath - 'http://example.com/index.php/one/two?foo=1&bar=2', // $expectedURI - 'one/two', // $expectedRoutePath - '/index.php/one/two', // $expectedPath - 'foo=1&bar=2', // $expectedQuery - '', // $expectedFragment - ['one', 'two'], // $expectedSegments - 2, // $expectedTotalSegments - ], - 'one/two#sec1' => [ - 'http://example.com/', - 'index.php', - 'one/two#sec1', - 'http://example.com/index.php/one/two#sec1', - 'one/two', - '/index.php/one/two', - '', - 'sec1', - ['one', 'two'], - 2, - ], - 'one/two?foo=1&bar=2#sec1' => [ - 'http://example.com/', - 'index.php', - 'one/two?foo=1&bar=2#sec1', - 'http://example.com/index.php/one/two?foo=1&bar=2#sec1', - 'one/two', - '/index.php/one/two', - 'foo=1&bar=2', - 'sec1', - ['one', 'two'], - 2, - ], - 'Subfolder: one/two?foo=1&bar=2' => [ - 'http://example.com/ci4/', - 'index.php', - 'one/two?foo=1&bar=2', - 'http://example.com/ci4/index.php/one/two?foo=1&bar=2', - 'one/two', - '/ci4/index.php/one/two', - 'foo=1&bar=2', - '', - ['one', 'two'], - 2, - ], - ]; - } - - public function testConstructorHost(): void - { - $config = new App(); - $config->allowedHostnames = ['sub.example.com']; - - $uri = new SiteURI($config, '', 'sub.example.com'); - - $this->assertInstanceOf(SiteURI::class, $uri); - $this->assertSame('http://sub.example.com/index.php', (string) $uri); - $this->assertSame('', $uri->getRoutePath()); - $this->assertSame('/index.php', $uri->getPath()); - $this->assertSame('http://sub.example.com/', $uri->getBaseURL()); - } - - public function testConstructorScheme(): void - { - $config = new App(); - - $uri = new SiteURI($config, '', null, 'https'); - - $this->assertInstanceOf(SiteURI::class, $uri); - $this->assertSame('https://example.com/index.php', (string) $uri); - $this->assertSame('https://example.com/', $uri->getBaseURL()); - } - - public function testConstructorEmptyScheme(): void - { - $config = new App(); - - $uri = new SiteURI($config, '', null, ''); - - $this->assertInstanceOf(SiteURI::class, $uri); - $this->assertSame('http://example.com/index.php', (string) $uri); - $this->assertSame('http://example.com/', $uri->getBaseURL()); - } - - public function testConstructorForceGlobalSecureRequests(): void - { - $config = new App(); - $config->forceGlobalSecureRequests = true; - - $uri = new SiteURI($config); - - $this->assertSame('https://example.com/index.php', (string) $uri); - $this->assertSame('https://example.com/', $uri->getBaseURL()); - } - - public function testConstructorInvalidBaseURL(): void - { - $this->expectException(ConfigException::class); - - $config = new App(); - $config->baseURL = 'invalid'; - - new SiteURI($config); - } - - #[DataProvider('provideSetPath')] - public function testSetPath( - string $baseURL, - string $indexPage, - string $relativePath, - string $expectedURI, - string $expectedRoutePath, - string $expectedPath, - string $expectedQuery, - string $expectedFragment, - array $expectedSegments, - int $expectedTotalSegments, - ): void { - $config = new App(); - $config->indexPage = $indexPage; - $config->baseURL = $baseURL; - - $uri = new SiteURI($config); - - $uri->setPath($relativePath); - - $this->assertSame($expectedURI, (string) $uri); - $this->assertSame($expectedRoutePath, $uri->getRoutePath()); - $this->assertSame($expectedPath, $uri->getPath()); - $this->assertSame($expectedQuery, $uri->getQuery()); - $this->assertSame($expectedFragment, $uri->getFragment()); - $this->assertSame($baseURL, $uri->getBaseURL()); - - $this->assertSame($expectedSegments, $uri->getSegments()); - $this->assertSame($expectedTotalSegments, $uri->getTotalSegments()); - } - public function testSetSegment(): void { $config = new App(); diff --git a/tests/system/HTTP/URITest.php b/tests/system/HTTP/URITest.php index 945b132876ef..3199d9c1cb25 100644 --- a/tests/system/HTTP/URITest.php +++ b/tests/system/HTTP/URITest.php @@ -468,6 +468,19 @@ public static function provideSetPath(): iterable ]; } + /** + * @param string $path + * @param string $expected + */ + #[DataProvider('providePathGetsFiltered')] + public function testPathGetsFiltered($path, $expected): void + { + $uri = new URI(); + $uri->setPath($path); + + $this->assertSame($expected, $uri->getPath()); + } + public static function providePathGetsFiltered(): iterable { return [ @@ -510,19 +523,6 @@ public static function providePathGetsFiltered(): iterable ]; } - /** - * @param string $path - * @param string $expected - */ - #[DataProvider('providePathGetsFiltered')] - public function testPathGetsFiltered($path, $expected): void - { - $uri = new URI(); - $uri->setPath($path); - - $this->assertSame($expected, $uri->getPath()); - } - public function testSetFragmentSetsValue(): void { $url = 'http://example.com/path'; @@ -603,6 +603,18 @@ public function testSetQueryThrowsErrorWhenFragmentPresentSilent(): void $this->assertSame('', $uri->getQuery()); } + /** + * @param string $url + * @param string $expected + */ + #[DataProvider('provideAuthorityReturnsExceptedValues')] + public function testAuthorityReturnsExceptedValues($url, $expected): void + { + $uri = new URI($url); + + $this->assertSame($expected, $uri->getAuthority()); + } + public static function provideAuthorityReturnsExceptedValues(): iterable { return [ @@ -622,19 +634,37 @@ public static function provideAuthorityReturnsExceptedValues(): iterable 'http://me@foo.com:3000/bar', 'me@foo.com:3000', ], + 'rtsp-with-port' => [ + 'rtsp://localhost:1234/stream', + 'localhost:1234', + ], + 'rtsp-no-port' => [ + 'rtsp://localhost/stream', + 'localhost', + ], + 'custom-scheme-with-port' => [ + 'myscheme://server:9999/resource', + 'server:9999', + ], + 'custom-scheme-no-port' => [ + 'myscheme://server/resource', + 'server', + ], ]; } /** - * @param string $url - * @param string $expected + * @param string $scheme + * @param int $port */ - #[DataProvider('provideAuthorityReturnsExceptedValues')] - public function testAuthorityReturnsExceptedValues($url, $expected): void + #[DataProvider('provideAuthorityRemovesDefaultPorts')] + public function testAuthorityRemovesDefaultPorts($scheme, $port): void { + $url = "{$scheme}://example.com:{$port}/path"; $uri = new URI($url); - $this->assertSame($expected, $uri->getAuthority()); + $expected = "{$scheme}://example.com/path"; + $this->assertSame($expected, (string) $uri); } public static function provideAuthorityRemovesDefaultPorts(): iterable @@ -651,20 +681,6 @@ public static function provideAuthorityRemovesDefaultPorts(): iterable ]; } - /** - * @param string $scheme - * @param int $port - */ - #[DataProvider('provideAuthorityRemovesDefaultPorts')] - public function testAuthorityRemovesDefaultPorts($scheme, $port): void - { - $url = "{$scheme}://example.com:{$port}/path"; - $uri = new URI($url); - - $expected = "{$scheme}://example.com/path"; - $this->assertSame($expected, (string) $uri); - } - public function testSetAuthorityReconstitutes(): void { $authority = 'me@foo.com:3000'; @@ -675,6 +691,16 @@ public function testSetAuthorityReconstitutes(): void $this->assertSame($authority, $uri->getAuthority()); } + /** + * @param string $path + * @param string $expected + */ + #[DataProvider('provideRemoveDotSegments')] + public function testRemoveDotSegments($path, $expected): void + { + $this->assertSame($expected, URI::removeDotSegments($path)); + } + public static function provideRemoveDotSegments(): iterable { return [ @@ -774,13 +800,35 @@ public static function provideRemoveDotSegments(): iterable } /** - * @param string $path + * @param string $rel * @param string $expected */ - #[DataProvider('provideRemoveDotSegments')] - public function testRemoveDotSegments($path, $expected): void + #[DataProvider('defaultResolutions')] + public function testResolveRelativeURI($rel, $expected): void { - $this->assertSame($expected, URI::removeDotSegments($path)); + $base = 'http://a/b/c/d'; + $uri = new URI($base); + + $new = $uri->resolveRelativeURI($rel); + + $this->assertSame($expected, (string) $new); + } + + /** + * @param string $rel + * @param string $expected + */ + #[DataProvider('defaultResolutions')] + public function testResolveRelativeURIHTTPS($rel, $expected): void + { + $base = 'https://a/b/c/d'; + $expected = str_replace('http:', 'https:', $expected); + + $uri = new URI($base); + + $new = $uri->resolveRelativeURI($rel); + + $this->assertSame($expected, (string) $new); } public static function defaultResolutions(): iterable @@ -813,38 +861,6 @@ public static function defaultResolutions(): iterable ]; } - /** - * @param string $rel - * @param string $expected - */ - #[DataProvider('defaultResolutions')] - public function testResolveRelativeURI($rel, $expected): void - { - $base = 'http://a/b/c/d'; - $uri = new URI($base); - - $new = $uri->resolveRelativeURI($rel); - - $this->assertSame($expected, (string) $new); - } - - /** - * @param string $rel - * @param string $expected - */ - #[DataProvider('defaultResolutions')] - public function testResolveRelativeURIHTTPS($rel, $expected): void - { - $base = 'https://a/b/c/d'; - $expected = str_replace('http:', 'https:', $expected); - - $uri = new URI($base); - - $new = $uri->resolveRelativeURI($rel); - - $this->assertSame($expected, (string) $new); - } - public function testResolveRelativeURIWithNoBase(): void { $base = 'http://a'; @@ -1226,4 +1242,17 @@ public function testForceGlobalSecureRequestsAndNonHTTPProtocol(): void $this->assertSame($expected, (string) $uri); } + + /** + * @see https://github.com/codeigniter4/CodeIgniter4/issues/9604 + */ + public function testAuthorityIncludesPortForCustomSchemes(): void + { + $url = 'rtsp://localhost:1234/stream'; + $uri = new URI($url); + + $this->assertSame('rtsp://localhost:1234/stream', (string) $uri); + $this->assertSame('localhost:1234', $uri->getAuthority()); + $this->assertSame(1234, $uri->getPort()); + } } diff --git a/tests/system/Helpers/ArrayHelperTest.php b/tests/system/Helpers/ArrayHelperTest.php index 77079d2a5a2b..0293cc6c826e 100644 --- a/tests/system/Helpers/ArrayHelperTest.php +++ b/tests/system/Helpers/ArrayHelperTest.php @@ -241,6 +241,32 @@ public function testArrayDeepSearch($key, $expected): void $this->assertSame($expected, $result); } + public static function provideArrayDeepSearch(): iterable + { + return [ + [ + 'key6441', + 'Value 6.4.4.1', + ], + [ + 'key64421', + null, + ], + [ + 42, + 'Value 42', + ], + [ + 'key644', + ['key6441' => 'Value 6.4.4.1'], + ], + [ + '', + null, + ], + ]; + } + public function testArrayDeepSearchReturnNullEmptyArray(): void { $data = []; @@ -308,32 +334,6 @@ public function testArraySortByMultipleKeysFailsInconsistentArraySizes($data): v array_sort_by_multiple_keys($data, $sortColumns); } - public static function provideArrayDeepSearch(): iterable - { - return [ - [ - 'key6441', - 'Value 6.4.4.1', - ], - [ - 'key64421', - null, - ], - [ - 42, - 'Value 42', - ], - [ - 'key644', - ['key6441' => 'Value 6.4.4.1'], - ], - [ - '', - null, - ], - ]; - } - public static function provideSortByMultipleKeys(): iterable { $seed = [ @@ -493,14 +493,6 @@ public function testArrayGroupByIncludeEmpty(array $indexes, array $data, array $this->assertSame($expected, $actual, 'array including empty not the same'); } - #[DataProvider('provideArrayGroupByExcludeEmpty')] - public function testArrayGroupByExcludeEmpty(array $indexes, array $data, array $expected): void - { - $actual = array_group_by($data, $indexes, false); - - $this->assertSame($expected, $actual, 'array excluding empty not the same'); - } - public static function provideArrayGroupByIncludeEmpty(): iterable { yield 'simple group-by test' => [ @@ -912,6 +904,14 @@ public static function provideArrayGroupByIncludeEmpty(): iterable ]; } + #[DataProvider('provideArrayGroupByExcludeEmpty')] + public function testArrayGroupByExcludeEmpty(array $indexes, array $data, array $expected): void + { + $actual = array_group_by($data, $indexes, false); + + $this->assertSame($expected, $actual, 'array excluding empty not the same'); + } + public static function provideArrayGroupByExcludeEmpty(): iterable { yield 'simple group-by test' => [ diff --git a/tests/system/Helpers/InflectorHelperTest.php b/tests/system/Helpers/InflectorHelperTest.php index f0df11977126..59068b8bf751 100644 --- a/tests/system/Helpers/InflectorHelperTest.php +++ b/tests/system/Helpers/InflectorHelperTest.php @@ -246,6 +246,12 @@ public function testDasherize(): void } } + #[DataProvider('provideOrdinal')] + public function testOrdinal(string $suffix, int $number): void + { + $this->assertSame($suffix, ordinal($number)); + } + public static function provideOrdinal(): iterable { return [ @@ -262,12 +268,6 @@ public static function provideOrdinal(): iterable ]; } - #[DataProvider('provideOrdinal')] - public function testOrdinal(string $suffix, int $number): void - { - $this->assertSame($suffix, ordinal($number)); - } - public function testOrdinalize(): void { $suffixedNumbers = [ diff --git a/tests/system/Helpers/URLHelper/CurrentUrlTest.php b/tests/system/Helpers/URLHelper/CurrentUrlTest.php index 1c7de84586e2..b802db349949 100644 --- a/tests/system/Helpers/URLHelper/CurrentUrlTest.php +++ b/tests/system/Helpers/URLHelper/CurrentUrlTest.php @@ -242,6 +242,44 @@ public function testUriStringSubfolderRelative(): void $this->assertSame('assets/image.jpg', uri_string()); } + #[DataProvider('provideUrlIs')] + public function testUrlIs(string $currentPath, string $testPath, bool $expected): void + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/' . $currentPath; + + $this->createRequest($this->config); + + $this->assertSame($expected, url_is($testPath)); + } + + #[DataProvider('provideUrlIs')] + public function testUrlIsNoIndex(string $currentPath, string $testPath, bool $expected): void + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/' . $currentPath; + + $this->config->indexPage = ''; + + $this->createRequest($this->config); + + $this->assertSame($expected, url_is($testPath)); + } + + #[DataProvider('provideUrlIs')] + public function testUrlIsWithSubfolder(string $currentPath, string $testPath, bool $expected): void + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/' . $currentPath; + $_SERVER['SCRIPT_NAME'] = '/subfolder/index.php'; + + $this->config->baseURL = 'http://example.com/subfolder/'; + + $this->createRequest($this->config); + + $this->assertSame($expected, url_is($testPath)); + } + public static function provideUrlIs(): iterable { return [ @@ -282,42 +320,4 @@ public static function provideUrlIs(): iterable ], ]; } - - #[DataProvider('provideUrlIs')] - public function testUrlIs(string $currentPath, string $testPath, bool $expected): void - { - $_SERVER['HTTP_HOST'] = 'example.com'; - $_SERVER['REQUEST_URI'] = '/' . $currentPath; - - $this->createRequest($this->config); - - $this->assertSame($expected, url_is($testPath)); - } - - #[DataProvider('provideUrlIs')] - public function testUrlIsNoIndex(string $currentPath, string $testPath, bool $expected): void - { - $_SERVER['HTTP_HOST'] = 'example.com'; - $_SERVER['REQUEST_URI'] = '/' . $currentPath; - - $this->config->indexPage = ''; - - $this->createRequest($this->config); - - $this->assertSame($expected, url_is($testPath)); - } - - #[DataProvider('provideUrlIs')] - public function testUrlIsWithSubfolder(string $currentPath, string $testPath, bool $expected): void - { - $_SERVER['HTTP_HOST'] = 'example.com'; - $_SERVER['REQUEST_URI'] = '/' . $currentPath; - $_SERVER['SCRIPT_NAME'] = '/subfolder/index.php'; - - $this->config->baseURL = 'http://example.com/subfolder/'; - - $this->createRequest($this->config); - - $this->assertSame($expected, url_is($testPath)); - } } diff --git a/tests/system/Helpers/URLHelper/MiscUrlTest.php b/tests/system/Helpers/URLHelper/MiscUrlTest.php index 78c7fd5b0d24..81ec9770ea2a 100644 --- a/tests/system/Helpers/URLHelper/MiscUrlTest.php +++ b/tests/system/Helpers/URLHelper/MiscUrlTest.php @@ -128,6 +128,21 @@ public function testIndexPageAlt(): void $this->assertSame('banana.php', index_page($this->config)); } + /** + * @param mixed $expected + * @param mixed $uri + * @param mixed $title + * @param mixed $attributes + */ + #[DataProvider('provideAnchor')] + public function testAnchor($expected = '', $uri = '', $title = '', $attributes = ''): void + { + $uriString = 'http://example.com/'; + $this->createRequest($uriString); + + $this->assertSame($expected, anchor($uri, $title, $attributes, $this->config)); + } + // Test anchor public static function provideAnchor(): iterable @@ -179,9 +194,11 @@ public static function provideAnchor(): iterable * @param mixed $title * @param mixed $attributes */ - #[DataProvider('provideAnchor')] - public function testAnchor($expected = '', $uri = '', $title = '', $attributes = ''): void + #[DataProvider('provideAnchorNoindex')] + public function testAnchorNoindex($expected = '', $uri = '', $title = '', $attributes = ''): void { + $this->config->indexPage = ''; + $uriString = 'http://example.com/'; $this->createRequest($uriString); @@ -243,8 +260,8 @@ public static function provideAnchorNoindex(): iterable * @param mixed $title * @param mixed $attributes */ - #[DataProvider('provideAnchorNoindex')] - public function testAnchorNoindex($expected = '', $uri = '', $title = '', $attributes = ''): void + #[DataProvider('provideAnchorTargetted')] + public function testAnchorTargetted($expected = '', $uri = '', $title = '', $attributes = ''): void { $this->config->indexPage = ''; @@ -299,11 +316,9 @@ public static function provideAnchorTargetted(): iterable * @param mixed $title * @param mixed $attributes */ - #[DataProvider('provideAnchorTargetted')] - public function testAnchorTargetted($expected = '', $uri = '', $title = '', $attributes = ''): void + #[DataProvider('provideAnchorExamples')] + public function testAnchorExamples($expected = '', $uri = '', $title = '', $attributes = ''): void { - $this->config->indexPage = ''; - $uriString = 'http://example.com/'; $this->createRequest($uriString); @@ -344,13 +359,13 @@ public static function provideAnchorExamples(): iterable * @param mixed $title * @param mixed $attributes */ - #[DataProvider('provideAnchorExamples')] - public function testAnchorExamples($expected = '', $uri = '', $title = '', $attributes = ''): void + #[DataProvider('provideAnchorPopup')] + public function testAnchorPopup($expected = '', $uri = '', $title = '', $attributes = false): void { $uriString = 'http://example.com/'; $this->createRequest($uriString); - $this->assertSame($expected, anchor($uri, $title, $attributes, $this->config)); + $this->assertSame($expected, anchor_popup($uri, $title, $attributes, $this->config)); } // Test anchor_popup @@ -397,17 +412,17 @@ public static function provideAnchorPopup(): iterable /** * @param mixed $expected - * @param mixed $uri + * @param mixed $email * @param mixed $title * @param mixed $attributes */ - #[DataProvider('provideAnchorPopup')] - public function testAnchorPopup($expected = '', $uri = '', $title = '', $attributes = false): void + #[DataProvider('provideMailto')] + public function testMailto($expected = '', $email = '', $title = '', $attributes = ''): void { $uriString = 'http://example.com/'; $this->createRequest($uriString); - $this->assertSame($expected, anchor_popup($uri, $title, $attributes, $this->config)); + $this->assertSame($expected, mailto($email, $title, $attributes)); } // Test mailto @@ -439,13 +454,13 @@ public static function provideMailto(): iterable * @param mixed $title * @param mixed $attributes */ - #[DataProvider('provideMailto')] - public function testMailto($expected = '', $email = '', $title = '', $attributes = ''): void + #[DataProvider('provideSafeMailto')] + public function testSafeMailto($expected = '', $email = '', $title = '', $attributes = ''): void { $uriString = 'http://example.com/'; $this->createRequest($uriString); - $this->assertSame($expected, mailto($email, $title, $attributes)); + $this->assertSame($expected, safe_mailto($email, $title, $attributes)); } // Test safe_mailto @@ -471,21 +486,6 @@ public static function provideSafeMailto(): iterable ]; } - /** - * @param mixed $expected - * @param mixed $email - * @param mixed $title - * @param mixed $attributes - */ - #[DataProvider('provideSafeMailto')] - public function testSafeMailto($expected = '', $email = '', $title = '', $attributes = ''): void - { - $uriString = 'http://example.com/'; - $this->createRequest($uriString); - - $this->assertSame($expected, safe_mailto($email, $title, $attributes)); - } - public function testSafeMailtoWithCsp(): void { $this->config->CSPEnabled = true; @@ -496,6 +496,16 @@ public function testSafeMailtoWithCsp(): void $this->assertMatchesRegularExpression('/