diff --git a/.gitattributes b/.gitattributes
index 30832ef77b1..237fac3f68d 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -34,16 +34,34 @@ tests/TestCase export-ignore
.editorconfig export-ignore
.gitattributes export-ignore
.gitignore export-ignore
+.git-blame-ignore-revs export-ignore
.mailmap export-ignore
-.stickler.yml export-ignore
Makefile export-ignore
phpcs.xml export-ignore
phpstan.neon.dist export-ignore
phpstan-baseline.neon export-ignore
phpunit.xml.dist export-ignore
+psalm.xml export-ignore
+psalm-baseline.xml export-ignore
rector.php export-ignore
+tests/composer.lock export-ignore
+tests/phpstan.neon export-ignore
+tests/phpstan-baseline.neon export-ignore
+
# Split package files
+src/Database/.gitattributes export-ignore
+src/Database/phpstan.neon.dist export-ignore
+src/Database/tests/ export-ignore
+src/Datasource/.gitattributes export-ignore
+src/Datasource/phpstan.neon.dist export-ignore
+src/Datasource/tests/ export-ignore
+src/Http/.gitattributes export-ignore
+src/Http/phpstan.neon.dist export-ignore
+src/Http/tests/ export-ignore
+src/ORM/.gitattributes export-ignore
+src/ORM/phpstan.neon.dist export-ignore
+src/ORM/tests/ export-ignore
src/Validation/.gitattributes export-ignore
src/Validation/phpstan.neon.dist export-ignore
src/Validation/tests/ export-ignore
diff --git a/.github/SECURITY.md b/.github/SECURITY.md
index 6b168892c37..a61b2a1656d 100644
--- a/.github/SECURITY.md
+++ b/.github/SECURITY.md
@@ -2,19 +2,7 @@
## Supported Versions
-We support fixing security issues on the following releases:
-
-| Version | Supported | Security fixes until
-| ------- | ------------------ | --------------------
-| 5.0 | :white_check_mark: | The release of 5.2
-| 4.5 | :white_check_mark: | 36 Months after the release of 5.0 (09 Sep 2026)
-| 4.4 | :white_check_mark: | 36 Months after the release of 5.0 (09 Sep 2026)
-| 4.3 | :white_check_mark: | 36 Months after the release of 5.0 (09 Sep 2026)
-| 4.2 | :x: | No longer supported
-| 4.1 | :x: | No longer supported
-| 4.0 | :x: | No longer supported
-| 3.10.x | :x: | No longer supported
-| 2.10.x | :x: | No longer supported
+The supported version list can be found in [GitHub wiki](https://github.com/cakephp/cakephp/wiki#supported-versions).
## Reporting a Vulnerability
@@ -32,3 +20,4 @@ the CakePHP team will take the following actions:
* Prepare a post describing the vulnerability, and the possible exploits.
* Release new versions of all affected versions.
* Prominently feature the problem in the release announcement
+
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 6647c42863e..78cb4271e62 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -5,8 +5,12 @@ updates:
schedule:
interval: weekly
open-pull-requests-limit: 10
+ cooldown:
+ default-days: 7
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 10
+ cooldown:
+ default-days: 7
diff --git a/.github/workflows/api-docs.yml b/.github/workflows/api-docs.yml
index 6a4e37236dd..495d772f691 100644
--- a/.github/workflows/api-docs.yml
+++ b/.github/workflows/api-docs.yml
@@ -22,9 +22,11 @@ jobs:
private_key: ${{ secrets.CAKEBOT_APP_PRIVATE_KEY }}
- name: Trigger API build
+ env:
+ APP_TOKEN: ${{ steps.app-token.outputs.token }}
run: >
curl -XPOST
- -H 'Authorization: Bearer ${{ steps.app-token.outputs.token }}'
+ -H "Authorization: Bearer ${APP_TOKEN}"
-H 'Accept: application/vnd.github.v3+json'
-H 'Content-Type: application/json'
https://api.github.com/repos/cakephp/cakephp-api-docs/actions/workflows/deploy_2x.yml/dispatches
diff --git a/.github/workflows/cancel.yml b/.github/workflows/cancel.yml
deleted file mode 100644
index 263c669071d..00000000000
--- a/.github/workflows/cancel.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-name: Cancel
-on:
- workflow_run:
- workflows: ["CI"]
- types:
- - requested
-permissions:
- contents: read
-
-jobs:
- cancel:
- permissions:
- actions: write # for styfle/cancel-workflow-action to cancel/stop running workflows
- runs-on: ubuntu-latest
- steps:
- - uses: styfle/cancel-workflow-action@0.13.1
- with:
- workflow_id: ${{ github.event.workflow.id }}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index bd265c7261f..f68d22c38e5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,6 +11,10 @@ on:
- '*'
workflow_dispatch:
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
permissions:
contents: read # to fetch code (actions/checkout)
@@ -110,7 +114,7 @@ jobs:
sleep 2
done
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
with:
persist-credentials: false
@@ -164,7 +168,7 @@ jobs:
- name: Submit code coverage
if: matrix.php-version == '8.2'
- uses: codecov/codecov-action@v6
+ uses: codecov/codecov-action@v7
with:
files: coverage.xml,coverage-database.xml,coverage-functions.xml
token: ${{ secrets.CODECOV_TOKEN }}
@@ -178,7 +182,7 @@ jobs:
PHP_VERSION: '8.2'
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
with:
persist-credentials: false
@@ -195,7 +199,7 @@ jobs:
key: ${{ steps.key-date.outputs.date }}
- name: Cache PHP extensions
- uses: actions/cache@v5
+ uses: actions/cache@v6.1.0
with:
path: ${{ steps.php-ext-cache.outputs.dir }}
key: ${{ runner.os }}-php-ext-${{ steps.php-ext-cache.outputs.key }}
@@ -242,7 +246,7 @@ jobs:
PHPSTAN_TESTS: 1
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
with:
persist-credentials: false
@@ -260,7 +264,7 @@ jobs:
uses: ramsey/composer-install@v4
- name: Cache phive tools
- uses: actions/cache@v5
+ uses: actions/cache@v6.1.0
with:
path: |
~/.phive
@@ -283,7 +287,7 @@ jobs:
- name: Setup phpcs cache
if: always()
- uses: actions/cache@v5
+ uses: actions/cache@v6.1.0
with:
path: ${{ runner.temp }}/phpcs.cache
key: ${{ runner.os }}-phpcs-${{ hashFiles('phpcs.xml', 'composer.lock') }}
@@ -311,7 +315,7 @@ jobs:
- name: Setup rector cache
if: always()
- uses: actions/cache@v5
+ uses: actions/cache@v6.1.0
with:
path: ${{ runner.temp }}/rector
key: ${{ runner.os }}-rector-${{ hashFiles('rector.php', 'composer.lock') }}
@@ -335,7 +339,7 @@ jobs:
PHIVE_KEYS: 'CF1A108D0E7AE720,51C67305FFC2E5C0,12CE0F1D262429A5,99BF4D9A33D65E1E'
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
with:
persist-credentials: false
@@ -353,7 +357,7 @@ jobs:
uses: ramsey/composer-install@v4
- name: Cache phive tools
- uses: actions/cache@v5
+ uses: actions/cache@v6.1.0
with:
path: |
~/.phive
diff --git a/.github/workflows/split-packages.yml b/.github/workflows/split-packages.yml
index 3d1dea6588f..927da3474d3 100644
--- a/.github/workflows/split-packages.yml
+++ b/.github/workflows/split-packages.yml
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
@@ -28,7 +28,8 @@ jobs:
- name: Push split packages
env:
+ CURRENT_BRANCH: ${{ github.ref_name }}
GITHUB_TOKEN: ${{ secrets.GH_SPLIT_PACKAGES_WRITE_TOKEN }}
run: |
- make CURRENT_BRANCH=${{ github.ref_name }} components
+ make CURRENT_BRANCH="${CURRENT_BRANCH}" components
make clean-components-branches
diff --git a/.phive/phars.xml b/.phive/phars.xml
index f648a9f744f..a1053000514 100644
--- a/.phive/phars.xml
+++ b/.phive/phars.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/Makefile b/Makefile
index 6227133dc26..222bece920b 100644
--- a/Makefile
+++ b/Makefile
@@ -146,7 +146,7 @@ package: clean dist/cakephp-$(DASH_VERSION).zip
# Publish app skeleton with dependencies zipballs to Github.
publish: guard-VERSION dist/cakephp-$(DASH_VERSION).zip
@echo "Creating draft release for $(VERSION). prerelease=$(PRERELEASE)"
- curl $(AUTH) -XPOST $(API_HOST)/repos/$(OWNER)/cakephp/releases -d '{"tag_name": "$(VERSION)", "name": "CakePHP $(VERSION) released", "draft": true, "prerelease": $(PRERELEASE)}' > release.json
+ curl $(AUTH) -XPOST $(API_HOST)/repos/$(OWNER)/cakephp/releases -d '{"tag_name": "$(VERSION)", "name": "CakePHP $(VERSION)", "draft": true, "prerelease": $(PRERELEASE)}' > release.json
# Extract id out of response json.
php -r '$$f = file_get_contents("./release.json"); $$d = json_decode($$f, true); file_put_contents("./id.txt", $$d["id"]);'
@echo "Uploading zip file to github."
diff --git a/VERSION.txt b/VERSION.txt
index 53a18289616..f91b5944c90 100644
--- a/VERSION.txt
+++ b/VERSION.txt
@@ -16,4 +16,4 @@
// @license https://opensource.org/licenses/mit-license.php MIT License
// +--------------------------------------------------------------------------------------------+ //
////////////////////////////////////////////////////////////////////////////////////////////////////
-5.3.6
+5.3.7
diff --git a/composer.json b/composer.json
index 23989ffa749..e64e685c727 100644
--- a/composer.json
+++ b/composer.json
@@ -134,7 +134,7 @@
"psalm-baseline": "tools/psalm --set-baseline=psalm-baseline.xml",
"lowest": "validate-prefer-lowest",
"lowest-setup": "composer update --prefer-lowest --prefer-stable --prefer-dist --no-interaction && cp composer.json composer.backup && composer require --dev dereuromark/composer-prefer-lowest && mv composer.backup composer.json",
- "rector-setup": "cp composer.json composer.backup && composer require --dev rector/rector:\"~2.3.1\" && mv composer.backup composer.json",
+ "rector-setup": "cp composer.json composer.backup && composer require --dev rector/rector:\"~2.4.0\" && mv composer.backup composer.json",
"rector-check": "vendor/bin/rector process --dry-run",
"rector-fix": "vendor/bin/rector process",
"test": "phpunit",
diff --git a/phpcs.xml b/phpcs.xml
index 71c1b80d43f..2f409ba72bb 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -16,6 +16,17 @@
+
+
+
+
+
+
+
+
+
+ tests/test_app/*
+
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index 6fe203586e1..ab3fafb2b3e 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -54,12 +54,6 @@ parameters:
count: 8
path: src/Database/Expression/QueryExpression.php
- -
- message: '#^Strict comparison using \!\=\= between Cake\\Database\\TypeMap and null will always evaluate to true\.$#'
- identifier: notIdentical.alwaysTrue
- count: 1
- path: src/Database/Expression/WhenThenExpression.php
-
-
message: '#^Dead catch \- InvalidArgumentException is never thrown in the try block\.$#'
identifier: catch.neverThrown
diff --git a/psalm-baseline.xml b/psalm-baseline.xml
index 13c9f2e5f1f..ad810f56aca 100644
--- a/psalm-baseline.xml
+++ b/psalm-baseline.xml
@@ -1,10 +1,5 @@
-
-
-
-
-
_config]]>
-
-
-
-
-
_fields[$field]]]>
diff --git a/psalm.xml b/psalm.xml
index 53050d23b63..7b40fa17225 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -27,10 +27,5 @@
-
-
-
-
-
diff --git a/rector.php b/rector.php
index 4f09b3f229f..4d4c50afc6d 100644
--- a/rector.php
+++ b/rector.php
@@ -127,4 +127,10 @@
\Rector\Php73\Rector\FuncCall\ArrayKeyFirstLastRector::class,
\Rector\Php80\Rector\FuncCall\ClassOnObjectRector::class,
\Rector\CodeQuality\Rector\Ternary\SwitchNegatedTernaryRector::class,
+
+ // Newly aggressive in rector 2.4 - keep the bump behavior-neutral:
+ // adds declare(strict_types=1) to test fixtures/config (out of scope here),
+ \Rector\TypeDeclaration\Rector\StmtsAwareInterface\SafeDeclareStrictTypesRector::class,
+ // and rewrites `$x ?: []` in ways that can change behavior on undefined/empty values.
+ \Rector\DeadCode\Rector\Ternary\RemoveUselessTernaryRector::class,
]);
diff --git a/src/Cache/Cache.php b/src/Cache/Cache.php
index 13f5b256606..f2349c823f9 100644
--- a/src/Cache/Cache.php
+++ b/src/Cache/Cache.php
@@ -72,8 +72,7 @@ class Cache
* An array mapping URL schemes to fully qualified caching engine
* class names.
*
- * @var array
- * @phpstan-var array
+ * @var array
*/
protected static array $_dsnClassMap = [
'array' => Engine\ArrayEngine::class,
diff --git a/src/Cache/Engine/RedisEngine.php b/src/Cache/Engine/RedisEngine.php
index f7d6f06ffdb..d35fccd9bf9 100644
--- a/src/Cache/Engine/RedisEngine.php
+++ b/src/Cache/Engine/RedisEngine.php
@@ -86,6 +86,12 @@ class RedisEngine extends CacheEngine
* - `clearUsesFlushDb` Enable clear() and clearBlocking() to use FLUSHDB. This will be
* faster than standard clear()/clearBlocking() but will ignore prefixes and will
* cause dataloss if other applications are sharing a redis database.
+ * - `allowedClasses` Controls the `allowed_classes` option passed to `unserialize()`
+ * when reading values back. Set to `false` to disallow all object unserialization
+ * (safest when the cache only stores scalar/array values), or provide an array of
+ * fully qualified class names to allow only those classes. Useful for hardening
+ * against PHP object injection when a cache backend is shared across applications.
+ * Defaults to `true` (allow all) for backwards compatibility.
*
* @var array
*/
@@ -108,6 +114,7 @@ class RedisEngine extends CacheEngine
'nodes' => [],
'failover' => null,
'clearUsesFlushDb' => false,
+ 'allowedClasses' => true,
];
/**
@@ -669,6 +676,11 @@ protected function unserialize(string $value): mixed
return (int)$value;
}
+ $allowedClasses = $this->getConfig('allowedClasses');
+ if ($allowedClasses !== true) {
+ return unserialize($value, ['allowed_classes' => $allowedClasses]);
+ }
+
return unserialize($value);
}
diff --git a/src/Collection/CollectionInterface.php b/src/Collection/CollectionInterface.php
index e28527760e0..4f876f0bad3 100644
--- a/src/Collection/CollectionInterface.php
+++ b/src/Collection/CollectionInterface.php
@@ -811,8 +811,7 @@ public function insert(string $path, mixed $values): CollectionInterface;
* collection as the array keys. Keep in mind that it is valid for iterators
* to return the same key for different elements, setting this value to false
* can help getting all items if keys are not important in the result.
- * @phpstan-return ($keepKeys is true ? array : array)
- * @return array|array
+ * @return ($keepKeys is true ? array : array)
*/
public function toArray(bool $keepKeys = true): array;
diff --git a/src/Collection/Iterator/TreeIterator.php b/src/Collection/Iterator/TreeIterator.php
index c4286fbf646..beb961004b3 100644
--- a/src/Collection/Iterator/TreeIterator.php
+++ b/src/Collection/Iterator/TreeIterator.php
@@ -38,8 +38,7 @@ class TreeIterator extends RecursiveIteratorIterator implements CollectionInterf
/**
* The iteration mode
*
- * @var int
- * @phpstan-var \RecursiveIteratorIterator::LEAVES_ONLY|\RecursiveIteratorIterator::SELF_FIRST|\RecursiveIteratorIterator::CHILD_FIRST
+ * @var \RecursiveIteratorIterator::LEAVES_ONLY|\RecursiveIteratorIterator::SELF_FIRST|\RecursiveIteratorIterator::CHILD_FIRST
*/
protected int $_mode;
@@ -47,10 +46,8 @@ class TreeIterator extends RecursiveIteratorIterator implements CollectionInterf
* Constructor
*
* @param \RecursiveIterator $items The iterator to flatten.
- * @param int $mode Iterator mode.
- * @param int $flags Iterator flags.
- * @phpstan-param \RecursiveIteratorIterator::LEAVES_ONLY|\RecursiveIteratorIterator::SELF_FIRST|\RecursiveIteratorIterator::CHILD_FIRST $mode
- * @phpstan-param \RecursiveIteratorIterator::LEAVES_ONLY|\RecursiveIteratorIterator::CATCH_GET_CHILD $flags
+ * @param \RecursiveIteratorIterator::LEAVES_ONLY|\RecursiveIteratorIterator::SELF_FIRST|\RecursiveIteratorIterator::CHILD_FIRST $mode Iterator mode.
+ * @param \RecursiveIteratorIterator::LEAVES_ONLY|\RecursiveIteratorIterator::CATCH_GET_CHILD $flags Iterator flags.
*/
public function __construct(
RecursiveIterator $items,
diff --git a/src/Collection/Iterator/TreePrinter.php b/src/Collection/Iterator/TreePrinter.php
index 9bc6e709765..95e73dbcc5a 100644
--- a/src/Collection/Iterator/TreePrinter.php
+++ b/src/Collection/Iterator/TreePrinter.php
@@ -73,8 +73,7 @@ class TreePrinter extends RecursiveIteratorIterator implements CollectionInterfa
* callable returning the key value.
* @param string $spacer The string to use for prefixing the values according to
* their depth in the tree.
- * @param int $mode Iterator mode.
- * @phpstan-param \RecursiveIteratorIterator::LEAVES_ONLY|\RecursiveIteratorIterator::SELF_FIRST|\RecursiveIteratorIterator::CHILD_FIRST $mode
+ * @param \RecursiveIteratorIterator::LEAVES_ONLY|\RecursiveIteratorIterator::SELF_FIRST|\RecursiveIteratorIterator::CHILD_FIRST $mode Iterator mode.
*/
public function __construct(
RecursiveIterator $items,
diff --git a/src/Console/Command/HelpCommand.php b/src/Console/Command/HelpCommand.php
index 366d09b69a5..639f29fb0c5 100644
--- a/src/Console/Command/HelpCommand.php
+++ b/src/Console/Command/HelpCommand.php
@@ -434,9 +434,8 @@ protected function outputPaths(ConsoleIo $io): void
}
/**
- * @param array $names Names
+ * @param non-empty-array $names Names
* @return string
- * @phpstan-param non-empty-array $names
*/
protected function getShortestName(array $names): string
{
diff --git a/src/Console/CommandCollection.php b/src/Console/CommandCollection.php
index cc816fba505..0e00b615009 100644
--- a/src/Console/CommandCollection.php
+++ b/src/Console/CommandCollection.php
@@ -162,8 +162,7 @@ public function get(string $name): CommandInterface|string
/**
* Implementation of IteratorAggregate.
*
- * @return \Traversable
- * @phpstan-return \Traversable>
+ * @return \Traversable>
*/
public function getIterator(): Traversable
{
diff --git a/src/Console/HelpFormatter.php b/src/Console/HelpFormatter.php
index 1be9a12cc9f..65dc3b96607 100644
--- a/src/Console/HelpFormatter.php
+++ b/src/Console/HelpFormatter.php
@@ -196,12 +196,16 @@ public function xml(bool $string = true): SimpleXMLElement|string
$xml->addChild('description', $parser->getDescription());
$options = $xml->addChild('options');
- foreach ($parser->options() as $option) {
- $option->xml($options);
+ if ($options !== null) {
+ foreach ($parser->options() as $option) {
+ $option->xml($options);
+ }
}
$arguments = $xml->addChild('arguments');
- foreach ($parser->arguments() as $argument) {
- $argument->xml($arguments);
+ if ($arguments !== null) {
+ foreach ($parser->arguments() as $argument) {
+ $argument->xml($arguments);
+ }
}
$xml->addChild('epilog', $parser->getEpilog());
diff --git a/src/Controller/Component.php b/src/Controller/Component.php
index 9cfdf716a95..595aa96b387 100644
--- a/src/Controller/Component.php
+++ b/src/Controller/Component.php
@@ -46,7 +46,7 @@
* - `afterFilter(EventInterface $event)`
* Called after the action is complete and the view has been rendered but
* before Controller::afterFilter().
- * - `beforeRedirect(EventInterface $event $url, Response $response)`
+ * - `beforeRedirect(EventInterface $event, $url, Response $response)`
* Called before a redirect is done. Allows you to change the URL that will
* be redirected to by returning a Response instance with new URL set using
* Response::location(). Redirection can be prevented by stopping the event
diff --git a/src/Controller/Component/FormProtectionComponent.php b/src/Controller/Component/FormProtectionComponent.php
index 924fbe87acf..100f71a0988 100644
--- a/src/Controller/Component/FormProtectionComponent.php
+++ b/src/Controller/Component/FormProtectionComponent.php
@@ -33,7 +33,7 @@
* - Existing fields have not been removed from the form.
* - Values of hidden inputs have not been changed.
*
- * @phpstan-property array{validate:bool, unlockedFields:array, unlockedActions:array, validationFailureCallback:?\Closure} $_config
+ * @property array{validate:bool, unlockedFields:array, unlockedActions:array, validationFailureCallback:?\Closure} $_config
*/
class FormProtectionComponent extends Component
{
diff --git a/src/Controller/Controller.php b/src/Controller/Controller.php
index 1518729161c..0a8e43aaf3d 100644
--- a/src/Controller/Controller.php
+++ b/src/Controller/Controller.php
@@ -175,8 +175,7 @@ class Controller implements EventListenerInterface, EventDispatcherInterface
/**
* Middlewares list.
*
- * @var array
- * @phpstan-var array
+ * @var array
*/
protected array $middlewares = [];
@@ -522,12 +521,11 @@ public function invokeAction(Closure $action, array $args): void
* Register middleware for the controller.
*
* @param \Psr\Http\Server\MiddlewareInterface|\Closure|string $middleware Middleware.
- * @param array $options Valid options:
+ * @param array{only?: array|string, except?: array|string} $options Valid options:
* - `only`: (array|string) Only run the middleware for specified actions.
* - `except`: (array|string) Run the middleware for all actions except the specified ones.
* @return void
* @since 4.3.0
- * @phpstan-param array{only?: array|string, except?: array|string} $options
*/
public function middleware(MiddlewareInterface|Closure|string $middleware, array $options = []): void
{
diff --git a/src/Core/Exception/CakeException.php b/src/Core/Exception/CakeException.php
index aae98c0e5b6..1028cd666e3 100644
--- a/src/Core/Exception/CakeException.php
+++ b/src/Core/Exception/CakeException.php
@@ -68,6 +68,9 @@ public function __construct(array|string $message = '', ?int $code = null, ?Thro
* Get the passed in attributes
*
* @return array
+ * @psalm-taint-escape html Exception attributes are developer-defined metadata (e.g. controller
+ * name, validation rule names) used only in debug output — log files and CLI console — never
+ * rendered unescaped into HTML responses.
*/
public function getAttributes(): array
{
diff --git a/src/Core/PluginCollection.php b/src/Core/PluginCollection.php
index 51dd526f96e..83d5a298b95 100644
--- a/src/Core/PluginCollection.php
+++ b/src/Core/PluginCollection.php
@@ -248,12 +248,11 @@ public function get(string $name): PluginInterface
/**
* Create a plugin instance from a name/classname and configuration.
*
- * @param string $name The plugin name or classname
+ * @param class-string<\Cake\Core\PluginInterface>|string $name The plugin name or classname
* @param array $config Configuration options for the plugin.
* @return \Cake\Core\PluginInterface
* @throws \Cake\Core\Exception\MissingPluginException When plugin instance could not be created.
* @throws \InvalidArgumentException When class name cannot be found or an empty name is provided.
- * @phpstan-param class-string<\Cake\Core\PluginInterface>|string $name
*/
public function create(string $name, array $config = []): PluginInterface
{
diff --git a/src/Core/StaticConfigTrait.php b/src/Core/StaticConfigTrait.php
index 717a6cbfb8f..c9c4408123d 100644
--- a/src/Core/StaticConfigTrait.php
+++ b/src/Core/StaticConfigTrait.php
@@ -312,9 +312,8 @@ public static function parseDsn(string $dsn): array
/**
* Updates the DSN class map for this class.
*
- * @param array $map Additions/edits to the class map to apply.
+ * @param array $map Additions/edits to the class map to apply.
* @return void
- * @phpstan-param array $map
*/
public static function setDsnClassMap(array $map): void
{
diff --git a/src/Core/functions.php b/src/Core/functions.php
index 9285ac0bfc9..c2e3bfdda3a 100644
--- a/src/Core/functions.php
+++ b/src/Core/functions.php
@@ -145,9 +145,8 @@ function h(mixed $text, bool $double = true, ?string $charset = null): mixed
* @param string $name The name you want to plugin split.
* @param bool $dotAppend Set to true if you want the plugin to have a '.' appended to it.
* @param string|null $plugin Optional default plugin to use if no plugin is found. Defaults to null.
- * @return array Array with 2 indexes. 0 => plugin name, 1 => class name.
+ * @return array{0: string|null, 1: string} Array with 2 indexes. 0 => plugin name, 1 => class name.
* @link https://book.cakephp.org/5/en/core-libraries/global-constants-and-functions.html#pluginsplit
- * @phpstan-return array{string|null, string}
*/
function pluginSplit(string $name, bool $dotAppend = false, ?string $plugin = null): array
{
@@ -157,7 +156,7 @@ function pluginSplit(string $name, bool $dotAppend = false, ?string $plugin = nu
$parts[0] .= '.';
}
- /** @phpstan-var array{string, string} */
+ /** @var array{string, string} */
return $parts;
}
@@ -248,7 +247,7 @@ function pj(mixed $var): mixed
* environment information.
*
* @param string $key Environment variable name.
- * @param string|bool|null $default Specify a default value in case the environment variable is not defined.
+ * @param string|float|int|bool|null $default Specify a default value in case the environment variable is not defined.
* @return string|float|int|bool|null Environment variable setting.
* @link https://book.cakephp.org/5/en/core-libraries/global-constants-and-functions.html#env
*/
diff --git a/src/Core/functions_global.php b/src/Core/functions_global.php
index 6b466565bb5..0644ecc15d8 100644
--- a/src/Core/functions_global.php
+++ b/src/Core/functions_global.php
@@ -82,9 +82,8 @@ function h(mixed $text, bool $double = true, ?string $charset = null): mixed
* @param string $name The name you want to plugin split.
* @param bool $dotAppend Set to true if you want the plugin to have a '.' appended to it.
* @param string|null $plugin Optional default plugin to use if no plugin is found. Defaults to null.
- * @return array Array with 2 indexes. 0 => plugin name, 1 => class name.
+ * @return array{0: string|null, 1: string} Array with 2 indexes. 0 => plugin name, 1 => class name.
* @link https://book.cakephp.org/5/en/core-libraries/global-constants-and-functions.html#pluginsplit
- * @phpstan-return array{string|null, string}
*/
function pluginSplit(string $name, bool $dotAppend = false, ?string $plugin = null): array
{
@@ -155,7 +154,7 @@ function pj(mixed $var): mixed
* environment information.
*
* @param string $key Environment variable name.
- * @param string|bool|null $default Specify a default value in case the environment variable is not defined.
+ * @param string|float|int|bool|null $default Specify a default value in case the environment variable is not defined.
* @return string|float|int|bool|null Environment variable setting.
* @link https://book.cakephp.org/5/en/core-libraries/global-constants-and-functions.html#env
*/
diff --git a/src/Database/Connection.php b/src/Database/Connection.php
index 119dfb9b66f..34e3ba62fa2 100644
--- a/src/Database/Connection.php
+++ b/src/Database/Connection.php
@@ -133,8 +133,7 @@ public function __construct(array $config)
* Creates read and write drivers.
*
* @param array $config Connection config
- * @return array
- * @phpstan-return array{read: \Cake\Database\Driver, write: \Cake\Database\Driver}
+ * @return array{read: \Cake\Database\Driver, write: \Cake\Database\Driver}
*/
protected function createDrivers(array $config): array
{
diff --git a/src/Database/Driver.php b/src/Database/Driver.php
index 552136c21b7..eabd43683a7 100644
--- a/src/Database/Driver.php
+++ b/src/Database/Driver.php
@@ -670,13 +670,11 @@ protected function _updateQueryTranslator(UpdateQuery $query): UpdateQuery
/**
* Removes aliases from the `WHERE` clause of a query.
*
- * @param \Cake\Database\Query\UpdateQuery|\Cake\Database\Query\DeleteQuery $query The query to process.
- * @return \Cake\Database\Query\UpdateQuery|\Cake\Database\Query\DeleteQuery The modified query.
+ * @template T of \Cake\Database\Query\UpdateQuery|\Cake\Database\Query\DeleteQuery
+ * @param T $query The query to process.
+ * @return T The modified query.
* @throws \Cake\Database\Exception\DatabaseException In case the processed query contains any joins, as removing
* aliases from the conditions can break references to the joined tables.
- * @template T of \Cake\Database\Query\UpdateQuery|\Cake\Database\Query\DeleteQuery
- * @phpstan-param T $query
- * @phpstan-return T
*/
protected function _removeAliasesFromConditions(UpdateQuery|DeleteQuery $query): UpdateQuery|DeleteQuery
{
diff --git a/src/Database/Driver/Postgres.php b/src/Database/Driver/Postgres.php
index 78574daea08..a65f164cfa2 100644
--- a/src/Database/Driver/Postgres.php
+++ b/src/Database/Driver/Postgres.php
@@ -328,9 +328,7 @@ protected function _transformFunctionExpression(FunctionExpression $expression):
$expression->setName('JSONB_PATH_QUERY')
->iterateParts(function ($p, $key) {
if ($key === 0) {
- $p = sprintf('%s::jsonb', $p);
- } elseif ($key === 1) {
- $p = sprintf("'%s'::jsonpath", $this->quoteIdentifier($p['value']));
+ return sprintf('%s::jsonb', $p);
}
return $p;
diff --git a/src/Database/Expression/WindowInterface.php b/src/Database/Expression/WindowInterface.php
index f96441e71b4..31016cc0c31 100644
--- a/src/Database/Expression/WindowInterface.php
+++ b/src/Database/Expression/WindowInterface.php
@@ -133,16 +133,13 @@ public function groups(?int $start, ?int $end = 0);
* - `0` - 'CURRENT ROW'
* - `null` - 'UNBOUNDED'
*
- * @param string $type Frame type
+ * @param self::RANGE|self::ROWS|self::GROUPS $type Frame type
* @param \Cake\Database\ExpressionInterface|string|int|null $startOffset Frame start offset
- * @param string $startDirection Frame start direction
+ * @param self::PRECEDING|self::FOLLOWING $startDirection Frame start direction
* @param \Cake\Database\ExpressionInterface|string|int|null $endOffset Frame end offset
- * @param string $endDirection Frame end direction
+ * @param self::PRECEDING|self::FOLLOWING $endDirection Frame end direction
* @return $this
* @throws \InvalidArgumentException WHen offsets are negative.
- * @phpstan-param self::RANGE|self::ROWS|self::GROUPS $type
- * @phpstan-param self::PRECEDING|self::FOLLOWING $startDirection
- * @phpstan-param self::PRECEDING|self::FOLLOWING $endDirection
*/
public function frame(
string $type,
diff --git a/src/Database/FunctionsBuilder.php b/src/Database/FunctionsBuilder.php
index 2afbc46b53b..515e9a52ad8 100644
--- a/src/Database/FunctionsBuilder.php
+++ b/src/Database/FunctionsBuilder.php
@@ -23,7 +23,11 @@
/**
* Contains methods related to generating FunctionExpression objects
* with most commonly used SQL functions.
+ *
* This acts as a factory for FunctionExpression objects.
+ *
+ * Supplying user-controlled data to parameters with the `ExpressionInterface|string`
+ * type is **unsafe**. These parameters are included in the final query without escaping.
*/
class FunctionsBuilder
{
@@ -40,7 +44,7 @@ public function rand(): FunctionExpression
/**
* Returns a AggregateExpression representing a call to SQL SUM function.
*
- * @param \Cake\Database\ExpressionInterface|string $expression the function argument
+ * @param \Cake\Database\ExpressionInterface|string $expression the expression for the sum() function.
* @param array $types list of types to bind to the arguments
* @return \Cake\Database\Expression\AggregateExpression
*/
@@ -57,7 +61,7 @@ public function sum(ExpressionInterface|string $expression, array $types = []):
/**
* Returns a AggregateExpression representing a call to SQL AVG function.
*
- * @param \Cake\Database\ExpressionInterface|string $expression the function argument
+ * @param \Cake\Database\ExpressionInterface|string $expression the expression for the avg() function.
* @param array $types list of types to bind to the arguments
* @return \Cake\Database\Expression\AggregateExpression
*/
@@ -69,7 +73,7 @@ public function avg(ExpressionInterface|string $expression, array $types = []):
/**
* Returns a AggregateExpression representing a call to SQL MAX function.
*
- * @param \Cake\Database\ExpressionInterface|string $expression the function argument
+ * @param \Cake\Database\ExpressionInterface|string $expression the expression for the max() function
* @param array $types list of types to bind to the arguments
* @return \Cake\Database\Expression\AggregateExpression
*/
@@ -81,7 +85,7 @@ public function max(ExpressionInterface|string $expression, array $types = []):
/**
* Returns a AggregateExpression representing a call to SQL MIN function.
*
- * @param \Cake\Database\ExpressionInterface|string $expression the function argument
+ * @param \Cake\Database\ExpressionInterface|string $expression the expression for the min() function.
* @param array $types list of types to bind to the arguments
* @return \Cake\Database\Expression\AggregateExpression
*/
@@ -93,7 +97,7 @@ public function min(ExpressionInterface|string $expression, array $types = []):
/**
* Returns a AggregateExpression representing a call to SQL COUNT function.
*
- * @param \Cake\Database\ExpressionInterface|string $expression the function argument
+ * @param \Cake\Database\ExpressionInterface|string $expression the expression for the count() function.
* @param array $types list of types to bind to the arguments
* @return \Cake\Database\Expression\AggregateExpression
*/
@@ -133,11 +137,12 @@ public function coalesce(array $args, array $types = []): FunctionExpression
* is the default type name. Use `setReturnType()` to update it.
*
* @param \Cake\Database\ExpressionInterface|string $field Field or expression to cast.
- * @param string $dataType The SQL data type
+ * @param string $dataType The SQL data type. Must be a simple alphanumeric string.
* @return \Cake\Database\Expression\FunctionExpression
*/
public function cast(ExpressionInterface|string $field, string $dataType): FunctionExpression
{
+ $this->ensureSimpleString('dataType', $dataType);
$expression = new FunctionExpression('CAST', $this->toLiteralParam($field));
return $expression->setConjunction(' AS')->add([$dataType => 'literal']);
@@ -159,7 +164,7 @@ public function dateDiff(array $args, array $types = []): FunctionExpression
/**
* Returns the specified date part from the SQL expression.
*
- * @param string $part Part of the date to return.
+ * @param string $part Part of the date to return. Must be a simple alphanumeric string.
* @param \Cake\Database\ExpressionInterface|string $expression Expression to obtain the date part from.
* @param array $types list of types to bind to the arguments
* @return \Cake\Database\Expression\FunctionExpression
@@ -175,13 +180,14 @@ public function datePart(
/**
* Returns the specified date part from the SQL expression.
*
- * @param string $part Part of the date to return.
+ * @param string $part Part of the date to return. Must be a simple alphanumeric string.
* @param \Cake\Database\ExpressionInterface|string $expression Expression to obtain the date part from.
* @param array $types list of types to bind to the arguments
* @return \Cake\Database\Expression\FunctionExpression
*/
public function extract(string $part, ExpressionInterface|string $expression, array $types = []): FunctionExpression
{
+ $this->ensureSimpleString('part', $part);
$expression = new FunctionExpression('EXTRACT', $this->toLiteralParam($expression), $types, 'integer');
return $expression->setConjunction(' FROM')->add([$part => 'literal'], [], true);
@@ -192,7 +198,7 @@ public function extract(string $part, ExpressionInterface|string $expression, ar
*
* @param \Cake\Database\ExpressionInterface|string $expression Expression to obtain the date part from.
* @param string|int $value Value to be added. Use negative to subtract.
- * @param string $unit Unit of the value e.g. hour or day.
+ * @param string $unit Unit of the value e.g. hour or day. Must be a simple alphanumeric string.
* @param array $types list of types to bind to the arguments
* @return \Cake\Database\Expression\FunctionExpression
*/
@@ -205,6 +211,7 @@ public function dateAdd(
if (!is_numeric($value)) {
$value = 0;
}
+ $this->ensureSimpleString('unit', $unit);
$interval = $value . ' ' . $unit;
$expression = new FunctionExpression('DATE_ADD', $this->toLiteralParam($expression), $types, 'datetime');
@@ -345,7 +352,7 @@ public function jsonValue(
* @param string $name The SQL aggregate function name
* @param array $params Array of arguments to be passed to the function.
* Can be an associative array with the literal value or identifier:
- * `['value' => 'literal']` or `['value' => 'identifier']
+ * `['value' => 'literal']` or `['value' => 'identifier']`
* @param array $types Array of types that match the names used in `$params`:
* `['name' => 'type']`
* @param string $return Return type of the entire expression. Defaults to float.
@@ -388,4 +395,19 @@ protected function toLiteralParam(ExpressionInterface|string $expression): array
return [$expression];
}
+
+ /**
+ * Ensures that string values are simple ascii values with no whitespace
+ *
+ * @param string $parameterName The name of the parameter being checked.
+ * @param string $value The value to check
+ * @return void
+ */
+ protected function ensureSimpleString(string $parameterName, string $value): void
+ {
+ if (preg_match('/^[a-zA-Z0-9]+$/', $value)) {
+ return;
+ }
+ throw new InvalidArgumentException("Argument `{$parameterName}` must be an alphanumeric string");
+ }
}
diff --git a/src/Database/Schema/Column.php b/src/Database/Schema/Column.php
index 3706ee67548..aa6d2ba0e52 100644
--- a/src/Database/Schema/Column.php
+++ b/src/Database/Schema/Column.php
@@ -409,7 +409,7 @@ public function setComment(?string $comment)
/**
* Gets the column comment.
*
- * @return string
+ * @return string|null
*/
public function getComment(): ?string
{
@@ -597,7 +597,7 @@ public function setAttributes(array $attributes)
/**
* Convert an index into an array that is compatible with the Column constructor.
*
- * @return array
+ * @return array{name: ?string, baseType: ?string, type: string, length: ?int, null: ?bool, default: mixed, generated: ?string, unsigned: ?bool, onUpdate: ?string, collate: ?string, precision: ?int, srid: ?int, comment: ?string, autoIncrement: bool, identity: bool, fixed: ?bool, geometryType?: ?string}
*/
public function toArray(): array
{
diff --git a/src/Database/Schema/PostgresSchemaDialect.php b/src/Database/Schema/PostgresSchemaDialect.php
index 0357240baa4..89831afc3f5 100644
--- a/src/Database/Schema/PostgresSchemaDialect.php
+++ b/src/Database/Schema/PostgresSchemaDialect.php
@@ -918,10 +918,11 @@ public function indexSql(TableSchema $schema, string $name): string
(array)$index->getColumns(),
);
$include = '';
- if ($index->getInclude()) {
+ $includes = $index->getInclude();
+ if ($includes) {
$included = array_map(
$this->_driver->quoteIdentifier(...),
- $index->getInclude(),
+ $includes,
);
$include = sprintf(' INCLUDE (%s)', implode(', ', $included));
}
diff --git a/src/Database/Schema/TableSchema.php b/src/Database/Schema/TableSchema.php
index 49c7b4479c7..7a4b633ae82 100644
--- a/src/Database/Schema/TableSchema.php
+++ b/src/Database/Schema/TableSchema.php
@@ -435,18 +435,14 @@ public function getColumn(string $name): ?array
if (isset(static::$_columnExtras[$attrs['type']])) {
$expected += static::$_columnExtras[$attrs['type']];
}
- // Remove any attributes that weren't in the allow list.
- // This is to provide backwards compatible keys
- $remove = array_diff(array_keys($attrs), array_keys($expected));
- foreach ($remove as $key) {
- unset($attrs[$key]);
- }
if (isset($attrs['baseType']) && $attrs['baseType'] === $attrs['type']) {
unset($attrs['baseType']);
}
- return $attrs;
+ // Remove any attributes that weren't in the allow list.
+ // This is to provide backwards compatible keys
+ return array_intersect_key($attrs, $expected);
}
/**
diff --git a/src/Database/Statement/Statement.php b/src/Database/Statement/Statement.php
index 9c751784f65..73024e98464 100644
--- a/src/Database/Statement/Statement.php
+++ b/src/Database/Statement/Statement.php
@@ -101,8 +101,7 @@ public function bindValue(string|int $column, mixed $value, string|int|null $typ
*
* @param mixed $value The value to cast.
* @param \Cake\Database\TypeInterface|string|int $type The type name or type instance to use.
- * @return array List containing converted value and internal type.
- * @phpstan-return array{0:mixed, 1:int}
+ * @return array{0:mixed, 1:int} List containing converted value and internal type.
*/
protected function cast(mixed $value, TypeInterface|string|int $type = 'string'): array
{
diff --git a/src/Database/TypeFactory.php b/src/Database/TypeFactory.php
index 7c97a789f59..6d1efb44499 100644
--- a/src/Database/TypeFactory.php
+++ b/src/Database/TypeFactory.php
@@ -26,8 +26,7 @@ class TypeFactory
* identifier is used as key and a complete namespaced class name as value
* representing the class that will do actual type conversions.
*
- * @var array
- * @phpstan-var array>
+ * @var array>
*/
protected static array $_types = [
'biginteger' => Type\IntegerType::class,
@@ -118,9 +117,8 @@ public static function set(string $name, TypeInterface $instance): void
* Registers a new type identifier and maps it to a fully namespaced classname.
*
* @param string $type Name of type to map.
- * @param string $className The classname to register.
+ * @param class-string<\Cake\Database\TypeInterface> $className The classname to register.
* @return void
- * @phpstan-param class-string<\Cake\Database\TypeInterface> $className
*/
public static function map(string $type, string $className): void
{
@@ -131,9 +129,8 @@ public static function map(string $type, string $className): void
/**
* Set type to classname mapping.
*
- * @param array $map List of types to be mapped.
+ * @param array> $map List of types to be mapped.
* @return void
- * @phpstan-param array> $map
*/
public static function setMap(array $map): void
{
@@ -168,8 +165,7 @@ public static function getMap(?string $type = null): array|string|null
* Get mapped class name for a specific type.
*
* @param string $type Type name to get mapped class for.
- * @return string|null Configured class name for given $type or null if not found.
- * @phpstan-return class-string<\Cake\Database\TypeInterface>|null
+ * @return class-string<\Cake\Database\TypeInterface>|null Configured class name for given $type or null if not found.
*/
public static function getMapped(string $type): ?string
{
diff --git a/src/Database/phpstan.neon.dist b/src/Database/phpstan.neon.dist
index ca17bf0b54d..573d19e7da3 100644
--- a/src/Database/phpstan.neon.dist
+++ b/src/Database/phpstan.neon.dist
@@ -9,13 +9,10 @@ parameters:
- vendor/
ignoreErrors:
- '#Unsafe usage of new static\(\).#'
- -
- identifier: notIdentical.alwaysTrue
-
identifier: missingType.iterableValue
-
-
message: '#^Dead catch \- InvalidArgumentException is never thrown in the try block\.$#'
identifier: catch.neverThrown
- count: 1
path: Type/DateTimeType.php
+ reportUnmatched: false
diff --git a/src/Datasource/ConnectionManager.php b/src/Datasource/ConnectionManager.php
index 20e990a270e..114fbb89f1e 100644
--- a/src/Datasource/ConnectionManager.php
+++ b/src/Datasource/ConnectionManager.php
@@ -51,8 +51,7 @@ class ConnectionManager
/**
* An array mapping url schemes to fully qualified driver class names
*
- * @var array
- * @phpstan-var array
+ * @var array
*/
protected static array $_dsnClassMap = [
'mysql' => Mysql::class,
diff --git a/src/Error/Debugger.php b/src/Error/Debugger.php
index b834e1bd0e7..ba73e1df3de 100644
--- a/src/Error/Debugger.php
+++ b/src/Error/Debugger.php
@@ -62,7 +62,15 @@ class Debugger
* @var array
*/
protected array $_defaultConfig = [
- 'outputMask' => [],
+ 'outputMask' => [
+ 'password' => '********',
+ 'login' => '********',
+ 'host' => '********',
+ 'database' => '********',
+ 'port' => '********',
+ 'prefix' => '********',
+ 'schema' => '********',
+ ],
'exportFormatter' => null,
'editor' => 'phpstorm',
'editorBasePath' => null,
@@ -661,7 +669,7 @@ protected static function export(mixed $var, DebugContext $context): NodeInterfa
/**
* Export an array type object. Filters out keys used in datasource configuration.
*
- * The following keys are replaced with ***'s
+ * By default the following keys are replaced with ***'s
*
* - password
* - login
diff --git a/src/Event/Event.php b/src/Event/Event.php
index cc1163d458f..b7ed901e419 100644
--- a/src/Event/Event.php
+++ b/src/Event/Event.php
@@ -78,7 +78,6 @@ class Event implements EventInterface
* (usually the object that is generating the event).
* @param array $data any value you wish to be transported
* with this event to it can be read by listeners.
- * @phpstan-param TSubject|null $subject
*/
public function __construct(string $name, ?object $subject = null, array $data = [])
{
diff --git a/src/Event/EventDispatcherTrait.php b/src/Event/EventDispatcherTrait.php
index 0856b75eddb..c5fbda4c005 100644
--- a/src/Event/EventDispatcherTrait.php
+++ b/src/Event/EventDispatcherTrait.php
@@ -78,7 +78,6 @@ public function setEventManager(EventManagerInterface $eventManager)
* @param TSubject|null $subject The object that this event applies to
* ($this by default).
* @return \Cake\Event\EventInterface
- * @phpstan-ignore missingType.generics
*/
public function dispatchEvent(string $name, array $data = [], ?object $subject = null): EventInterface // @phpstan-ignore missingType.generics
{
@@ -86,7 +85,6 @@ public function dispatchEvent(string $name, array $data = [], ?object $subject =
/**
* @var \Cake\Event\EventInterface $event Coerce for psalm/phpstan
- * @phpstan-ignore missingType.generics (TSubject may itself be generic)
*/
$event = new $this->_eventClass($name, $subject, $data);
$this->getEventManager()->dispatch($event);
diff --git a/src/Form/Form.php b/src/Form/Form.php
index e5270cc495e..519187f5e05 100644
--- a/src/Form/Form.php
+++ b/src/Form/Form.php
@@ -74,8 +74,7 @@ class Form implements EventListenerInterface, EventDispatcherInterface, Validato
/**
* Schema class.
*
- * @var string
- * @phpstan-var class-string<\Cake\Form\Schema>
+ * @var class-string<\Cake\Form\Schema>
*/
protected string $_schemaClass = Schema::class;
diff --git a/src/Form/FormProtector.php b/src/Form/FormProtector.php
index b303a3629b9..61b8a122db0 100644
--- a/src/Form/FormProtector.php
+++ b/src/Form/FormProtector.php
@@ -270,8 +270,7 @@ protected function extractToken(mixed $formData): ?string
* Return hash parts for the token generation
*
* @param array $formData Form data.
- * @return array Contains 'fields' and 'unlockedFields' keys. Additional keys allowed.
- * @phpstan-return array{fields: array, unlockedFields: array, ...}
+ * @return array{fields: array, unlockedFields: array, ...} Contains 'fields' and 'unlockedFields' keys. Additional keys allowed.
*/
protected function extractHashParts(array $formData): array
{
@@ -380,8 +379,7 @@ protected function sortedUnlockedFields(array $formData): array
*
* @param string $url Form URL.
* @param string $sessionId Session ID.
- * @return array The token data. Contains 'fields', 'unlocked', and 'debug' keys. Additional keys allowed.
- * @phpstan-return array{fields: string, unlocked: string, debug: string, ...}
+ * @return array{fields: string, unlocked: string, debug: string, ...} The token data. Contains 'fields', 'unlocked', and 'debug' keys. Additional keys allowed.
*/
public function buildTokenData(string $url = '', string $sessionId = ''): array
{
diff --git a/src/Http/Client/Adapter/Curl.php b/src/Http/Client/Adapter/Curl.php
index 40aca2afe0a..118b2539292 100644
--- a/src/Http/Client/Adapter/Curl.php
+++ b/src/Http/Client/Adapter/Curl.php
@@ -128,7 +128,7 @@ public function buildOptions(RequestInterface $request, array $options): array
}
if (empty($options['ssl_cafile'])) {
- $options['ssl_cafile'] = CaBundle::getBundledCaBundlePath();
+ $options['ssl_cafile'] = ini_get('curl.cainfo') ?: CaBundle::getBundledCaBundlePath();
}
if (!empty($options['ssl_verify_host'])) {
// Value of 1 or true is deprecated. Only 2 or 0 should be used now.
diff --git a/src/Http/Client/Request.php b/src/Http/Client/Request.php
index 16f2b70474e..52abaa3dbb3 100644
--- a/src/Http/Client/Request.php
+++ b/src/Http/Client/Request.php
@@ -36,10 +36,9 @@ class Request extends Message implements RequestInterface
*
* Provides backwards compatible defaults for some properties.
*
- * @phpstan-param array $headers
* @param \Psr\Http\Message\UriInterface|string $url The request URL
* @param string $method The HTTP method to use.
- * @param array $headers The HTTP headers to set.
+ * @param array $headers The HTTP headers to set.
* @param array|string|null $data The request body to use.
*/
public function __construct(
@@ -65,8 +64,7 @@ public function __construct(
/**
* Add an array of headers to the request.
*
- * @phpstan-param array $headers
- * @param array $headers The headers to add.
+ * @param array $headers The headers to add.
* @return void
*/
protected function addHeaders(array $headers): void
@@ -101,7 +99,7 @@ protected function setContent(array|string $content)
$formData = new FormData();
$formData->addMany($content);
- /** @phpstan-var array $headers */
+ /** @var array $headers */
$headers = ['Content-Type' => $formData->contentType()];
$this->addHeaders($headers);
$content = (string)$formData;
diff --git a/src/Http/ControllerFactoryInterface.php b/src/Http/ControllerFactoryInterface.php
index d7e34fb0bd7..8406b440891 100644
--- a/src/Http/ControllerFactoryInterface.php
+++ b/src/Http/ControllerFactoryInterface.php
@@ -30,18 +30,16 @@ interface ControllerFactoryInterface
* Create a controller for a given request
*
* @param \Psr\Http\Message\ServerRequestInterface $request The request to build a controller for.
- * @return mixed
+ * @return TController
* @throws \Cake\Http\Exception\MissingControllerException
- * @phpstan-return TController
*/
public function create(ServerRequestInterface $request): mixed;
/**
* Invoke a controller's action and wrapping methods.
*
- * @param mixed $controller The controller to invoke.
+ * @param TController $controller The controller to invoke.
* @return \Psr\Http\Message\ResponseInterface The response
- * @phpstan-param TController $controller
*/
public function invoke(mixed $controller): ResponseInterface;
}
diff --git a/src/Http/Response.php b/src/Http/Response.php
index 29f84f62443..7fd55100ce0 100644
--- a/src/Http/Response.php
+++ b/src/Http/Response.php
@@ -304,8 +304,7 @@ public function withLocation(string $url): static
/**
* Sets a header.
*
- * @phpstan-param non-empty-string $header
- * @param string $header Header key.
+ * @param non-empty-string $header Header key.
* @param string $value Header value.
* @return void
*/
@@ -319,8 +318,7 @@ protected function _setHeader(string $header, string $value): void
/**
* Clear header
*
- * @phpstan-param non-empty-string $header
- * @param string $header Header key.
+ * @param non-empty-string $header Header key.
* @return void
*/
protected function _clearHeader(string $header): void
diff --git a/src/Http/ServerRequest.php b/src/Http/ServerRequest.php
index 1e29dd3b6d5..8079954f00e 100644
--- a/src/Http/ServerRequest.php
+++ b/src/Http/ServerRequest.php
@@ -430,8 +430,10 @@ public function getTrustedProxies(): array
/**
* Returns the referer that referred this request.
*
- * @param bool $local Attempt to return a local address.
- * Local addresses do not contain hostnames.
+ * @param bool $local When true, return the referer as a host-stripped local path,
+ * or null if the referer is not on the same origin as this application.
+ * When false, return the raw referer URL as-is. In both cases null is returned
+ * when no referer is available.
* @return string|null The referring address for this request or null.
*/
public function referer(bool $local = true): ?string
diff --git a/src/Http/UriFactory.php b/src/Http/UriFactory.php
index 0ba277139c9..05037457549 100644
--- a/src/Http/UriFactory.php
+++ b/src/Http/UriFactory.php
@@ -44,8 +44,7 @@ public function createUri(string $uri = ''): UriInterface
*
* @param array|null $server Array of server data to build the Uri from.
* $_SERVER will be used if $server parameter is null.
- * @return array
- * @phpstan-return array{uri: \Psr\Http\Message\UriInterface, base: string, webroot: string}
+ * @return array{uri: \Psr\Http\Message\UriInterface, base: string, webroot: string}
*/
public static function marshalUriAndBaseFromSapi(?array $server = null): array
{
@@ -117,8 +116,7 @@ protected static function updatePath(string $base, UriInterface $uri): UriInterf
*
* @param \Psr\Http\Message\UriInterface $uri The Uri instance.
* @param array $server The SERVER data to use.
- * @return array An array containing the base and webroot paths.
- * @phpstan-return array{base: string, webroot: string}
+ * @return array{base: string, webroot: string} An array containing the base and webroot paths.
*/
protected static function getBase(UriInterface $uri, array $server): array
{
diff --git a/src/I18n/Date.php b/src/I18n/Date.php
index a1a699059f4..671408e3d56 100644
--- a/src/I18n/Date.php
+++ b/src/I18n/Date.php
@@ -29,7 +29,7 @@
*
* Adds handy methods and locale-aware formatting helpers.
*
- * @phpstan-immutable
+ * @immutable
*/
class Date extends ChronosDate implements JsonSerializable, Stringable
{
diff --git a/src/I18n/DateTime.php b/src/I18n/DateTime.php
index 462bddffba4..aee19dfbd51 100644
--- a/src/I18n/DateTime.php
+++ b/src/I18n/DateTime.php
@@ -29,7 +29,7 @@
* Extends the built-in DateTime class to provide handy methods and locale-aware
* formatting helpers.
*
- * @phpstan-immutable
+ * @immutable
*/
class DateTime extends Chronos implements JsonSerializable, Stringable
{
diff --git a/src/I18n/RelativeTimeFormatter.php b/src/I18n/RelativeTimeFormatter.php
index b202daad0c4..56fd9b96a2b 100644
--- a/src/I18n/RelativeTimeFormatter.php
+++ b/src/I18n/RelativeTimeFormatter.php
@@ -413,9 +413,8 @@ public function dateAgoInWords(DateTime|Date $date, array $options = []): string
* Build the options for relative date formatting.
*
* @param array $options The options provided by the user.
- * @param string $class The class name to use for defaults.
+ * @param class-string<\Cake\I18n\Date>|class-string<\Cake\I18n\DateTime> $class The class name to use for defaults.
* @return array Options with defaults applied.
- * @phpstan-param class-string<\Cake\I18n\Date>|class-string<\Cake\I18n\DateTime> $class
*/
protected function _options(array $options, string $class): array
{
diff --git a/src/I18n/Time.php b/src/I18n/Time.php
index 79def737ae2..8913e2c64b3 100644
--- a/src/I18n/Time.php
+++ b/src/I18n/Time.php
@@ -28,7 +28,7 @@
*
* Adds handy methods and locale-aware formatting helpers.
*
- * @phpstan-immutable
+ * @immutable
*/
class Time extends ChronosTime implements JsonSerializable, Stringable
{
diff --git a/src/Log/Log.php b/src/Log/Log.php
index 0440836c1ac..ffecaf2b54f 100644
--- a/src/Log/Log.php
+++ b/src/Log/Log.php
@@ -115,8 +115,7 @@ class Log
/**
* An array mapping url schemes to fully qualified Log engine class names
*
- * @var array
- * @phpstan-var array
+ * @var array
*/
protected static array $_dsnClassMap = [
'console' => Engine\ConsoleLog::class,
diff --git a/src/Mailer/AbstractTransport.php b/src/Mailer/AbstractTransport.php
index 4964d1c10c6..e72218490cd 100644
--- a/src/Mailer/AbstractTransport.php
+++ b/src/Mailer/AbstractTransport.php
@@ -37,8 +37,7 @@ abstract class AbstractTransport
* Send mail
*
* @param \Cake\Mailer\Message $message Email message.
- * @return array Contains 'headers' and 'message' keys. Additional keys allowed.
- * @phpstan-return array{headers: string, message: string, ...}
+ * @return array{headers: string, message: string, ...} Contains 'headers' and 'message' keys. Additional keys allowed.
*/
abstract public function send(Message $message): array;
diff --git a/src/Mailer/Mailer.php b/src/Mailer/Mailer.php
index 3f4b32c6250..efc69281927 100644
--- a/src/Mailer/Mailer.php
+++ b/src/Mailer/Mailer.php
@@ -153,8 +153,7 @@ class Mailer implements EventListenerInterface
/**
* Message class name.
*
- * @var string
- * @phpstan-var class-string<\Cake\Mailer\Message>
+ * @var class-string<\Cake\Mailer\Message>
*/
protected string $messageClass = Message::class;
@@ -187,8 +186,7 @@ class Mailer implements EventListenerInterface
/**
* Mailer driver class map.
*
- * @var array
- * @phpstan-var array
+ * @var array
*/
protected static array $_dsnClassMap = [];
@@ -315,8 +313,7 @@ public function setViewVars(array|string $key, mixed $value = null)
* If no action is specified then all other method arguments will be ignored.
* @param array $args Arguments to pass to the triggered mailer action.
* @param array $headers Headers to set.
- * @return array Contains 'headers' and 'message' keys. Additional keys allowed.
- * @phpstan-return array{headers: string, message: string, ...}
+ * @return array{headers: string, message: string, ...} Contains 'headers' and 'message' keys. Additional keys allowed.
* @throws \Cake\Mailer\Exception\MissingActionException
* @throws \BadMethodCallException
*/
@@ -373,8 +370,7 @@ public function render(string $content = '')
* Render content and send email using configured transport.
*
* @param string $content Content.
- * @return array Contains 'headers' and 'message' keys. Additional keys allowed.
- * @phpstan-return array{headers: string, message: string, ...}
+ * @return array{headers: string, message: string, ...} Contains 'headers' and 'message' keys. Additional keys allowed.
*/
public function deliver(string $content = ''): array
{
@@ -552,8 +548,7 @@ public function reset()
/**
* Log the email message delivery.
*
- * @param array $contents The content with 'headers' and 'message' keys.
- * @phpstan-param array{headers: string, message: string, ...} $contents
+ * @param array{headers: string, message: string, ...} $contents The content with 'headers' and 'message' keys.
* @return void
*/
protected function logDelivery(array $contents): void
diff --git a/src/Mailer/Renderer.php b/src/Mailer/Renderer.php
index d1d9f411fdf..1459a2afebd 100644
--- a/src/Mailer/Renderer.php
+++ b/src/Mailer/Renderer.php
@@ -49,10 +49,8 @@ public function __construct()
* of the specified content types for the email.
*
* @param string $content The content.
- * @param array $types Content types to render. Valid array values are {@link Message::MESSAGE_HTML}, {@link Message::MESSAGE_TEXT}.
- * @return array The rendered content with "html" and/or "text" keys.
- * @phpstan-param array<\Cake\Mailer\Message::MESSAGE_HTML|\Cake\Mailer\Message::MESSAGE_TEXT> $types
- * @phpstan-return array{html?: string, text?: string}
+ * @param array<\Cake\Mailer\Message::MESSAGE_HTML|\Cake\Mailer\Message::MESSAGE_TEXT> $types Content types to render. Valid array values are {@link Message::MESSAGE_HTML}, {@link Message::MESSAGE_TEXT}.
+ * @return array{html?: string, text?: string} The rendered content with "html" and/or "text" keys.
*/
public function render(string $content, array $types = []): array
{
diff --git a/src/Mailer/Transport/SmtpTransport.php b/src/Mailer/Transport/SmtpTransport.php
index 90f77292c91..175ad78f4cd 100644
--- a/src/Mailer/Transport/SmtpTransport.php
+++ b/src/Mailer/Transport/SmtpTransport.php
@@ -202,8 +202,7 @@ public function getLastResponse(): array
* Send mail
*
* @param \Cake\Mailer\Message $message Message instance
- * @return array Contains 'headers' and 'message' keys. Additional keys allowed.
- * @phpstan-return array{headers: string, message: string, ...}
+ * @return array{headers: string, message: string, ...} Contains 'headers' and 'message' keys. Additional keys allowed.
* @throws \Cake\Network\Exception\SocketException
*/
public function send(Message $message): array
@@ -558,16 +557,22 @@ protected function _sendData(Message $message): void
{
$this->_smtpSend('DATA', '354');
- $headers = $message->getHeadersString([
- 'from',
- 'sender',
- 'replyTo',
- 'readReceipt',
- 'to',
- 'cc',
- 'subject',
- 'returnPath',
- ]);
+ $headers = $message->getHeadersString(
+ [
+ 'from',
+ 'sender',
+ 'replyTo',
+ 'readReceipt',
+ 'to',
+ 'cc',
+ 'subject',
+ 'returnPath',
+ ],
+ "\r\n",
+ function (string $val): string {
+ return str_replace("\r\n", '', $val);
+ },
+ );
$message = $this->_prepareMessage($message);
$this->_smtpSend($headers . "\r\n\r\n" . $message . "\r\n\r\n\r\n.");
diff --git a/src/Mailer/TransportFactory.php b/src/Mailer/TransportFactory.php
index 4794e899ed8..a35e149f8d7 100644
--- a/src/Mailer/TransportFactory.php
+++ b/src/Mailer/TransportFactory.php
@@ -36,8 +36,7 @@ class TransportFactory
/**
* An array mapping url schemes to fully qualified Transport class names
*
- * @var array
- * @phpstan-var array
+ * @var array
*/
protected static array $_dsnClassMap = [
'debug' => Transport\DebugTransport::class,
diff --git a/src/ORM/Association/BelongsTo.php b/src/ORM/Association/BelongsTo.php
index 908b2914d9e..a504900c476 100644
--- a/src/ORM/Association/BelongsTo.php
+++ b/src/ORM/Association/BelongsTo.php
@@ -148,7 +148,6 @@ public function saveAssociated(EntityInterface $entity, array $options = []): En
$targetEntity->extract((array)$this->getBindingKey()),
);
- // @phpstan-ignore function.alreadyNarrowedType (patch method available on EntityInterface)
if (method_exists($entity, 'patch')) {
$entity = $entity->patch($properties, ['guard' => false]);
} else {
diff --git a/src/ORM/Association/HasMany.php b/src/ORM/Association/HasMany.php
index 3b352b1dbf3..5325d670fb7 100644
--- a/src/ORM/Association/HasMany.php
+++ b/src/ORM/Association/HasMany.php
@@ -228,7 +228,6 @@ protected function _saveTarget(
}
if ($foreignKeyReference !== $entity->extract($foreignKey)) {
- // @phpstan-ignore function.alreadyNarrowedType (patch method available on EntityInterface)
if (method_exists($entity, 'patch')) {
$entity->patch($foreignKeyReference, ['guard' => false]);
} else {
diff --git a/src/ORM/Association/HasOne.php b/src/ORM/Association/HasOne.php
index 71d7d4da7b0..51012c7733f 100644
--- a/src/ORM/Association/HasOne.php
+++ b/src/ORM/Association/HasOne.php
@@ -127,7 +127,6 @@ public function saveAssociated(EntityInterface $entity, array $options = []): En
$foreignKeys,
$entity->extract((array)$this->getBindingKey()),
);
- // @phpstan-ignore function.alreadyNarrowedType (patch method available on EntityInterface)
if (method_exists($targetEntity, 'patch')) {
$targetEntity = $targetEntity->patch($properties, ['guard' => false]);
} else {
diff --git a/src/ORM/Association/Loader/SelectLoader.php b/src/ORM/Association/Loader/SelectLoader.php
index 866e6ac7fbd..1ca8d0b5b2b 100644
--- a/src/ORM/Association/Loader/SelectLoader.php
+++ b/src/ORM/Association/Loader/SelectLoader.php
@@ -463,10 +463,15 @@ protected function _subqueryFields(SelectQuery $query): array
$order = $query->clause('order');
if ($order) {
$columns = $query->clause('select');
- $order->iterateParts(function ($direction, $field) use (&$fields, $columns): void {
+ // iterateParts() rebuilds the expression from the callback's return value, so each part
+ // has to be handed back. Returning nothing would strip the ORDER BY from $query itself,
+ // which is the caller's query, not a clone of it.
+ $order->iterateParts(function ($direction, $field) use (&$fields, $columns) {
if (isset($columns[$field])) {
$fields[$field] = $columns[$field];
}
+
+ return $direction;
});
}
diff --git a/src/ORM/AssociationCollection.php b/src/ORM/AssociationCollection.php
index a286d783f6a..05667b364d6 100644
--- a/src/ORM/AssociationCollection.php
+++ b/src/ORM/AssociationCollection.php
@@ -68,13 +68,11 @@ public function __construct(?LocatorInterface $tableLocator = null)
* If the alias added contains a `.` the part preceding the `.` will be dropped.
* This makes using plugins simpler as the Plugin.Class syntax is frequently used.
*
+ * @template T of \Cake\ORM\Association
* @param string $alias The association alias
- * @param \Cake\ORM\Association $association The association to add.
- * @return \Cake\ORM\Association The association object being added.
+ * @param T $association The association to add.
+ * @return T The association object being added.
* @throws \Cake\Core\Exception\CakeException If the alias is already added.
- * @template T of \Cake\ORM\Association
- * @phpstan-param T $association
- * @phpstan-return T
*/
public function add(string $alias, Association $association): Association
{
@@ -90,14 +88,12 @@ public function add(string $alias, Association $association): Association
/**
* Creates and adds the Association object to this collection.
*
- * @param string $className The name of association class.
+ * @template T of \Cake\ORM\Association
+ * @param class-string $className The name of association class.
* @param string $associated The alias for the target table.
* @param array $options List of options to configure the association definition.
- * @return \Cake\ORM\Association
+ * @return T
* @throws \InvalidArgumentException
- * @template T of \Cake\ORM\Association
- * @phpstan-param class-string $className
- * @phpstan-return T
*/
public function load(string $className, string $associated, array $options = []): Association
{
diff --git a/src/ORM/Behavior/TranslateBehavior.php b/src/ORM/Behavior/TranslateBehavior.php
index c1950963ca5..4d9c2f3d8b0 100644
--- a/src/ORM/Behavior/TranslateBehavior.php
+++ b/src/ORM/Behavior/TranslateBehavior.php
@@ -73,8 +73,7 @@ class TranslateBehavior extends Behavior implements PropertyMarshalInterface
/**
* Default strategy class name.
*
- * @var string
- * @phpstan-var class-string<\Cake\ORM\Behavior\Translate\TranslateStrategyInterface>
+ * @var class-string<\Cake\ORM\Behavior\Translate\TranslateStrategyInterface>
*/
protected static string $defaultStrategyClass = ShadowTableStrategy::class;
@@ -136,10 +135,9 @@ public function initialize(array $config): void
/**
* Set default strategy class name.
*
- * @param string $class Class name.
+ * @param class-string<\Cake\ORM\Behavior\Translate\TranslateStrategyInterface> $class Class name.
* @return void
* @since 4.0.0
- * @phpstan-param class-string<\Cake\ORM\Behavior\Translate\TranslateStrategyInterface> $class
*/
public static function setDefaultStrategyClass(string $class): void
{
@@ -149,9 +147,8 @@ public static function setDefaultStrategyClass(string $class): void
/**
* Get default strategy class name.
*
- * @return string
+ * @return class-string<\Cake\ORM\Behavior\Translate\TranslateStrategyInterface>
* @since 4.0.0
- * @phpstan-return class-string<\Cake\ORM\Behavior\Translate\TranslateStrategyInterface>
*/
public static function getDefaultStrategyClass(): string
{
diff --git a/src/ORM/Behavior/TreeBehavior.php b/src/ORM/Behavior/TreeBehavior.php
index 2a9ffde9bed..f86cfee3366 100644
--- a/src/ORM/Behavior/TreeBehavior.php
+++ b/src/ORM/Behavior/TreeBehavior.php
@@ -944,7 +944,6 @@ protected function _ensureFields(EntityInterface $entity): void
}
$fresh = $this->_table->get($entity->get($this->_getPrimaryKey()));
- // @phpstan-ignore function.alreadyNarrowedType (patch method available on EntityInterface)
if (method_exists($entity, 'patch')) {
$entity->patch($fresh->extract($fields), ['guard' => false]);
} else {
diff --git a/src/ORM/BehaviorRegistry.php b/src/ORM/BehaviorRegistry.php
index b32c93fc2ba..4e6e3c95e3b 100644
--- a/src/ORM/BehaviorRegistry.php
+++ b/src/ORM/BehaviorRegistry.php
@@ -91,8 +91,7 @@ public function setTable(Table $table): void
* Resolve a behavior classname.
*
* @param string $class Partial classname to resolve.
- * @return string|null Either the correct classname or null.
- * @phpstan-return class-string|null
+ * @return class-string|null Either the correct classname or null.
*/
public static function className(string $class): ?string
{
diff --git a/src/ORM/Marshaller.php b/src/ORM/Marshaller.php
index b21f9149aac..7573fc2cfac 100644
--- a/src/ORM/Marshaller.php
+++ b/src/ORM/Marshaller.php
@@ -250,7 +250,6 @@ public function one(array $data, array $options = []): EntityInterface
$entity->set($field, $properties[$field], ['asOriginal' => true]);
}
}
- // @phpstan-ignore function.alreadyNarrowedType (patch method available on EntityInterface)
} elseif (method_exists($entity, 'patch')) {
$entity->patch($properties, ['asOriginal' => true]);
} else {
@@ -624,7 +623,6 @@ public function merge(EntityInterface $entity, array $data, array $options = [])
$entity->setErrors($errors);
if (!isset($options['fields'])) {
- // @phpstan-ignore function.alreadyNarrowedType (patch method available on EntityInterface)
if (method_exists($entity, 'patch')) {
$entity->patch($properties);
} else {
diff --git a/src/ORM/Table.php b/src/ORM/Table.php
index 688f7d4a84a..1ed4860d007 100644
--- a/src/ORM/Table.php
+++ b/src/ORM/Table.php
@@ -263,8 +263,7 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
/**
* The name of the class that represent a single row for this table
*
- * @var string|null
- * @phpstan-var class-string|null
+ * @var class-string|null
*/
protected ?string $_entityClass = null;
@@ -843,11 +842,9 @@ public function behaviors(): BehaviorRegistry
/**
* Get a behavior from the registry.
*
- * @param string $name The behavior alias to get from the registry.
- * @return \Cake\ORM\Behavior
+ * @param TName $name The behavior alias to get from the registry.
+ * @return (TName is key-of ? TBehaviors[TName] : \Cake\ORM\Behavior)
* @template TName of string
- * @phpstan-param TName $name The behavior alias to get from the registry.
- * @phpstan-return (TName is key-of ? TBehaviors[TName] : \Cake\ORM\Behavior)
* @throws \InvalidArgumentException If the behavior does not exist.
*/
public function getBehavior(string $name): Behavior
@@ -2195,7 +2192,6 @@ protected function _insert(EntityInterface $entity, array $data): EntityInterfac
if ($statement->rowCount() !== 0) {
$success = $entity;
- // @phpstan-ignore function.alreadyNarrowedType (patch method available on EntityInterface)
if (method_exists($entity, 'patch')) {
$entity = $entity->patch($filteredKeys, ['guard' => false]);
} else {
diff --git a/src/Routing/Asset.php b/src/Routing/Asset.php
index a76dcbe5dee..16708c63adf 100644
--- a/src/Routing/Asset.php
+++ b/src/Routing/Asset.php
@@ -355,8 +355,7 @@ protected static function requestWebroot(): string
* It checks if the plugin is loaded, else filename will stay unchanged for filenames containing dot.
*
* @param string $name The name you want to plugin split.
- * @return array Array with 2 indexes. 0 => plugin name, 1 => filename.
- * @phpstan-return array{string|null, string}
+ * @return array{0: string|null, 1: string} Array with 2 indexes. 0 => plugin name, 1 => filename.
*/
protected static function pluginSplit(string $name): array
{
diff --git a/src/Routing/Route/Route.php b/src/Routing/Route/Route.php
index e513a788e5a..25ace3da42c 100644
--- a/src/Routing/Route/Route.php
+++ b/src/Routing/Route/Route.php
@@ -22,6 +22,7 @@
use InvalidArgumentException;
use Psr\Http\Message\ServerRequestInterface;
use UnitEnum;
+use function Cake\Routing\urldecodeSegments;
/**
* A single Route used by the Router to connect requests to
@@ -468,8 +469,8 @@ public function parse(string $url, string $method): ?array
[$url, $ext] = $this->_parseExtension($url);
$urldecode = $this->options['_urldecode'] ?? true;
- if ($urldecode) {
- $url = urldecode($url);
+ if ($urldecode && str_contains($url, '%')) {
+ $url = urldecodeSegments($url);
}
if (!preg_match($compiledRoute, $url, $route)) {
diff --git a/src/Routing/RouteCollection.php b/src/Routing/RouteCollection.php
index ecec092a060..a7b30f51809 100644
--- a/src/Routing/RouteCollection.php
+++ b/src/Routing/RouteCollection.php
@@ -136,15 +136,10 @@ public function add(Route $route, array $options = []): void
public function parseRequest(ServerRequestInterface $request): array
{
$uri = $request->getUri();
+
$urlPath = $uri->getPath();
if (str_contains($urlPath, '%')) {
- // decode urlencoded segments, but don't decode %2f aka /
- $parts = explode('/', $urlPath);
- $parts = array_map(
- fn(string $part) => str_replace('/', '%2f', urldecode($part)),
- $parts,
- );
- $urlPath = implode('/', $parts);
+ $urlPath = urldecodeSegments($urlPath);
}
if ($urlPath !== '/') {
$urlPath = rtrim($urlPath, '/');
@@ -294,10 +289,17 @@ public function match(array $url, array $context): string
if ($out) {
return $out;
}
+ $message = sprintf(
+ 'A named route was found for `%s`, but matching failed. Passed parameters: `%s`.',
+ $name,
+ (string)json_encode($url),
+ );
+
throw new MissingRouteException([
'url' => $name,
'context' => $context,
- 'message' => "A named route was found for `{$name}`, but matching failed.",
+ // Escape `%` so the message survives CakeException's vsprintf() pass unchanged.
+ 'message' => str_replace('%', '%%', $message),
]);
}
throw new MissingRouteException(['url' => $name, 'context' => $context]);
diff --git a/src/Routing/functions.php b/src/Routing/functions.php
index 1c340ccf659..23a7f814b90 100644
--- a/src/Routing/functions.php
+++ b/src/Routing/functions.php
@@ -57,3 +57,20 @@ function url(UriInterface|array|string|null $url = null, bool $full = false): st
{
return Router::url($url, $full);
}
+
+/**
+ * urldecode all of the segments in a path, but skip over %2f
+ * because applications can use %2f in a segment.
+ *
+ * @internal
+ */
+function urldecodeSegments(string $url): string
+{
+ $parts = explode('/', $url);
+ $parts = array_map(
+ fn(string $part) => str_replace('/', '%2f', urldecode($part)),
+ $parts,
+ );
+
+ return implode('/', $parts);
+}
diff --git a/src/TestSuite/MiddlewareDispatcher.php b/src/TestSuite/MiddlewareDispatcher.php
index 4e6f18ab892..51e053921cc 100644
--- a/src/TestSuite/MiddlewareDispatcher.php
+++ b/src/TestSuite/MiddlewareDispatcher.php
@@ -124,6 +124,7 @@ protected function _createRequest(array $spec): ServerRequest
array_merge($_SERVER, ['REQUEST_URI' => $spec['url']]),
$spec['environment'],
);
+ /** @phpstan-ignore offsetAccess.notFound */
if (str_contains($environment['PHP_SELF'], 'phpunit')) {
$environment['PHP_SELF'] = '/';
}
diff --git a/src/TestSuite/TestCase.php b/src/TestSuite/TestCase.php
index 368db63f82b..c68b7852cad 100644
--- a/src/TestSuite/TestCase.php
+++ b/src/TestSuite/TestCase.php
@@ -562,11 +562,10 @@ public function assertTextEquals(string $expected, string $result, string $messa
* Asserts that a string starts with a given prefix, ignoring differences in newlines.
* Helpful for doing cross platform tests of blocks of text.
*
- * @param string $prefix The prefix to check for.
+ * @param non-empty-string $prefix The prefix to check for.
* @param string $string The string to search in.
* @param string $message The message to use for failure.
* @return void
- * @phpstan-param non-empty-string $prefix
*/
public function assertTextStartsWith(string $prefix, string $string, string $message = ''): void
{
@@ -580,11 +579,10 @@ public function assertTextStartsWith(string $prefix, string $string, string $mes
* Asserts that a string starts not with a given prefix, ignoring differences in newlines.
* Helpful for doing cross platform tests of blocks of text.
*
- * @param string $prefix The prefix to not find.
+ * @param non-empty-string $prefix The prefix to not find.
* @param string $string The string to search.
* @param string $message The message to use for failure.
* @return void
- * @phpstan-param non-empty-string $prefix
*/
public function assertTextStartsNotWith(string $prefix, string $string, string $message = ''): void
{
@@ -598,11 +596,10 @@ public function assertTextStartsNotWith(string $prefix, string $string, string $
* Asserts that a string ends with a given prefix, ignoring differences in newlines.
* Helpful for doing cross platform tests of blocks of text.
*
- * @param string $suffix The suffix to find.
+ * @param non-empty-string $suffix The suffix to find.
* @param string $string The string to search.
* @param string $message The message to use for failure.
* @return void
- * @phpstan-param non-empty-string $suffix
*/
public function assertTextEndsWith(string $suffix, string $string, string $message = ''): void
{
@@ -616,11 +613,10 @@ public function assertTextEndsWith(string $suffix, string $string, string $messa
* Asserts that a string ends not with a given prefix, ignoring differences in newlines.
* Helpful for doing cross platform tests of blocks of text.
*
- * @param string $suffix The suffix to not find.
+ * @param non-empty-string $suffix The suffix to not find.
* @param string $string The string to search.
* @param string $message The message to use for failure.
* @return void
- * @phpstan-param non-empty-string $suffix
*/
public function assertTextEndsNotWith(string $suffix, string $string, string $message = ''): void
{
diff --git a/src/TestSuite/TestEmailTransport.php b/src/TestSuite/TestEmailTransport.php
index 0b34fd257ce..5ad9a2a6a0a 100644
--- a/src/TestSuite/TestEmailTransport.php
+++ b/src/TestSuite/TestEmailTransport.php
@@ -38,8 +38,7 @@ class TestEmailTransport extends DebugTransport
* Stores email for later assertions
*
* @param \Cake\Mailer\Message $message Message
- * @return array Contains 'headers' and 'message' keys. Additional keys allowed.
- * @phpstan-return array{headers: string, message: string, ...}
+ * @return array{headers: string, message: string, ...} Contains 'headers' and 'message' keys. Additional keys allowed.
*/
public function send(Message $message): array
{
diff --git a/src/Utility/Hash.php b/src/Utility/Hash.php
index f8abbda50db..5f96e2c3099 100644
--- a/src/Utility/Hash.php
+++ b/src/Utility/Hash.php
@@ -53,6 +53,9 @@ class Hash
* @return mixed The value fetched from the array, or $default if path doesn't exist, is null,
* or $data is empty.
* @link https://book.cakephp.org/5/en/core-libraries/hash.html#hash-get
+ * @psalm-taint-specialize Psalm tracks taint per call site instead of globally, preventing
+ * false positives where taint from one caller (e.g. request body) bleeds into unrelated
+ * callers of this generic utility (e.g. Configure::read, getParam).
*/
public static function get(ArrayAccess|array $data, array|string|int|null $path, mixed $default = null): mixed
{
@@ -113,10 +116,9 @@ public static function get(ArrayAccess|array $data, array|string|int|null $path,
*
* @param \ArrayAccess|array $data The data to extract from.
* @param string $path The path to extract.
- * @return \ArrayAccess|array An array of the extracted values. Returns an empty array
+ * @return ($path is non-empty-string ? array : \ArrayAccess|array) An array of the extracted values. Returns an empty array
* if there are no matches.
* @link https://book.cakephp.org/5/en/core-libraries/hash.html#hash-extract
- * @phpstan-return ($path is non-empty-string ? array : \ArrayAccess|array)
*/
public static function extract(ArrayAccess|array $data, string $path): ArrayAccess|array
{
@@ -294,9 +296,11 @@ protected static function _matches(ArrayAccess|array $data, string $selector): b
* @param T $data The data to insert into.
* @param string $path The path to insert at.
* @param mixed $values The values to insert.
- * @return \ArrayAccess|array The data with $values inserted.
- * @phpstan-return (T is array ? array : \ArrayAccess)
+ * @return (T is array ? array : \ArrayAccess) The data with $values inserted.
* @link https://book.cakephp.org/5/en/core-libraries/hash.html#hash-insert
+ * @psalm-taint-specialize Psalm tracks taint per call site instead of globally, preventing
+ * false positives where taint from one caller (e.g. ServerRequest::withData) bleeds into
+ * unrelated callers of this generic utility (e.g. Configure::write).
*/
public static function insert(ArrayAccess|array $data, string $path, mixed $values = null): ArrayAccess|array
{
@@ -349,6 +353,10 @@ public static function insert(ArrayAccess|array $data, string $path, mixed $valu
* @param array $path The path to work on.
* @param mixed $values The values to insert when doing inserts.
* @return \ArrayAccess|array
+ * @psalm-taint-specialize Psalm tracks taint per call site instead of globally. Without this,
+ * the ArrayAccess|array $data parameter causes Psalm to dispatch taint through every
+ * ArrayAccess::offsetSet implementation in the codebase (e.g. Validator) when this method
+ * is called with tainted request data.
*/
protected static function _simpleOp(
string $op,
@@ -398,9 +406,11 @@ protected static function _simpleOp(
* @template T of \ArrayAccess|array
* @param T $data The data to operate on
* @param string $path A path expression to use to remove.
- * @return \ArrayAccess|array The modified array.
- * @phpstan-return (T is array ? array : \ArrayAccess)
+ * @return (T is array ? array : \ArrayAccess) The modified array.
* @link https://book.cakephp.org/5/en/core-libraries/hash.html#hash-remove
+ * @psalm-taint-specialize Psalm tracks taint per call site instead of globally, preventing
+ * false positives where taint from one caller (e.g. ServerRequest::withoutData) bleeds into
+ * unrelated callers of this generic utility (e.g. Configure::delete).
*/
public static function remove(ArrayAccess|array $data, string $path): ArrayAccess|array
{
@@ -553,12 +563,11 @@ public static function combine(
* @param array $data Source array from which to extract the data
* @param array $paths An array containing one or more Hash::extract()-style key paths
* @param string $format Format string into which values will be inserted, see sprintf()
- * @return array|null An array of strings extracted from `$path` and formatted with `$format`,
+ * @return ($paths is non-empty-array ? array : null) An array of strings extracted from `$path` and formatted with `$format`,
* or null if $paths is empty.
* @link https://book.cakephp.org/5/en/core-libraries/hash.html#hash-format
* @see sprintf()
* @see \Cake\Utility\Hash::extract()
- * @phpstan-return ($paths is non-empty-array ? array : null)
*/
public static function format(array $data, array $paths, string $format): ?array
{
@@ -731,9 +740,8 @@ public static function flatten(array $data, string $separator = '.'): array
* into a multi-dimensional array. So, `['0.Foo.Bar' => 'Far']` becomes
* `[['Foo' => ['Bar' => 'Far']]]`.
*
- * @phpstan-param non-empty-string $separator
* @param array $data Flattened array
- * @param string $separator The delimiter used
+ * @param non-empty-string $separator The delimiter used
* @return array
* @link https://book.cakephp.org/5/en/core-libraries/hash.html#hash-expand
*/
@@ -776,6 +784,10 @@ public static function expand(array $data, string $separator = '.'): array
* @param mixed $merge Array to merge with. The argument and all trailing arguments will be array cast when merged
* @return array Merged array
* @link https://book.cakephp.org/5/en/core-libraries/hash.html#hash-merge
+ * @psalm-taint-specialize Psalm tracks taint per call site instead of globally, preventing
+ * false positives where taint from one caller (e.g. ServerRequestFactory merging POST body
+ * with uploaded files) bleeds into unrelated callers (e.g. InstanceConfigTrait merging
+ * developer configuration, ServerRequest::addDetector merging detector definitions).
*/
public static function merge(array $data, mixed $merge): array
{
@@ -1212,12 +1224,11 @@ public static function normalize(array $data, bool $assoc = true, mixed $default
* - `root` The id of the desired top-most result.
*
* @param array $data The data to nest.
- * @param array $options Options.
+ * @param array{idPath?: string, parentPath?: string, children?: string, root?: string|null} $options Options.
* @return array of results, nested
* @see \Cake\Utility\Hash::extract()
* @throws \InvalidArgumentException When providing invalid data.
* @link https://book.cakephp.org/5/en/core-libraries/hash.html#hash-nest
- * @phpstan-param array{idPath?: string, parentPath?: string, children?: string, root?: string|null} $options
*/
public static function nest(array $data, array $options = []): array
{
diff --git a/src/Utility/Text.php b/src/Utility/Text.php
index ef2349b9eec..57a5099058c 100644
--- a/src/Utility/Text.php
+++ b/src/Utility/Text.php
@@ -422,10 +422,9 @@ public static function wrapBlock(string $text, array|int $options = []): string
/**
* Unicode and newline aware version of wordwrap.
*
- * @phpstan-param non-empty-string $break
* @param string $text The text to format.
* @param int $width The width to wrap to. Defaults to 72.
- * @param string $break The line is broken using the optional break parameter. Defaults to '\n'.
+ * @param non-empty-string $break The line is broken using the optional break parameter. Defaults to '\n'.
* @param bool $cut If the cut is set to true, the string is always wrapped at the specified width.
* @return string Formatted text.
*/
diff --git a/src/Utility/Xml.php b/src/Utility/Xml.php
index c49c4d4771f..442a4e01035 100644
--- a/src/Utility/Xml.php
+++ b/src/Utility/Xml.php
@@ -412,9 +412,8 @@ protected static function _fromArray(
/**
* Helper to _fromArray(). It will create children of arrays
*
- * @param array $data Array with information to create children
+ * @param array{dom: \DOMDocument, node: \DOMNode, key: string, format: string, value?: mixed} $data Array with information to create children
* @return void
- * @phpstan-param array{dom: \DOMDocument, node: \DOMNode, key: string, format: string, value?: mixed} $data
*/
protected static function _createChild(array $data): void
{
diff --git a/src/Validation/RulesProvider.php b/src/Validation/RulesProvider.php
index 077b7cc77f0..ef98036185f 100644
--- a/src/Validation/RulesProvider.php
+++ b/src/Validation/RulesProvider.php
@@ -46,9 +46,8 @@ class RulesProvider
/**
* Constructor, sets the default class to use for calling methods
*
- * @param object|string $class the default class to proxy
+ * @param object|class-string $class the default class to proxy
* @throws \ReflectionException
- * @phpstan-param object|class-string $class
*/
public function __construct(object|string $class = Validation::class)
{
diff --git a/src/Validation/Validator.php b/src/Validation/Validator.php
index 81934e83c26..e407236b2ba 100644
--- a/src/Validation/Validator.php
+++ b/src/Validation/Validator.php
@@ -139,16 +139,14 @@ class Validator implements ArrayAccess, IteratorAggregate, Countable
* An associative array of objects or classes containing methods
* used for validation
*
- * @var array
- * @phpstan-var array
+ * @var array
*/
protected array $_providers = [];
/**
* An associative array of objects or classes used as a default provider list
*
- * @var array
- * @phpstan-var array
+ * @var array
*/
protected static array $_defaultProviders = [];
@@ -314,8 +312,7 @@ public function hasField(string $name): bool
* when called will receive the full list of providers stored in this validator.
*
* @param string $name The name under which the provider should be set.
- * @param object|string $object Provider object or class name.
- * @phpstan-param object|class-string $object
+ * @param object|class-string $object Provider object or class name.
* @return $this
*/
public function setProvider(string $name, object|string $object)
@@ -351,8 +348,7 @@ public static function getDefaultProvider(string $name): object|string|null
* Associates an object to a name so it can be used as a default provider.
*
* @param string $name The name under which the provider should be set.
- * @param object|string $object Provider object or class name.
- * @phpstan-param object|class-string $object
+ * @param object|class-string $object Provider object or class name.
* @return void
*/
public static function addDefaultProvider(string $name, object|string $object): void
diff --git a/src/View/Helper/FormHelper.php b/src/View/Helper/FormHelper.php
index c9bc5c57fa6..0b08a12c9f2 100644
--- a/src/View/Helper/FormHelper.php
+++ b/src/View/Helper/FormHelper.php
@@ -1401,7 +1401,13 @@ protected function enumOptions(string $enumClass): array
$hasLabel = true;
} elseif (method_exists($enumClass, 'label')) {
$hasLabel = true;
- deprecationWarning('5.2.0', 'Enums with the `label()` method must implement the `EnumLabelInterface`.');
+ deprecationWarning(
+ '5.2.0',
+ sprintf(
+ 'Enum class `%s` with a `label()` method must implement the `EnumLabelInterface`.',
+ $enumClass,
+ ),
+ );
} else {
$hasLabel = false;
}
diff --git a/src/View/Helper/HtmlHelper.php b/src/View/Helper/HtmlHelper.php
index aeed440de05..98d450c4996 100644
--- a/src/View/Helper/HtmlHelper.php
+++ b/src/View/Helper/HtmlHelper.php
@@ -523,11 +523,10 @@ public function script(array|string $url, array $options = []): ?string
/**
* Generate the "importmap" script tag.
*
- * @param array $map Map array.
+ * @param array{imports?: array, scopes?: array>>, integrity?: array} $map Map array.
* @param array $options Same options as `UrlHelper::script()`.
* @return string
* @since 5.2.0
- * @phpstan-param array{imports?: array, scopes?: array>>, integrity?: array} $map
*/
public function importmap(array $map, array $options = []): string
{
diff --git a/src/View/Helper/PaginatorHelper.php b/src/View/Helper/PaginatorHelper.php
index ecfa89f4359..16f80a22e45 100644
--- a/src/View/Helper/PaginatorHelper.php
+++ b/src/View/Helper/PaginatorHelper.php
@@ -755,8 +755,7 @@ public function numbers(array $options = []): string
*
* @param array $params Params from the numbers() method.
* @param array $options Options from the numbers() method.
- * @return array An array with the start and end numbers.
- * @phpstan-return array{0: int, 1: int}
+ * @return array{0: int, 1: int} An array with the start and end numbers.
*/
protected function _getNumbersStartAndEnd(array $params, array $options): array
{
diff --git a/src/View/View.php b/src/View/View.php
index 7010edef7a7..54fd512df4d 100644
--- a/src/View/View.php
+++ b/src/View/View.php
@@ -285,8 +285,7 @@ class View implements EventDispatcherInterface
/**
* ViewBlock class.
*
- * @var string
- * @phpstan-var class-string<\Cake\View\ViewBlock>
+ * @var class-string<\Cake\View\ViewBlock>
*/
protected string $_viewBlockClass = ViewBlock::class;
@@ -650,7 +649,7 @@ public function setLayout(string $name)
* or `MyPlugin.template` to use the template element from MyPlugin. If the element
* is not found in the plugin, the normal view path cascade will be searched.
* @param array $data Array of data to be made available to the rendered view (i.e. the Element)
- * @param array $options Array of options. Possible keys are:
+ * @param array{cache?:array|true, callbacks?:bool, plugin?:string|false, ignoreMissing?:bool} $options Array of options. Possible keys are:
*
* - `cache` - Can either be `true`, to enable caching using the config in View::$elementCache. Or an array
* If an array, the following keys can be used:
@@ -666,7 +665,6 @@ public function setLayout(string $name)
* @return string Rendered Element
* @throws \Cake\View\Exception\MissingElementException When an element is missing and `ignoreMissing`
* is false.
- * @phpstan-param array{cache?:array|true, callbacks?:bool, plugin?:string|false, ignoreMissing?:bool} $options
*/
public function element(string $name, array $data = [], array $options = []): string
{
@@ -1449,8 +1447,7 @@ protected function _checkFilePath(string $file, ?string $plugin): string
*
* @param string $name The name you want to plugin split.
* @param bool $fallback If true uses the plugin set in the current Request when parsed plugin is not loaded
- * @return array Array with 2 indexes. 0 => plugin name, 1 => filename.
- * @phpstan-return array{string|null, string}
+ * @return array{0:string|null, 1:string} Array with 2 indexes. 0 => plugin name, 1 => filename.
*/
public function pluginSplit(string $name, bool $fallback = true): array
{
@@ -1655,13 +1652,12 @@ protected function _paths(?string $plugin = null, bool $cached = true): array
* @param string $name Element name
* @param array $data Data
* @param array $options Element options
- * @return array Element Cache configuration.
- * @phpstan-return array{key:string, config:string}
+ * @return array{key:string, config:string} Element Cache configuration.
*/
protected function _elementCache(string $name, array $data, array $options): array
{
if (isset($options['cache']['key'], $options['cache']['config'])) {
- /** @phpstan-var array{key:string, config:string} $cache */
+ /** @var array{key:string, config:string} $cache */
$cache = $options['cache'];
$cache['key'] = 'element_' . $cache['key'];
diff --git a/src/View/ViewBuilder.php b/src/View/ViewBuilder.php
index adaba7271bc..b6200e84a7f 100644
--- a/src/View/ViewBuilder.php
+++ b/src/View/ViewBuilder.php
@@ -111,8 +111,7 @@ class ViewBuilder implements JsonSerializable
* Can either use plugin notation, a short name
* or a fully namespaced classname.
*
- * @var string|null
- * @phpstan-var class-string<\Cake\View\View>|string|null
+ * @var class-string<\Cake\View\View>|string|null
*/
protected ?string $_className = null;
diff --git a/tests/TestCase/Cache/Engine/RedisClusterEngineTest.php b/tests/TestCase/Cache/Engine/RedisClusterEngineTest.php
index 2b557dc7eea..bf79ad7f5a8 100644
--- a/tests/TestCase/Cache/Engine/RedisClusterEngineTest.php
+++ b/tests/TestCase/Cache/Engine/RedisClusterEngineTest.php
@@ -136,6 +136,7 @@ public function testConfig(): void
'unix_socket' => false,
'clearUsesFlushDb' => false,
'failover' => null,
+ 'allowedClasses' => true,
];
$this->assertEquals($expecting, $config);
}
diff --git a/tests/TestCase/Cache/Engine/RedisEngineTest.php b/tests/TestCase/Cache/Engine/RedisEngineTest.php
index 86a62f06cd0..1d0b9c5d66e 100644
--- a/tests/TestCase/Cache/Engine/RedisEngineTest.php
+++ b/tests/TestCase/Cache/Engine/RedisEngineTest.php
@@ -16,12 +16,14 @@
*/
namespace Cake\Test\TestCase\Cache\Engine;
+use __PHP_Incomplete_Class;
use Cake\Cache\Cache;
use Cake\Cache\Engine\RedisEngine;
use Cake\TestSuite\TestCase;
use DateInterval;
use Mockery;
use Redis;
+use stdClass;
use function Cake\Core\env;
/**
@@ -123,6 +125,7 @@ public function testConfig(): void
'nodes' => [],
'clearUsesFlushDb' => false,
'failover' => null,
+ 'allowedClasses' => true,
];
$this->assertEquals($expecting, $config);
}
@@ -157,6 +160,7 @@ public function testConfigDsn(): void
'nodes' => [],
'clearUsesFlushDb' => false,
'failover' => null,
+ 'allowedClasses' => true,
];
$this->assertEquals($expecting, $config);
}
@@ -198,6 +202,7 @@ public function testConfigDsnSSLContext(): void
'nodes' => [],
'clearUsesFlushDb' => false,
'failover' => null,
+ 'allowedClasses' => true,
];
$this->assertEquals($expecting, $config);
}
@@ -539,6 +544,80 @@ public function testReadAndWriteCache(): void
Cache::delete('test', 'redis');
}
+ /**
+ * Objects are unserialized normally with the default `allowedClasses` of `true`.
+ */
+ public function testAllowedClassesDefaultAllowsObjects(): void
+ {
+ $this->_configCache();
+
+ $data = new stdClass();
+ $data->foo = 'bar';
+ $this->assertTrue(Cache::write('object', $data, 'redis'));
+
+ $result = Cache::read('object', 'redis');
+ $this->assertInstanceOf(stdClass::class, $result);
+ $this->assertSame('bar', $result->foo);
+
+ Cache::delete('object', 'redis');
+ }
+
+ /**
+ * Setting `allowedClasses` to `false` blocks object unserialization,
+ * yielding an incomplete class instance instead of the original object.
+ */
+ public function testAllowedClassesFalseBlocksObjects(): void
+ {
+ $this->_configCache(['allowedClasses' => false]);
+
+ $data = new stdClass();
+ $data->foo = 'bar';
+ $this->assertTrue(Cache::write('object', $data, 'redis'));
+
+ $result = Cache::read('object', 'redis');
+ $this->assertInstanceOf(__PHP_Incomplete_Class::class, $result);
+
+ Cache::delete('object', 'redis');
+ }
+
+ /**
+ * An array of `allowedClasses` permits only the listed classes; others
+ * are returned as incomplete class instances.
+ */
+ public function testAllowedClassesWhitelist(): void
+ {
+ $this->_configCache(['allowedClasses' => [stdClass::class]]);
+
+ $allowed = new stdClass();
+ $allowed->foo = 'bar';
+ $this->assertTrue(Cache::write('allowed', $allowed, 'redis'));
+ $this->assertInstanceOf(stdClass::class, Cache::read('allowed', 'redis'));
+
+ $blocked = new DateInterval('PT1S');
+ $this->assertTrue(Cache::write('blocked', $blocked, 'redis'));
+ $this->assertInstanceOf(__PHP_Incomplete_Class::class, Cache::read('blocked', 'redis'));
+
+ Cache::delete('allowed', 'redis');
+ Cache::delete('blocked', 'redis');
+ }
+
+ /**
+ * Integers keep round-tripping correctly regardless of `allowedClasses`.
+ */
+ public function testAllowedClassesPreservesScalars(): void
+ {
+ $this->_configCache(['allowedClasses' => false]);
+
+ $this->assertTrue(Cache::write('int', 42, 'redis'));
+ $this->assertSame(42, Cache::read('int', 'redis'));
+
+ $this->assertTrue(Cache::write('array', ['a' => 1, 'b' => 2], 'redis'));
+ $this->assertSame(['a' => 1, 'b' => 2], Cache::read('array', 'redis'));
+
+ Cache::delete('int', 'redis');
+ Cache::delete('array', 'redis');
+ }
+
/**
* Test get with default value
*/
diff --git a/tests/TestCase/Database/Driver/PostgresTest.php b/tests/TestCase/Database/Driver/PostgresTest.php
index 47a2465caed..ad8324a94fb 100644
--- a/tests/TestCase/Database/Driver/PostgresTest.php
+++ b/tests/TestCase/Database/Driver/PostgresTest.php
@@ -19,6 +19,7 @@
use Cake\Database\Connection;
use Cake\Database\Driver\Postgres;
use Cake\Database\DriverFeatureEnum;
+use Cake\Database\Exception\QueryException;
use Cake\Database\Query\SelectQuery;
use Cake\Datasource\ConnectionManager;
use Cake\TestSuite\TestCase;
@@ -270,4 +271,43 @@ public function testQuote(): void
$expected = "'O\slash'";
$this->assertEquals($expected, $result);
}
+
+ /**
+ * test jsonValue expression tranformation and parameter binding
+ *
+ * @return void
+ */
+ public function testJsonValueParameterBinding(): void
+ {
+ $connection = ConnectionManager::get('test');
+ $this->skipIf(!$connection->getDriver() instanceof Postgres);
+
+ $stmt = $connection->insert(
+ 'comments',
+ ['article_id' => 1, 'user_id' => 1, 'comment' => ['scores' => [25, 36]]],
+ ['comment' => 'json'],
+ );
+ $this->assertEquals(1, $stmt->rowCount());
+
+ $query = new SelectQuery($connection);
+ $query
+ ->select(['score' => $query->func()->jsonValue('comment', '$.scores[1]')])
+ ->from('comments')
+ ->where(['id' => 1]);
+
+ $result = $query->execute();
+ $comment = $result->fetchAll('assoc');
+ $result->closeCursor();
+ $this->assertSame('36', $comment[0]['score']);
+
+ $query = new SelectQuery($connection);
+ $query
+ ->select(['score' => $query->func()->jsonValue('comment', "x')) > '0' OR (SELECT 1) --")])
+ ->from('comments')
+ ->where(['id' => 1]);
+
+ $this->expectException(QueryException::class);
+ $this->expectExceptionMessage('syntax error at end of jsonpath input');
+ $query->execute();
+ }
}
diff --git a/tests/TestCase/Database/FunctionsBuilderTest.php b/tests/TestCase/Database/FunctionsBuilderTest.php
index 082c74f10ee..3177e172bd8 100644
--- a/tests/TestCase/Database/FunctionsBuilderTest.php
+++ b/tests/TestCase/Database/FunctionsBuilderTest.php
@@ -21,6 +21,8 @@
use Cake\Database\FunctionsBuilder;
use Cake\Database\ValueBinder;
use Cake\TestSuite\TestCase;
+use InvalidArgumentException;
+use PHPUnit\Framework\Attributes\DataProvider;
/**
* Tests FunctionsBuilder class
@@ -172,6 +174,26 @@ public function testCast(): void
$this->assertSame('string', $function->getReturnType());
}
+ public static function invalidValues(): array
+ {
+ return [
+ ['words with spaces'],
+ ["' drop table users --"],
+ [' word '],
+ ];
+ }
+
+ /**
+ * Ensure that only alphanumeric values are accepted for types.
+ */
+ #[DataProvider('invalidValues')]
+ public function testCastInvalidValue(string $type): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('`dataType` must be an alphanumeric string');
+ $this->functions->cast('field', $type);
+ }
+
/**
* Tests generating a NOW(), CURRENT_TIME() and CURRENT_DATE() function
*/
@@ -209,6 +231,22 @@ public function testExtract(): void
$this->assertSame('integer', $function->getReturnType());
}
+ #[DataProvider('invalidValues')]
+ public function testExtractInvalidValue(string $part): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('`part` must be an alphanumeric string');
+ $this->functions->extract($part, 'created');
+ }
+
+ #[DataProvider('invalidValues')]
+ public function testDatePartInvalidValue(string $part): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('`part` must be an alphanumeric string');
+ $this->functions->datePart($part, 'created');
+ }
+
/**
* Tests generating a DATE_ADD() function
*/
@@ -224,6 +262,14 @@ public function testDateAdd(): void
$this->assertSame('DATE_ADD(created, INTERVAL -3 day)', $function->sql(new ValueBinder()));
}
+ #[DataProvider('invalidValues')]
+ public function testDateAddInvalidValue(string $unit): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('`unit` must be an alphanumeric string');
+ $this->functions->dateAdd('created', -3, $unit);
+ }
+
/**
* Tests generating a DAYOFWEEK() function
*/
diff --git a/tests/TestCase/Error/DebuggerTest.php b/tests/TestCase/Error/DebuggerTest.php
index 1887ef5d0de..217db518bd3 100644
--- a/tests/TestCase/Error/DebuggerTest.php
+++ b/tests/TestCase/Error/DebuggerTest.php
@@ -65,6 +65,9 @@ protected function setUp(): void
Log::drop('stderr');
Log::drop('stdout');
Debugger::configInstance('exportFormatter', TextFormatter::class);
+ // The instances need to be initialized for the tests to pass
+ Debugger::getInstance(TestDebugger::class);
+ Debugger::getInstance(Debugger::class);
}
/**
@@ -659,9 +662,14 @@ public function testDebugInfo(): void
public function testSetOutputMask(): void
{
Debugger::setOutputMask(['password' => '[**********]']);
- $this->assertEquals(['password' => '[**********]'], Debugger::outputMask());
+ $result = Debugger::outputMask();
+ $this->assertEquals('[**********]', $result['password']);
+ $this->assertEquals('********', $result['login']);
+ $this->assertEquals('********', $result['host']);
Debugger::setOutputMask(['serial' => 'XXXXXX']);
- $this->assertEquals(['password' => '[**********]', 'serial' => 'XXXXXX'], Debugger::outputMask());
+ $result = Debugger::outputMask();
+ $this->assertEquals('[**********]', $result['password']);
+ $this->assertEquals('XXXXXX', $result['serial']);
Debugger::setOutputMask([], false);
$this->assertSame([], Debugger::outputMask());
}
@@ -703,6 +711,71 @@ public function testMaskObject(): void
$this->assertSame($expected, preg_replace('/\s+/', '', $result));
}
+ /**
+ * Tests that default outputMask keys are masked by default.
+ */
+ public function testDefaultOutputMaskMasksPassword(): void
+ {
+ $result = Debugger::exportVar(['password' => 'supersecret']);
+ $this->assertStringContainsString('********', $result);
+ $this->assertStringNotContainsString('supersecret', $result);
+ }
+
+ /**
+ * Tests that all default outputMask keys are present.
+ */
+ public function testDefaultOutputMaskContainsAllKeys(): void
+ {
+ $mask = Debugger::outputMask();
+ $this->assertArrayHasKey('password', $mask);
+ $this->assertArrayHasKey('login', $mask);
+ $this->assertArrayHasKey('host', $mask);
+ $this->assertArrayHasKey('database', $mask);
+ $this->assertArrayHasKey('port', $mask);
+ $this->assertArrayHasKey('prefix', $mask);
+ $this->assertArrayHasKey('schema', $mask);
+ $this->assertSame('********', $mask['password']);
+ $this->assertSame('********', $mask['login']);
+ $this->assertSame('********', $mask['host']);
+ $this->assertSame('********', $mask['database']);
+ $this->assertSame('********', $mask['port']);
+ $this->assertSame('********', $mask['prefix']);
+ $this->assertSame('********', $mask['schema']);
+ }
+
+ /**
+ * Tests that custom masks merge with defaults.
+ */
+ public function testCustomMaskMergesWithDefaults(): void
+ {
+ Debugger::setOutputMask(['api_key' => 'SECRET']);
+ $mask = Debugger::outputMask();
+ $this->assertSame('SECRET', $mask['api_key']);
+ $this->assertSame('********', $mask['password']);
+ $this->assertSame('********', $mask['host']);
+ }
+
+ /**
+ * Tests that non-default array keys are not masked.
+ */
+ public function testNonDefaultKeysAreNotMasked(): void
+ {
+ $result = Debugger::exportVar(['username' => 'admin', 'email' => 'test@example.com']);
+ $this->assertStringContainsString('admin', $result);
+ $this->assertStringContainsString('test@example.com', $result);
+ }
+
+ /**
+ * Tests that default mask works on object properties.
+ */
+ public function testDefaultMaskOnObjectProperties(): void
+ {
+ $object = new SecurityThing();
+ $result = Debugger::exportVar($object);
+ $this->assertStringContainsString('********', $result);
+ $this->assertStringNotContainsString('pass1234', $result);
+ }
+
/**
* test testPrintVar()
*/
diff --git a/tests/TestCase/Error/Renderer/WebExceptionRendererTest.php b/tests/TestCase/Error/Renderer/WebExceptionRendererTest.php
index b41a1c17163..d05228b0301 100644
--- a/tests/TestCase/Error/Renderer/WebExceptionRendererTest.php
+++ b/tests/TestCase/Error/Renderer/WebExceptionRendererTest.php
@@ -14,7 +14,7 @@
* @since 2.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
-namespace Cake\Test\TestCase\Error;
+namespace Cake\Test\TestCase\Error\Renderer;
use Cake\Controller\Controller;
use Cake\Controller\ErrorController;
diff --git a/tests/TestCase/Http/Client/Adapter/CurlTest.php b/tests/TestCase/Http/Client/Adapter/CurlTest.php
index 8341877bb38..48daf5e3f51 100644
--- a/tests/TestCase/Http/Client/Adapter/CurlTest.php
+++ b/tests/TestCase/Http/Client/Adapter/CurlTest.php
@@ -43,7 +43,9 @@ protected function setUp(): void
$this->skipIf(!function_exists('curl_init'), 'Skipping as ext/curl is not installed.');
$this->curl = new Curl();
- $this->caFile = CaBundle::getBundledCaBundlePath();
+ // Mirror the adapter default: an explicit `curl.cainfo` (php.ini) wins,
+ // otherwise the bundled CA file is used.
+ $this->caFile = ini_get('curl.cainfo') ?: CaBundle::getBundledCaBundlePath();
}
/**
@@ -122,6 +124,21 @@ public function testBuildOptionsGet(): void
$this->assertSame($expected, $result);
}
+ /**
+ * An explicitly passed `ssl_cafile` must take precedence over both the
+ * `curl.cainfo` php.ini setting and the bundled CA file.
+ */
+ public function testBuildOptionsExplicitCafileWins(): void
+ {
+ $options = [
+ 'ssl_cafile' => '/custom/ca-bundle.pem',
+ ];
+ $request = new Request('http://localhost/things', 'GET');
+ $result = $this->curl->buildOptions($request, $options);
+
+ $this->assertSame('/custom/ca-bundle.pem', $result[CURLOPT_CAINFO]);
+ }
+
/**
* Test converting client options into curl ones.
*/
diff --git a/tests/TestCase/Http/MimeTypeTest.php b/tests/TestCase/Http/MimeTypeTest.php
index 75c18bcf42d..cd8fefdceb9 100644
--- a/tests/TestCase/Http/MimeTypeTest.php
+++ b/tests/TestCase/Http/MimeTypeTest.php
@@ -14,7 +14,7 @@
* @since 5.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
-namespace Cake\Http\Test\TestCase;
+namespace Cake\Test\TestCase\Http;
use Cake\Http\MimeType;
use Cake\TestSuite\TestCase;
diff --git a/tests/TestCase/Mailer/Transport/MailTransportTest.php b/tests/TestCase/Mailer/Transport/MailTransportTest.php
index 0d1944e3f74..332748517a0 100644
--- a/tests/TestCase/Mailer/Transport/MailTransportTest.php
+++ b/tests/TestCase/Mailer/Transport/MailTransportTest.php
@@ -114,4 +114,45 @@ public function testSendData(): void
$this->assertStringContainsString('Subject: ', $result['headers']);
$this->assertStringContainsString('To: ', $result['headers']);
}
+
+ /**
+ * test send strips crlf from headers
+ */
+ public function testSendHeadersStripCrlf(): void
+ {
+ $eol = "\r\n";
+ $date = date(DATE_RFC2822);
+
+ $message = new Message();
+ $message->setFrom('noreply@cakephp.org', 'CakePHP Test');
+ $message->setTo('cake@cakephp.org', 'CakePHP');
+ $message->setSubject('injected headers');
+ $message->setMessageId('');
+
+ $message->setHeaders([
+ 'X-inject' => "line one\r\nline two",
+ ]);
+ $message->setBody(['text' => "First Line\nSecond Line"]);
+
+ $data = "From: CakePHP Test {$eol}";
+ $data .= 'X-inject: line oneline two' . $eol;
+ $data .= 'Date: ' . $date . $eol;
+ $data .= 'Message-ID: ' . $eol;
+ $data .= "MIME-Version: 1.0{$eol}";
+ $data .= "Content-Type: text/plain; charset=UTF-8{$eol}";
+ $data .= 'Content-Transfer-Encoding: 8bit';
+
+ $this->MailTransport->expects($this->once())->method('_mail')
+ ->with(
+ 'CakePHP ',
+ 'injected headers',
+ implode($eol, ['First Line', 'Second Line', '', '']),
+ $data,
+ '-f',
+ );
+
+ $result = $this->MailTransport->send($message);
+ $this->assertStringContainsString('Subject: ', $result['headers']);
+ $this->assertStringContainsString('To: ', $result['headers']);
+ }
}
diff --git a/tests/TestCase/Mailer/Transport/SmtpTransportTest.php b/tests/TestCase/Mailer/Transport/SmtpTransportTest.php
index 2cf22ec7b1d..ad0636917e2 100644
--- a/tests/TestCase/Mailer/Transport/SmtpTransportTest.php
+++ b/tests/TestCase/Mailer/Transport/SmtpTransportTest.php
@@ -600,6 +600,52 @@ public function testSendData(): void
$this->SmtpTransport->sendData($message);
}
+ /**
+ * test send strips crlf from headers
+ */
+ public function testSendHeadersStripCrlf(): void
+ {
+ $eol = "\r\n";
+ $date = date(DATE_RFC2822);
+
+ $message = new Message();
+ $message->setFrom('noreply@cakephp.org', 'CakePHP Test');
+ $message->setTo('cake@cakephp.org', 'CakePHP');
+ $message->setSubject('injected headers');
+ $message->setMessageId('');
+
+ $message->setHeaders([
+ 'X-inject' => "line one\r\nline two",
+ ]);
+ $message->setBody(['text' => 'oh no']);
+
+ $data = "From: CakePHP Test {$eol}";
+ $data .= 'To: CakePHP ' . $eol;
+ $data .= 'X-inject: line oneline two' . $eol;
+ $data .= 'Date: ' . $date . $eol;
+ $data .= 'Message-ID: ' . $eol;
+ $data .= 'Subject: injected headers' . $eol;
+ $data .= "MIME-Version: 1.0{$eol}";
+ $data .= "Content-Type: text/plain; charset=UTF-8{$eol}";
+ $data .= 'Content-Transfer-Encoding: 8bit' . $eol;
+ $data .= "\r\n";
+ $data .= "oh no\r\n";
+ $data .= "\r\n\r\n";
+ $data .= "\r\n\r\n.\r\n";
+
+ $this->socket->shouldReceive('read')
+ ->andReturn(
+ "354 OK\r\n",
+ "250 OK\r\n",
+ )
+ ->twice();
+
+ $this->socket->shouldReceive('write')->with("DATA\r\n")->once();
+ $this->socket->shouldReceive('write')->with($data)->once();
+
+ $this->SmtpTransport->sendData($message);
+ }
+
/**
* testQuit method
*/
diff --git a/tests/TestCase/ORM/Query/QueryRegressionTest.php b/tests/TestCase/ORM/Query/QueryRegressionTest.php
index 32381ceab8a..e5fe48d018a 100644
--- a/tests/TestCase/ORM/Query/QueryRegressionTest.php
+++ b/tests/TestCase/ORM/Query/QueryRegressionTest.php
@@ -23,6 +23,7 @@
use Cake\Datasource\EntityInterface;
use Cake\Event\EventInterface;
use Cake\I18n\DateTime;
+use Cake\ORM\Association;
use Cake\ORM\Entity;
use Cake\ORM\Query\SelectQuery;
use Cake\TestSuite\TestCase;
@@ -1849,4 +1850,62 @@ public function testExecutedSubqueryCanBeReused(): void
$this->assertEquals('First Article', $result[0]->title);
$this->assertNotEmpty($result[0]->user);
}
+
+ /**
+ * The subquery strategy must not strip the ORDER BY from the query it is given.
+ *
+ * The loader for the first association would otherwise leave the caller's query without an
+ * ORDER BY, so every subsequent loader builds its filtering subquery unordered, pages on the
+ * wrong rows and returns no results at all.
+ */
+ public function testSubqueryStrategyDoesNotMutateOrder(): void
+ {
+ $authors = $this->getTableLocator()->get('Authors');
+ $authors->hasMany('Articles', ['strategy' => Association::STRATEGY_SUBQUERY]);
+
+ $query = $authors->find()
+ ->contain(['Articles'])
+ ->orderBy(['Authors.id' => 'DESC'])
+ ->limit(2);
+
+ $this->assertCount(1, $query->clause('order'));
+ $query->all()->toArray();
+ $this->assertCount(1, $query->clause('order'), 'The ORDER BY was removed from the query.');
+ }
+
+ /**
+ * Every hasMany in the contain list must be loaded, not just the first one.
+ *
+ * Ordering by id DESC with a limit of 2 pages onto authors 4 and 3. An unordered subquery
+ * would instead filter on authors 1 and 2, so author 4's comment goes missing.
+ */
+ public function testSubqueryStrategyWithMultipleHasMany(): void
+ {
+ $authors = $this->getTableLocator()->get('Authors');
+ $authors->hasMany('Articles', ['strategy' => Association::STRATEGY_SUBQUERY]);
+ $authors->hasMany('Comments', [
+ 'foreignKey' => 'user_id',
+ 'strategy' => Association::STRATEGY_SUBQUERY,
+ ]);
+
+ $results = $this->getTableLocator()->get('Authors')->find()
+ ->contain(['Articles', 'Comments'])
+ ->orderBy(['Authors.id' => 'DESC'])
+ ->limit(2)
+ ->all()
+ ->toArray();
+
+ $this->assertCount(2, $results);
+ $this->assertSame([4, 3], array_map(fn(EntityInterface $author) => $author->id, $results));
+
+ // Second hasMany in the contain list: empty before the fix.
+ $this->assertCount(1, $results[0]->comments);
+ $this->assertSame(4, $results[0]->comments[0]->user_id);
+ $this->assertEmpty($results[0]->articles);
+
+ // First hasMany in the contain list: loaded correctly either way.
+ $this->assertCount(1, $results[1]->articles);
+ $this->assertSame('Second Article', $results[1]->articles[0]->title);
+ $this->assertEmpty($results[1]->comments);
+ }
}
diff --git a/tests/TestCase/Routing/Route/RouteTest.php b/tests/TestCase/Routing/Route/RouteTest.php
index f1fa7f91c64..e12cbd56846 100644
--- a/tests/TestCase/Routing/Route/RouteTest.php
+++ b/tests/TestCase/Routing/Route/RouteTest.php
@@ -1181,6 +1181,28 @@ public function testParseUrlDecodeElements(): void
$this->assertSame('∂∂', $result['slug']);
}
+ /**
+ * Test that segments are urldecoded into parsed route, but not %2f
+ * as applications use %2f for encoded data that path prefix matching
+ * doesn't treat as /
+ */
+ public function testParseUrlDecodeButNot2f(): void
+ {
+ $route = new Route(
+ '/{controller}/{slug}',
+ ['action' => 'view'],
+ );
+ $route->compile();
+ $result = $route->parse('/posts/foo', 'GET');
+ $this->assertSame('foo', $result['slug']);
+
+ $result = $route->parse('/posts/foo%20bar', 'GET');
+ $this->assertSame('foo bar', $result['slug']);
+
+ $result = $route->parse('/posts%2ffoo', 'GET');
+ $this->assertNull($result, 'encoded / should not count as real / ');
+ }
+
/**
* Test numerically indexed defaults, get appended to pass
*/
diff --git a/tests/TestCase/Routing/RouteBuilderTest.php b/tests/TestCase/Routing/RouteBuilderTest.php
index ba0595854c4..1cf7d360249 100644
--- a/tests/TestCase/Routing/RouteBuilderTest.php
+++ b/tests/TestCase/Routing/RouteBuilderTest.php
@@ -19,6 +19,7 @@
use BadMethodCallException;
use Cake\Core\Exception\MissingPluginException;
use Cake\Core\Plugin;
+use Cake\Http\Response;
use Cake\Http\ServerRequest;
use Cake\Routing\Route\InflectedRoute;
use Cake\Routing\Route\RedirectRoute;
@@ -957,7 +958,7 @@ public function testScopeInheritMiddleware(): void
/**
* Test using name prefixes.
*/
- public function testNamePrefixes(): void
+ public function testScopeNamePrefixes(): void
{
$routes = new RouteBuilder($this->collection, '/api', [], ['namePrefix' => 'api:']);
$routes->scope('/v1', ['version' => 1, '_namePrefix' => 'v1:'], function (RouteBuilder $routes): void {
@@ -973,6 +974,41 @@ public function testNamePrefixes(): void
$this->assertArrayHasKey('web:pong', $all);
}
+ /**
+ * Test that if an application has scoped middleware on a path prefix,
+ * and fallback routes, that URL decoding in Route::parse() would
+ * decode the %2f that was not accepted in the scope path prefix check.
+ */
+ public function testScopeMiddlewarePathMatch(): void
+ {
+ $gate = function () {
+ return new Response(['body' => 'no', 'status' => 403]);
+ };
+
+ $routes = new RouteBuilder($this->collection, '/');
+ $routes->registerMiddleware('gate', $gate);
+ $routes->scope('/admin', function (RouteBuilder $routes): void {
+ $routes->applyMiddleware('gate');
+ $routes->connect('/secret', ['controller' => 'Admin', 'action' => 'secret']);
+ });
+ $routes->fallbacks(DashedRoute::class);
+
+ // No encoded / is a standard request.
+ $result = $this->collection->parseRequest(new ServerRequest([
+ 'url' => '/admin/secret',
+ ]));
+ $this->assertEquals(['gate'], $result['_route']->getMiddleware());
+ $this->assertEquals('/admin/secret', $result['_route']->template);
+
+ // Encoded / will not match path prefix. It will match /controller though
+ $result = $this->collection->parseRequest(new ServerRequest([
+ 'url' => '/admin%2Fsecret',
+ ]));
+ $this->assertEquals('/{controller}', $result['_route']->template);
+ $this->assertEquals('Admin%2fsecret', $result['controller']);
+ $this->assertEquals('index', $result['action']);
+ }
+
/**
* Test adding middleware to the collection.
*/
diff --git a/tests/TestCase/Routing/RouteCollectionTest.php b/tests/TestCase/Routing/RouteCollectionTest.php
index 253af476a70..cb888be70e9 100644
--- a/tests/TestCase/Routing/RouteCollectionTest.php
+++ b/tests/TestCase/Routing/RouteCollectionTest.php
@@ -666,7 +666,9 @@ public function testMatchNamed(): void
public function testMatchNamedError(): void
{
$this->expectException(MissingRouteException::class);
- $this->expectExceptionMessage('A named route was found for `fail`, but matching failed');
+ $this->expectExceptionMessage(
+ 'A named route was found for `fail`, but matching failed. Passed parameters: `{"controller":"Articles"}`.',
+ );
$context = [
'_base' => '/',
'_scheme' => 'http',
@@ -675,7 +677,7 @@ public function testMatchNamedError(): void
$routes = new RouteBuilder($this->collection, '/b');
$routes->connect('/{lang}/articles', ['controller' => 'Articles'], ['_name' => 'fail']);
- $this->collection->match(['_name' => 'fail'], $context);
+ $this->collection->match(['_name' => 'fail', 'controller' => 'Articles'], $context);
}
/**
diff --git a/tests/TestCase/TestSuite/Fixture/FixtureHelperTest.php b/tests/TestCase/TestSuite/Fixture/FixtureHelperTest.php
index 86f53df35a3..ecb6fa84bc1 100644
--- a/tests/TestCase/TestSuite/Fixture/FixtureHelperTest.php
+++ b/tests/TestCase/TestSuite/Fixture/FixtureHelperTest.php
@@ -14,7 +14,7 @@
* @since 4.3.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
-namespace Cake\Test\TestCase\TestSuite;
+namespace Cake\Test\TestCase\TestSuite\Fixture;
use Cake\Core\Exception\CakeException;
use Cake\Database\Connection;
diff --git a/tests/TestCase/TestSuite/Fixture/TransactionStrategyTest.php b/tests/TestCase/TestSuite/Fixture/TransactionStrategyTest.php
index 6bf0bad00e2..1cc600dee37 100644
--- a/tests/TestCase/TestSuite/Fixture/TransactionStrategyTest.php
+++ b/tests/TestCase/TestSuite/Fixture/TransactionStrategyTest.php
@@ -14,7 +14,7 @@
* @since 4.3.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
-namespace Cake\Test\TestCase\TestSuite;
+namespace Cake\Test\TestCase\TestSuite\Fixture;
use Cake\Datasource\ConnectionManager;
use Cake\TestSuite\Fixture\TransactionStrategy;
diff --git a/tests/TestCase/TestSuite/Fixture/TruncateStrategyTest.php b/tests/TestCase/TestSuite/Fixture/TruncateStrategyTest.php
index d56ddc122cc..057cbaebe13 100644
--- a/tests/TestCase/TestSuite/Fixture/TruncateStrategyTest.php
+++ b/tests/TestCase/TestSuite/Fixture/TruncateStrategyTest.php
@@ -14,7 +14,7 @@
* @since 4.3.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
-namespace Cake\Test\TestCase\TestSuite;
+namespace Cake\Test\TestCase\TestSuite\Fixture;
use Cake\Datasource\ConnectionManager;
use Cake\TestSuite\Fixture\TruncateStrategy;