From 458931c9c744590400a1408a451a8d7f6b9e190b Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Thu, 3 Nov 2022 16:56:46 +0100 Subject: [PATCH 001/111] [HttpFoundation][Validator] Leverage json_validate() --- RequestMatcher/IsJsonRequestMatcher.php | 8 +------- composer.json | 3 ++- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/RequestMatcher/IsJsonRequestMatcher.php b/RequestMatcher/IsJsonRequestMatcher.php index 5da46840f..875f992be 100644 --- a/RequestMatcher/IsJsonRequestMatcher.php +++ b/RequestMatcher/IsJsonRequestMatcher.php @@ -23,12 +23,6 @@ class IsJsonRequestMatcher implements RequestMatcherInterface { public function matches(Request $request): bool { - try { - json_decode($request->getContent(), true, 512, \JSON_BIGINT_AS_STRING | \JSON_THROW_ON_ERROR); - } catch (\JsonException) { - return false; - } - - return true; + return json_validate($request->getContent()); } } diff --git a/composer.json b/composer.json index e333a23b7..ddcb50251 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,8 @@ "require": { "php": ">=8.1", "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-mbstring": "~1.1" + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php83": "^1.27" }, "require-dev": { "predis/predis": "~1.0", From 003bfc1e42d812e105213f7b44fae34bcc543255 Mon Sep 17 00:00:00 2001 From: Allison Guilhem Date: Mon, 31 Oct 2022 12:45:42 +0100 Subject: [PATCH 002/111] [HttpFoundation] Create migration for session table when pdo handler is used --- CHANGELOG.md | 5 ++ .../Handler/MigratingSessionHandler.php | 11 +--- Session/Storage/Handler/PdoSessionHandler.php | 53 ++++++++++++++++++- .../Storage/Handler/PdoSessionHandlerTest.php | 31 +++++++++++ composer.json | 1 + 5 files changed, 90 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdea3d67e..ae0902547 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +6.3 +--- + + * Create migration for session table when pdo handler is used + 6.2 --- diff --git a/Session/Storage/Handler/MigratingSessionHandler.php b/Session/Storage/Handler/MigratingSessionHandler.php index 1d4255236..46fbeb006 100644 --- a/Session/Storage/Handler/MigratingSessionHandler.php +++ b/Session/Storage/Handler/MigratingSessionHandler.php @@ -22,15 +22,8 @@ */ class MigratingSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface { - /** - * @var \SessionHandlerInterface&\SessionUpdateTimestampHandlerInterface - */ - private \SessionHandlerInterface $currentHandler; - - /** - * @var \SessionHandlerInterface&\SessionUpdateTimestampHandlerInterface - */ - private \SessionHandlerInterface $writeOnlyHandler; + private \SessionHandlerInterface&\SessionUpdateTimestampHandlerInterface $currentHandler; + private \SessionHandlerInterface&\SessionUpdateTimestampHandlerInterface $writeOnlyHandler; public function __construct(\SessionHandlerInterface $currentHandler, \SessionHandlerInterface $writeOnlyHandler) { diff --git a/Session/Storage/Handler/PdoSessionHandler.php b/Session/Storage/Handler/PdoSessionHandler.php index 302372627..a046aef95 100644 --- a/Session/Storage/Handler/PdoSessionHandler.php +++ b/Session/Storage/Handler/PdoSessionHandler.php @@ -11,6 +11,9 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; +use Doctrine\DBAL\Schema\Schema; +use Doctrine\DBAL\Types\Types; + /** * Session handler using a PDO connection to read and write data. * @@ -175,6 +178,52 @@ public function __construct(\PDO|string $pdoOrDsn = null, array $options = []) $this->ttl = $options['ttl'] ?? null; } + public function configureSchema(Schema $schema, \Closure $isSameDatabase): void + { + if ($schema->hasTable($this->table) || !$isSameDatabase($this->getConnection()->exec(...))) { + return; + } + + $table = $schema->createTable($this->table); + switch ($this->driver) { + case 'mysql': + $table->addColumn($this->idCol, Types::BINARY)->setLength(128)->setNotnull(true); + $table->addColumn($this->dataCol, Types::BLOB)->setNotnull(true); + $table->addColumn($this->lifetimeCol, Types::INTEGER)->setUnsigned(true)->setNotnull(true); + $table->addColumn($this->timeCol, Types::INTEGER)->setUnsigned(true)->setNotnull(true); + $table->addOption('collate', 'utf8mb4_bin'); + $table->addOption('engine', 'InnoDB'); + break; + case 'sqlite': + $table->addColumn($this->idCol, Types::TEXT)->setNotnull(true); + $table->addColumn($this->dataCol, Types::BLOB)->setNotnull(true); + $table->addColumn($this->lifetimeCol, Types::INTEGER)->setNotnull(true); + $table->addColumn($this->timeCol, Types::INTEGER)->setNotnull(true); + break; + case 'pgsql': + $table->addColumn($this->idCol, Types::STRING)->setLength(128)->setNotnull(true); + $table->addColumn($this->dataCol, Types::BINARY)->setNotnull(true); + $table->addColumn($this->lifetimeCol, Types::INTEGER)->setNotnull(true); + $table->addColumn($this->timeCol, Types::INTEGER)->setNotnull(true); + break; + case 'oci': + $table->addColumn($this->idCol, Types::STRING)->setLength(128)->setNotnull(true); + $table->addColumn($this->dataCol, Types::BLOB)->setNotnull(true); + $table->addColumn($this->lifetimeCol, Types::INTEGER)->setNotnull(true); + $table->addColumn($this->timeCol, Types::INTEGER)->setNotnull(true); + break; + case 'sqlsrv': + $table->addColumn($this->idCol, Types::TEXT)->setLength(128)->setNotnull(true); + $table->addColumn($this->dataCol, Types::BLOB)->setNotnull(true); + $table->addColumn($this->lifetimeCol, Types::INTEGER)->setUnsigned(true)->setNotnull(true); + $table->addColumn($this->timeCol, Types::INTEGER)->setUnsigned(true)->setNotnull(true); + break; + default: + throw new \DomainException(sprintf('Creating the session table is currently not implemented for PDO driver "%s".', $this->driver)); + } + $table->setPrimaryKey([$this->idCol]); + } + /** * Creates the table to store sessions which can be called once for setup. * @@ -441,8 +490,8 @@ private function buildDsnFromUrl(string $dsnOrUrl): string return $dsn; } } - // If "unix_socket" is not in the query, we continue with the same process as pgsql - // no break + // If "unix_socket" is not in the query, we continue with the same process as pgsql + // no break case 'pgsql': $dsn ??= 'pgsql:'; diff --git a/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php b/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php index 34dad9685..07a82d09a 100644 --- a/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php @@ -11,11 +11,13 @@ namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler; +use Doctrine\DBAL\Schema\Schema; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler; /** * @requires extension pdo_sqlite + * * @group time-sensitive */ class PdoSessionHandlerTest extends TestCase @@ -326,6 +328,35 @@ public function testUrlDsn($url, $expectedDsn, $expectedUser = null, $expectedPa } } + public function testConfigureSchemaDifferentDatabase() + { + $schema = new Schema(); + + $pdoSessionHandler = new PdoSessionHandler($this->getMemorySqlitePdo()); + $pdoSessionHandler->configureSchema($schema, fn() => false); + $this->assertFalse($schema->hasTable('sessions')); + } + + public function testConfigureSchemaSameDatabase() + { + $schema = new Schema(); + + $pdoSessionHandler = new PdoSessionHandler($this->getMemorySqlitePdo()); + $pdoSessionHandler->configureSchema($schema, fn() => true); + $this->assertTrue($schema->hasTable('sessions')); + } + + public function testConfigureSchemaTableExistsPdo() + { + $schema = new Schema(); + $schema->createTable('sessions'); + + $pdoSessionHandler = new PdoSessionHandler($this->getMemorySqlitePdo()); + $pdoSessionHandler->configureSchema($schema, fn() => true); + $table = $schema->getTable('sessions'); + $this->assertEmpty($table->getColumns(), 'The table was not overwritten'); + } + public function provideUrlDsnPairs() { yield ['mysql://localhost/test', 'mysql:host=localhost;dbname=test;']; diff --git a/composer.json b/composer.json index ddcb50251..2023300b8 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "symfony/polyfill-php83": "^1.27" }, "require-dev": { + "doctrine/dbal": "^2.13.1|^3.0", "predis/predis": "~1.0", "symfony/cache": "^5.4|^6.0", "symfony/dependency-injection": "^5.4|^6.0", From d227b691797a5174ff20769c7ba0766f46e74aba Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Wed, 28 Dec 2022 15:47:09 +0100 Subject: [PATCH 003/111] Drop v1 contracts packages everywhere --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 2023300b8..944029655 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ ], "require": { "php": ">=8.1", - "symfony/deprecation-contracts": "^2.1|^3", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.1", "symfony/polyfill-php83": "^1.27" }, From 5018a632c53587f68a2071bf53219b28532595ca Mon Sep 17 00:00:00 2001 From: Alexander Schranz Date: Mon, 24 Oct 2022 21:08:21 +0200 Subject: [PATCH 004/111] [HttpFoundation] Add `StreamedJsonResponse` for efficient JSON streaming --- CHANGELOG.md | 1 + StreamedJsonResponse.php | 139 +++++++++++++++++ Tests/StreamedJsonResponseTest.php | 241 +++++++++++++++++++++++++++++ 3 files changed, 381 insertions(+) create mode 100644 StreamedJsonResponse.php create mode 100644 Tests/StreamedJsonResponseTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index ae0902547..1aaf6ed0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ CHANGELOG 6.2 --- + * Add `StreamedJsonResponse` class for efficient JSON streaming * The HTTP cache store uses the `xxh128` algorithm * Deprecate calling `JsonResponse::setCallback()`, `Response::setExpires/setLastModified/setEtag()`, `MockArraySessionStorage/NativeSessionStorage::setMetadataBag()`, `NativeSessionStorage::setSaveHandler()` without arguments * Add request matchers under the `Symfony\Component\HttpFoundation\RequestMatcher` namespace diff --git a/StreamedJsonResponse.php b/StreamedJsonResponse.php new file mode 100644 index 000000000..445bd77d7 --- /dev/null +++ b/StreamedJsonResponse.php @@ -0,0 +1,139 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * StreamedJsonResponse represents a streamed HTTP response for JSON. + * + * A StreamedJsonResponse uses a structure and generics to create an + * efficient resource-saving JSON response. + * + * It is recommended to use flush() function after a specific number of items to directly stream the data. + * + * @see flush() + * + * @author Alexander Schranz + * + * Example usage: + * + * function loadArticles(): \Generator + * // some streamed loading + * yield ['title' => 'Article 1']; + * yield ['title' => 'Article 2']; + * yield ['title' => 'Article 3']; + * // recommended to use flush() after every specific number of items + * }), + * + * $response = new StreamedJsonResponse( + * // json structure with generators in which will be streamed + * [ + * '_embedded' => [ + * 'articles' => loadArticles(), // any generator which you want to stream as list of data + * ], + * ], + * ); + */ +class StreamedJsonResponse extends StreamedResponse +{ + private const PLACEHOLDER = '__symfony_json__'; + + /** + * @param mixed[] $data JSON Data containing PHP generators which will be streamed as list of data + * @param int $status The HTTP status code (200 "OK" by default) + * @param array $headers An array of HTTP headers + * @param int $encodingOptions Flags for the json_encode() function + */ + public function __construct( + private readonly array $data, + int $status = 200, + array $headers = [], + private int $encodingOptions = JsonResponse::DEFAULT_ENCODING_OPTIONS, + ) { + parent::__construct($this->stream(...), $status, $headers); + + if (!$this->headers->get('Content-Type')) { + $this->headers->set('Content-Type', 'application/json'); + } + } + + private function stream(): void + { + $generators = []; + $structure = $this->data; + + array_walk_recursive($structure, function (&$item, $key) use (&$generators) { + if (self::PLACEHOLDER === $key) { + // if the placeholder is already in the structure it should be replaced with a new one that explode + // works like expected for the structure + $generators[] = $key; + } + + // generators should be used but for better DX all kind of Traversable and objects are supported + if (\is_object($item)) { + $generators[] = $item; + $item = self::PLACEHOLDER; + } elseif (self::PLACEHOLDER === $item) { + // if the placeholder is already in the structure it should be replaced with a new one that explode + // works like expected for the structure + $generators[] = $item; + } + }); + + $jsonEncodingOptions = \JSON_THROW_ON_ERROR | $this->encodingOptions; + $keyEncodingOptions = $jsonEncodingOptions & ~\JSON_NUMERIC_CHECK; + + $jsonParts = explode('"'.self::PLACEHOLDER.'"', json_encode($structure, $jsonEncodingOptions)); + + foreach ($generators as $index => $generator) { + // send first and between parts of the structure + echo $jsonParts[$index]; + + if ($generator instanceof \JsonSerializable || !$generator instanceof \Traversable) { + // the placeholders, JsonSerializable and none traversable items in the structure are rendered here + echo json_encode($generator, $jsonEncodingOptions); + + continue; + } + + $isFirstItem = true; + $startTag = '['; + + foreach ($generator as $key => $item) { + if ($isFirstItem) { + $isFirstItem = false; + // depending on the first elements key the generator is detected as a list or map + // we can not check for a whole list or map because that would hurt the performance + // of the streamed response which is the main goal of this response class + if (0 !== $key) { + $startTag = '{'; + } + + echo $startTag; + } else { + // if not first element of the generic, a separator is required between the elements + echo ','; + } + + if ('{' === $startTag) { + echo json_encode((string) $key, $keyEncodingOptions).':'; + } + + echo json_encode($item, $jsonEncodingOptions); + } + + echo '[' === $startTag ? ']' : '}'; + } + + // send last part of the structure + echo $jsonParts[array_key_last($jsonParts)]; + } +} diff --git a/Tests/StreamedJsonResponseTest.php b/Tests/StreamedJsonResponseTest.php new file mode 100644 index 000000000..e142672fd --- /dev/null +++ b/Tests/StreamedJsonResponseTest.php @@ -0,0 +1,241 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\StreamedJsonResponse; + +class StreamedJsonResponseTest extends TestCase +{ + public function testResponseSimpleList() + { + $content = $this->createSendResponse( + [ + '_embedded' => [ + 'articles' => $this->generatorSimple('Article'), + 'news' => $this->generatorSimple('News'), + ], + ], + ); + + $this->assertSame('{"_embedded":{"articles":["Article 1","Article 2","Article 3"],"news":["News 1","News 2","News 3"]}}', $content); + } + + public function testResponseObjectsList() + { + $content = $this->createSendResponse( + [ + '_embedded' => [ + 'articles' => $this->generatorArray('Article'), + ], + ], + ); + + $this->assertSame('{"_embedded":{"articles":[{"title":"Article 1"},{"title":"Article 2"},{"title":"Article 3"}]}}', $content); + } + + public function testResponseWithoutGenerator() + { + // while it is not the intended usage, all kind of iterables should be supported for good DX + $content = $this->createSendResponse( + [ + '_embedded' => [ + 'articles' => ['Article 1', 'Article 2', 'Article 3'], + ], + ], + ); + + $this->assertSame('{"_embedded":{"articles":["Article 1","Article 2","Article 3"]}}', $content); + } + + public function testResponseWithPlaceholder() + { + // the placeholder must not conflict with generator injection + $content = $this->createSendResponse( + [ + '_embedded' => [ + 'articles' => $this->generatorArray('Article'), + 'placeholder' => '__symfony_json__', + 'news' => $this->generatorSimple('News'), + ], + 'placeholder' => '__symfony_json__', + ], + ); + + $this->assertSame('{"_embedded":{"articles":[{"title":"Article 1"},{"title":"Article 2"},{"title":"Article 3"}],"placeholder":"__symfony_json__","news":["News 1","News 2","News 3"]},"placeholder":"__symfony_json__"}', $content); + } + + public function testResponseWithMixedKeyType() + { + $content = $this->createSendResponse( + [ + '_embedded' => [ + 'list' => (function (): \Generator { + yield 0 => 'test'; + yield 'key' => 'value'; + })(), + 'map' => (function (): \Generator { + yield 'key' => 'value'; + yield 0 => 'test'; + })(), + 'integer' => (function (): \Generator { + yield 1 => 'one'; + yield 3 => 'three'; + })(), + ], + ] + ); + + $this->assertSame('{"_embedded":{"list":["test","value"],"map":{"key":"value","0":"test"},"integer":{"1":"one","3":"three"}}}', $content); + } + + public function testResponseOtherTraversable() + { + $arrayObject = new \ArrayObject(['__symfony_json__' => '__symfony_json__']); + + $iteratorAggregate = new class() implements \IteratorAggregate { + public function getIterator(): \Traversable + { + return new \ArrayIterator(['__symfony_json__']); + } + }; + + $jsonSerializable = new class() implements \IteratorAggregate, \JsonSerializable { + public function getIterator(): \Traversable + { + return new \ArrayIterator(['This should be ignored']); + } + + public function jsonSerialize(): mixed + { + return ['__symfony_json__' => '__symfony_json__']; + } + }; + + // while Generators should be used for performance reasons, the object should also work with any Traversable + // to make things easier for a developer + $content = $this->createSendResponse( + [ + 'arrayObject' => $arrayObject, + 'iteratorAggregate' => $iteratorAggregate, + 'jsonSerializable' => $jsonSerializable, + // add a Generator to make sure it still work in combination with other Traversable objects + 'articles' => $this->generatorArray('Article'), + ], + ); + + $this->assertSame('{"arrayObject":{"__symfony_json__":"__symfony_json__"},"iteratorAggregate":["__symfony_json__"],"jsonSerializable":{"__symfony_json__":"__symfony_json__"},"articles":[{"title":"Article 1"},{"title":"Article 2"},{"title":"Article 3"}]}', $content); + } + + public function testPlaceholderAsKeyAndValueInStructure() + { + $content = $this->createSendResponse( + [ + '__symfony_json__' => '__symfony_json__', + 'articles' => $this->generatorArray('Article'), + ], + ); + + $this->assertSame('{"__symfony_json__":"__symfony_json__","articles":[{"title":"Article 1"},{"title":"Article 2"},{"title":"Article 3"}]}', $content); + } + + public function testResponseStatusCode() + { + $response = new StreamedJsonResponse([], 201); + + $this->assertSame(201, $response->getStatusCode()); + } + + public function testPlaceholderAsObjectStructure() + { + $object = new class() { + public $__symfony_json__ = 'foo'; + public $bar = '__symfony_json__'; + }; + + $content = $this->createSendResponse( + [ + 'object' => $object, + // add a Generator to make sure it still work in combination with other object holding placeholders + 'articles' => $this->generatorArray('Article'), + ], + ); + + $this->assertSame('{"object":{"__symfony_json__":"foo","bar":"__symfony_json__"},"articles":[{"title":"Article 1"},{"title":"Article 2"},{"title":"Article 3"}]}', $content); + } + + public function testResponseHeaders() + { + $response = new StreamedJsonResponse([], 200, ['X-Test' => 'Test']); + + $this->assertSame('Test', $response->headers->get('X-Test')); + } + + public function testCustomContentType() + { + $response = new StreamedJsonResponse([], 200, ['Content-Type' => 'application/json+stream']); + + $this->assertSame('application/json+stream', $response->headers->get('Content-Type')); + } + + public function testEncodingOptions() + { + $response = new StreamedJsonResponse([ + '_embedded' => [ + 'count' => '2', // options are applied to the initial json encode + 'values' => (function (): \Generator { + yield 'with/unescaped/slash' => 'With/a/slash'; // options are applied to key and values + yield '3' => '3'; // numeric check for value, but not for the key + })(), + ], + ], encodingOptions: \JSON_UNESCAPED_SLASHES | \JSON_NUMERIC_CHECK); + + ob_start(); + $response->send(); + $content = ob_get_clean(); + + $this->assertSame('{"_embedded":{"count":2,"values":{"with/unescaped/slash":"With/a/slash","3":3}}}', $content); + } + + /** + * @param mixed[] $data + */ + private function createSendResponse(array $data): string + { + $response = new StreamedJsonResponse($data); + + ob_start(); + $response->send(); + + return ob_get_clean(); + } + + /** + * @return \Generator + */ + private function generatorSimple(string $test): \Generator + { + yield $test.' 1'; + yield $test.' 2'; + yield $test.' 3'; + } + + /** + * @return \Generator + */ + private function generatorArray(string $test): \Generator + { + yield ['title' => $test.' 1']; + yield ['title' => $test.' 2']; + yield ['title' => $test.' 3']; + } +} From 170e1734a1d4762e36dcf9dc0056d15b8cc56b83 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Thu, 29 Dec 2022 16:19:30 +0100 Subject: [PATCH 005/111] [HttpFoundation] ParameterBag::getEnum() --- CHANGELOG.md | 1 + InputBag.php | 19 +++++++++++++++++++ ParameterBag.php | 25 +++++++++++++++++++++++++ Tests/InputBagTest.php | 17 +++++++++++++++++ Tests/ParameterBagTest.php | 35 +++++++++++++++++++++++++++++++++++ 5 files changed, 97 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1aaf6ed0f..208f466d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 6.3 --- + * Add `ParameterBag::getEnum()` * Create migration for session table when pdo handler is used 6.2 diff --git a/InputBag.php b/InputBag.php index 877ac60f3..446b82132 100644 --- a/InputBag.php +++ b/InputBag.php @@ -73,6 +73,25 @@ public function set(string $key, mixed $value) $this->parameters[$key] = $value; } + /** + * Returns the parameter value converted to an enum. + * + * @template T of \BackedEnum + * + * @param class-string $class + * @param ?T $default + * + * @return ?T + */ + public function getEnum(string $key, string $class, \BackedEnum $default = null): ?\BackedEnum + { + try { + return parent::getEnum($key, $class, $default); + } catch (\UnexpectedValueException $e) { + throw new BadRequestException($e->getMessage(), $e->getCode(), $e); + } + } + public function filter(string $key, mixed $default = null, int $filter = \FILTER_DEFAULT, mixed $options = []): mixed { $value = $this->has($key) ? $this->all()[$key] : $default; diff --git a/ParameterBag.php b/ParameterBag.php index 72c8f0949..9df9604e6 100644 --- a/ParameterBag.php +++ b/ParameterBag.php @@ -141,6 +141,31 @@ public function getBoolean(string $key, bool $default = false): bool return $this->filter($key, $default, \FILTER_VALIDATE_BOOL); } + /** + * Returns the parameter value converted to an enum. + * + * @template T of \BackedEnum + * + * @param class-string $class + * @param ?T $default + * + * @return ?T + */ + public function getEnum(string $key, string $class, \BackedEnum $default = null): ?\BackedEnum + { + $value = $this->get($key); + + if (null === $value) { + return $default; + } + + try { + return $class::from($value); + } catch (\ValueError|\TypeError $e) { + throw new \UnexpectedValueException(sprintf('Parameter "%s" cannot be converted to enum: %s.', $key, $e->getMessage()), $e->getCode(), $e); + } + } + /** * Filter key. * diff --git a/Tests/InputBagTest.php b/Tests/InputBagTest.php index 696318e91..ccb4779ef 100644 --- a/Tests/InputBagTest.php +++ b/Tests/InputBagTest.php @@ -106,4 +106,21 @@ public function testFilterArrayWithoutArrayFlag() $bag = new InputBag(['foo' => ['bar', 'baz']]); $bag->filter('foo', \FILTER_VALIDATE_INT); } + + public function testGetEnum() + { + $bag = new InputBag(['valid-value' => 1]); + + $this->assertSame(Foo::Bar, $bag->getEnum('valid-value', Foo::class)); + } + + public function testGetEnumThrowsExceptionWithInvalidValue() + { + $bag = new InputBag(['invalid-value' => 2]); + + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('Parameter "invalid-value" cannot be converted to enum: 2 is not a valid backing value for enum "Symfony\Component\HttpFoundation\Tests\Foo".'); + + $this->assertNull($bag->getEnum('invalid-value', Foo::class)); + } } diff --git a/Tests/ParameterBagTest.php b/Tests/ParameterBagTest.php index 1b60fb241..43aaade7e 100644 --- a/Tests/ParameterBagTest.php +++ b/Tests/ParameterBagTest.php @@ -226,4 +226,39 @@ public function testGetBoolean() $this->assertFalse($bag->getBoolean('string_false'), '->getBoolean() gets the string false as boolean false'); $this->assertFalse($bag->getBoolean('unknown'), '->getBoolean() returns false if a parameter is not defined'); } + + public function testGetEnum() + { + $bag = new ParameterBag(['valid-value' => 1]); + + $this->assertSame(Foo::Bar, $bag->getEnum('valid-value', Foo::class)); + + $this->assertNull($bag->getEnum('invalid-key', Foo::class)); + $this->assertSame(Foo::Bar, $bag->getEnum('invalid-key', Foo::class, Foo::Bar)); + } + + public function testGetEnumThrowsExceptionWithNotBackingValue() + { + $bag = new ParameterBag(['invalid-value' => 2]); + + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage('Parameter "invalid-value" cannot be converted to enum: 2 is not a valid backing value for enum "Symfony\Component\HttpFoundation\Tests\Foo".'); + + $this->assertNull($bag->getEnum('invalid-value', Foo::class)); + } + + public function testGetEnumThrowsExceptionWithInvalidValueType() + { + $bag = new ParameterBag(['invalid-value' => ['foo']]); + + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage('Parameter "invalid-value" cannot be converted to enum: Symfony\Component\HttpFoundation\Tests\Foo::from(): Argument #1 ($value) must be of type int, array given.'); + + $this->assertNull($bag->getEnum('invalid-value', Foo::class)); + } +} + +enum Foo: int +{ + case Bar = 1; } From 9164469b555f0923bd6639d0cd6239c2e392a274 Mon Sep 17 00:00:00 2001 From: Sergey Rabochiy Date: Sat, 7 Jan 2023 11:40:16 +0700 Subject: [PATCH 006/111] Fix ParameterBagTest message with PHP 8.2 --- Tests/Fixtures/FooEnum.php | 17 +++++++++++++++++ Tests/InputBagTest.php | 11 ++++++++--- Tests/ParameterBagTest.php | 24 ++++++++++++------------ 3 files changed, 37 insertions(+), 15 deletions(-) create mode 100644 Tests/Fixtures/FooEnum.php diff --git a/Tests/Fixtures/FooEnum.php b/Tests/Fixtures/FooEnum.php new file mode 100644 index 000000000..a6f56fba1 --- /dev/null +++ b/Tests/Fixtures/FooEnum.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Tests\Fixtures; + +enum FooEnum: int +{ + case Bar = 1; +} diff --git a/Tests/InputBagTest.php b/Tests/InputBagTest.php index ccb4779ef..0d0d959a6 100644 --- a/Tests/InputBagTest.php +++ b/Tests/InputBagTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Exception\BadRequestException; use Symfony\Component\HttpFoundation\InputBag; +use Symfony\Component\HttpFoundation\Tests\Fixtures\FooEnum; class InputBagTest extends TestCase { @@ -111,7 +112,7 @@ public function testGetEnum() { $bag = new InputBag(['valid-value' => 1]); - $this->assertSame(Foo::Bar, $bag->getEnum('valid-value', Foo::class)); + $this->assertSame(FooEnum::Bar, $bag->getEnum('valid-value', FooEnum::class)); } public function testGetEnumThrowsExceptionWithInvalidValue() @@ -119,8 +120,12 @@ public function testGetEnumThrowsExceptionWithInvalidValue() $bag = new InputBag(['invalid-value' => 2]); $this->expectException(BadRequestException::class); - $this->expectExceptionMessage('Parameter "invalid-value" cannot be converted to enum: 2 is not a valid backing value for enum "Symfony\Component\HttpFoundation\Tests\Foo".'); + if (\PHP_VERSION_ID >= 80200) { + $this->expectExceptionMessage('Parameter "invalid-value" cannot be converted to enum: 2 is not a valid backing value for enum Symfony\Component\HttpFoundation\Tests\Fixtures\FooEnum.'); + } else { + $this->expectExceptionMessage('Parameter "invalid-value" cannot be converted to enum: 2 is not a valid backing value for enum "Symfony\Component\HttpFoundation\Tests\Fixtures\FooEnum".'); + } - $this->assertNull($bag->getEnum('invalid-value', Foo::class)); + $this->assertNull($bag->getEnum('invalid-value', FooEnum::class)); } } diff --git a/Tests/ParameterBagTest.php b/Tests/ParameterBagTest.php index 43aaade7e..7c9a228f7 100644 --- a/Tests/ParameterBagTest.php +++ b/Tests/ParameterBagTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Exception\BadRequestException; use Symfony\Component\HttpFoundation\ParameterBag; +use Symfony\Component\HttpFoundation\Tests\Fixtures\FooEnum; class ParameterBagTest extends TestCase { @@ -231,10 +232,10 @@ public function testGetEnum() { $bag = new ParameterBag(['valid-value' => 1]); - $this->assertSame(Foo::Bar, $bag->getEnum('valid-value', Foo::class)); + $this->assertSame(FooEnum::Bar, $bag->getEnum('valid-value', FooEnum::class)); - $this->assertNull($bag->getEnum('invalid-key', Foo::class)); - $this->assertSame(Foo::Bar, $bag->getEnum('invalid-key', Foo::class, Foo::Bar)); + $this->assertNull($bag->getEnum('invalid-key', FooEnum::class)); + $this->assertSame(FooEnum::Bar, $bag->getEnum('invalid-key', FooEnum::class, FooEnum::Bar)); } public function testGetEnumThrowsExceptionWithNotBackingValue() @@ -242,9 +243,13 @@ public function testGetEnumThrowsExceptionWithNotBackingValue() $bag = new ParameterBag(['invalid-value' => 2]); $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessage('Parameter "invalid-value" cannot be converted to enum: 2 is not a valid backing value for enum "Symfony\Component\HttpFoundation\Tests\Foo".'); + if (\PHP_VERSION_ID >= 80200) { + $this->expectExceptionMessage('Parameter "invalid-value" cannot be converted to enum: 2 is not a valid backing value for enum Symfony\Component\HttpFoundation\Tests\Fixtures\FooEnum.'); + } else { + $this->expectExceptionMessage('Parameter "invalid-value" cannot be converted to enum: 2 is not a valid backing value for enum "Symfony\Component\HttpFoundation\Tests\Fixtures\FooEnum".'); + } - $this->assertNull($bag->getEnum('invalid-value', Foo::class)); + $this->assertNull($bag->getEnum('invalid-value', FooEnum::class)); } public function testGetEnumThrowsExceptionWithInvalidValueType() @@ -252,13 +257,8 @@ public function testGetEnumThrowsExceptionWithInvalidValueType() $bag = new ParameterBag(['invalid-value' => ['foo']]); $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessage('Parameter "invalid-value" cannot be converted to enum: Symfony\Component\HttpFoundation\Tests\Foo::from(): Argument #1 ($value) must be of type int, array given.'); + $this->expectExceptionMessage('Parameter "invalid-value" cannot be converted to enum: Symfony\Component\HttpFoundation\Tests\Fixtures\FooEnum::from(): Argument #1 ($value) must be of type int, array given.'); - $this->assertNull($bag->getEnum('invalid-value', Foo::class)); + $this->assertNull($bag->getEnum('invalid-value', FooEnum::class)); } } - -enum Foo: int -{ - case Bar = 1; -} From cfe92c3dac0806f0690e1993d5d47ffe2f5fd0c0 Mon Sep 17 00:00:00 2001 From: tigitz Date: Sun, 1 Jan 2023 19:45:34 +0100 Subject: [PATCH 007/111] Leverage arrow function syntax for closure --- AcceptHeader.php | 4 +--- FileBag.php | 2 +- Request.php | 4 +--- RequestMatcher.php | 4 +--- RequestMatcher/IpsRequestMatcher.php | 4 +--- RequestMatcher/MethodRequestMatcher.php | 4 +--- RequestMatcher/SchemeRequestMatcher.php | 4 +--- Test/Constraint/ResponseCookieValueSame.php | 4 +--- Test/Constraint/ResponseHasCookie.php | 4 +--- Tests/InputBagTest.php | 4 +--- Tests/ParameterBagTest.php | 4 +--- Tests/RequestMatcher/AttributesRequestMatcherTest.php | 4 +--- Tests/RequestMatcherTest.php | 4 +--- Tests/RequestTest.php | 4 +--- Tests/ResponseFunctionalTest.php | 2 +- .../Storage/Handler/AbstractSessionHandlerTest.php | 2 +- .../Storage/Handler/Fixtures/invalid_regenerate.php | 2 +- Tests/Session/Storage/Handler/Fixtures/regenerate.php | 2 +- Tests/Session/Storage/Handler/Fixtures/storage.php | 2 +- .../Session/Storage/Handler/Fixtures/with_samesite.php | 2 +- .../Handler/Fixtures/with_samesite_and_migration.php | 2 +- .../Storage/Handler/MemcachedSessionHandlerTest.php | 2 +- .../Session/Storage/Handler/PdoSessionHandlerTest.php | 10 ++++------ .../Storage/Handler/SessionHandlerFactoryTest.php | 2 +- 24 files changed, 27 insertions(+), 55 deletions(-) diff --git a/AcceptHeader.php b/AcceptHeader.php index 180e9604c..5edf5f5f1 100644 --- a/AcceptHeader.php +++ b/AcceptHeader.php @@ -115,9 +115,7 @@ public function all(): array */ public function filter(string $pattern): self { - return new self(array_filter($this->items, function (AcceptHeaderItem $item) use ($pattern) { - return preg_match($pattern, $item->getValue()); - })); + return new self(array_filter($this->items, fn (AcceptHeaderItem $item) => preg_match($pattern, $item->getValue()))); } /** diff --git a/FileBag.php b/FileBag.php index 7ed39408f..3ccc45d1d 100644 --- a/FileBag.php +++ b/FileBag.php @@ -75,7 +75,7 @@ protected function convertFileInformation(array|UploadedFile $file): array|Uploa $file = new UploadedFile($file['tmp_name'], $file['name'], $file['type'], $file['error'], false); } } else { - $file = array_map(function ($v) { return $v instanceof UploadedFile || \is_array($v) ? $this->convertFileInformation($v) : $v; }, $file); + $file = array_map(fn ($v) => $v instanceof UploadedFile || \is_array($v) ? $this->convertFileInformation($v) : $v, $file); if (array_keys($keys) === $keys) { $file = array_filter($file); } diff --git a/Request.php b/Request.php index 3cf8a9954..56d74874c 100644 --- a/Request.php +++ b/Request.php @@ -605,9 +605,7 @@ public static function getTrustedHeaderSet(): int */ public static function setTrustedHosts(array $hostPatterns) { - self::$trustedHostPatterns = array_map(function ($hostPattern) { - return sprintf('{%s}i', $hostPattern); - }, $hostPatterns); + self::$trustedHostPatterns = array_map(fn ($hostPattern) => sprintf('{%s}i', $hostPattern), $hostPatterns); // we need to reset trusted hosts on trusted host patterns change self::$trustedHosts = []; } diff --git a/RequestMatcher.php b/RequestMatcher.php index c2addd36e..28cdd20c5 100644 --- a/RequestMatcher.php +++ b/RequestMatcher.php @@ -120,9 +120,7 @@ public function matchIps(string|array|null $ips) { $ips = null !== $ips ? (array) $ips : []; - $this->ips = array_reduce($ips, static function (array $ips, string $ip) { - return array_merge($ips, preg_split('/\s*,\s*/', $ip)); - }, []); + $this->ips = array_reduce($ips, static fn (array $ips, string $ip) => array_merge($ips, preg_split('/\s*,\s*/', $ip)), []); } /** diff --git a/RequestMatcher/IpsRequestMatcher.php b/RequestMatcher/IpsRequestMatcher.php index 2ddff038d..333612e2f 100644 --- a/RequestMatcher/IpsRequestMatcher.php +++ b/RequestMatcher/IpsRequestMatcher.php @@ -30,9 +30,7 @@ class IpsRequestMatcher implements RequestMatcherInterface */ public function __construct(array|string $ips) { - $this->ips = array_reduce((array) $ips, static function (array $ips, string $ip) { - return array_merge($ips, preg_split('/\s*,\s*/', $ip)); - }, []); + $this->ips = array_reduce((array) $ips, static fn (array $ips, string $ip) => array_merge($ips, preg_split('/\s*,\s*/', $ip)), []); } public function matches(Request $request): bool diff --git a/RequestMatcher/MethodRequestMatcher.php b/RequestMatcher/MethodRequestMatcher.php index c7a915980..b37f6e3c8 100644 --- a/RequestMatcher/MethodRequestMatcher.php +++ b/RequestMatcher/MethodRequestMatcher.php @@ -32,9 +32,7 @@ class MethodRequestMatcher implements RequestMatcherInterface */ public function __construct(array|string $methods) { - $this->methods = array_reduce(array_map('strtoupper', (array) $methods), static function (array $methods, string $method) { - return array_merge($methods, preg_split('/\s*,\s*/', $method)); - }, []); + $this->methods = array_reduce(array_map('strtoupper', (array) $methods), static fn (array $methods, string $method) => array_merge($methods, preg_split('/\s*,\s*/', $method)), []); } public function matches(Request $request): bool diff --git a/RequestMatcher/SchemeRequestMatcher.php b/RequestMatcher/SchemeRequestMatcher.php index 4f5eabc2c..9c9cd58b9 100644 --- a/RequestMatcher/SchemeRequestMatcher.php +++ b/RequestMatcher/SchemeRequestMatcher.php @@ -32,9 +32,7 @@ class SchemeRequestMatcher implements RequestMatcherInterface */ public function __construct(array|string $schemes) { - $this->schemes = array_reduce(array_map('strtolower', (array) $schemes), static function (array $schemes, string $scheme) { - return array_merge($schemes, preg_split('/\s*,\s*/', $scheme)); - }, []); + $this->schemes = array_reduce(array_map('strtolower', (array) $schemes), static fn (array $schemes, string $scheme) => array_merge($schemes, preg_split('/\s*,\s*/', $scheme)), []); } public function matches(Request $request): bool diff --git a/Test/Constraint/ResponseCookieValueSame.php b/Test/Constraint/ResponseCookieValueSame.php index b3d375e4c..417efc77a 100644 --- a/Test/Constraint/ResponseCookieValueSame.php +++ b/Test/Constraint/ResponseCookieValueSame.php @@ -69,9 +69,7 @@ protected function getCookie(Response $response): ?Cookie { $cookies = $response->headers->getCookies(); - $filteredCookies = array_filter($cookies, function (Cookie $cookie) { - return $cookie->getName() === $this->name && $cookie->getPath() === $this->path && $cookie->getDomain() === $this->domain; - }); + $filteredCookies = array_filter($cookies, fn (Cookie $cookie) => $cookie->getName() === $this->name && $cookie->getPath() === $this->path && $cookie->getDomain() === $this->domain); return reset($filteredCookies) ?: null; } diff --git a/Test/Constraint/ResponseHasCookie.php b/Test/Constraint/ResponseHasCookie.php index 9b15aeae8..73393d386 100644 --- a/Test/Constraint/ResponseHasCookie.php +++ b/Test/Constraint/ResponseHasCookie.php @@ -61,9 +61,7 @@ private function getCookie(Response $response): ?Cookie { $cookies = $response->headers->getCookies(); - $filteredCookies = array_filter($cookies, function (Cookie $cookie) { - return $cookie->getName() === $this->name && $cookie->getPath() === $this->path && $cookie->getDomain() === $this->domain; - }); + $filteredCookies = array_filter($cookies, fn (Cookie $cookie) => $cookie->getName() === $this->name && $cookie->getPath() === $this->path && $cookie->getDomain() === $this->domain); return reset($filteredCookies) ?: null; } diff --git a/Tests/InputBagTest.php b/Tests/InputBagTest.php index 696318e91..8d758916c 100644 --- a/Tests/InputBagTest.php +++ b/Tests/InputBagTest.php @@ -64,9 +64,7 @@ public function testFilterCallback() public function testFilterClosure() { $bag = new InputBag(['foo' => 'bar']); - $result = $bag->filter('foo', null, \FILTER_CALLBACK, ['options' => function ($value) { - return strtoupper($value); - }]); + $result = $bag->filter('foo', null, \FILTER_CALLBACK, ['options' => strtoupper(...)]); $this->assertSame('BAR', $result); } diff --git a/Tests/ParameterBagTest.php b/Tests/ParameterBagTest.php index 1b60fb241..7a737e2f2 100644 --- a/Tests/ParameterBagTest.php +++ b/Tests/ParameterBagTest.php @@ -188,9 +188,7 @@ public function testFilterCallback() public function testFilterClosure() { $bag = new ParameterBag(['foo' => 'bar']); - $result = $bag->filter('foo', null, \FILTER_CALLBACK, ['options' => function ($value) { - return strtoupper($value); - }]); + $result = $bag->filter('foo', null, \FILTER_CALLBACK, ['options' => strtoupper(...)]); $this->assertSame('BAR', $result); } diff --git a/Tests/RequestMatcher/AttributesRequestMatcherTest.php b/Tests/RequestMatcher/AttributesRequestMatcherTest.php index 9ca887652..ce8cc7f20 100644 --- a/Tests/RequestMatcher/AttributesRequestMatcherTest.php +++ b/Tests/RequestMatcher/AttributesRequestMatcherTest.php @@ -26,9 +26,7 @@ public function test(string $key, string $regexp, bool $expected) $matcher = new AttributesRequestMatcher([$key => $regexp]); $request = Request::create('/admin/foo'); $request->attributes->set('foo', 'foo_bar'); - $request->attributes->set('_controller', function () { - return new Response('foo'); - }); + $request->attributes->set('_controller', fn () => new Response('foo')); $this->assertSame($expected, $matcher->matches($request)); } diff --git a/Tests/RequestMatcherTest.php b/Tests/RequestMatcherTest.php index 0419d3693..a51df6234 100644 --- a/Tests/RequestMatcherTest.php +++ b/Tests/RequestMatcherTest.php @@ -173,9 +173,7 @@ public function testAttributesWithClosure() $matcher = new RequestMatcher(); $request = Request::create('/admin/foo'); - $request->attributes->set('_controller', function () { - return new Response('foo'); - }); + $request->attributes->set('_controller', fn () => new Response('foo')); $matcher->matchAttribute('_controller', 'babar'); $this->assertFalse($matcher->matches($request)); diff --git a/Tests/RequestTest.php b/Tests/RequestTest.php index 23acd2e05..fda638305 100644 --- a/Tests/RequestTest.php +++ b/Tests/RequestTest.php @@ -2139,9 +2139,7 @@ public function testSetTrustedHostsDoesNotBreakOnSpecialCharacters() public function testFactory() { - Request::setFactory(function (array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null) { - return new NewRequest(); - }); + Request::setFactory(fn (array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null) => new NewRequest()); $this->assertEquals('foo', Request::create('/')->getFoo()); diff --git a/Tests/ResponseFunctionalTest.php b/Tests/ResponseFunctionalTest.php index aca283af0..dc3e5a3ac 100644 --- a/Tests/ResponseFunctionalTest.php +++ b/Tests/ResponseFunctionalTest.php @@ -44,7 +44,7 @@ public static function tearDownAfterClass(): void public function testCookie($fixture) { $result = file_get_contents(sprintf('http://localhost:8054/%s.php', $fixture)); - $result = preg_replace_callback('/expires=[^;]++/', function ($m) { return str_replace('-', ' ', $m[0]); }, $result); + $result = preg_replace_callback('/expires=[^;]++/', fn ($m) => str_replace('-', ' ', $m[0]), $result); $this->assertStringMatchesFormatFile(__DIR__.sprintf('/Fixtures/response-functional/%s.expected', $fixture), $result); } diff --git a/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php b/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php index aca2bfd88..ae470da5d 100644 --- a/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php @@ -46,7 +46,7 @@ public function testSession($fixture) $context = ['http' => ['header' => "Cookie: sid=123abc\r\n"]]; $context = stream_context_create($context); $result = file_get_contents(sprintf('http://localhost:8053/%s.php', $fixture), false, $context); - $result = preg_replace_callback('/expires=[^;]++/', function ($m) { return str_replace('-', ' ', $m[0]); }, $result); + $result = preg_replace_callback('/expires=[^;]++/', fn ($m) => str_replace('-', ' ', $m[0]), $result); $this->assertStringEqualsFile(__DIR__.sprintf('/Fixtures/%s.expected', $fixture), $result); } diff --git a/Tests/Session/Storage/Handler/Fixtures/invalid_regenerate.php b/Tests/Session/Storage/Handler/Fixtures/invalid_regenerate.php index 2798442a9..d7ec890d9 100644 --- a/Tests/Session/Storage/Handler/Fixtures/invalid_regenerate.php +++ b/Tests/Session/Storage/Handler/Fixtures/invalid_regenerate.php @@ -17,4 +17,4 @@ echo empty($_SESSION) ? '$_SESSION is empty' : '$_SESSION is not empty'; echo "\n"; -ob_start(function ($buffer) { return preg_replace('~_sf2_meta.*$~m', '', str_replace(session_id(), 'random_session_id', $buffer)); }); +ob_start(fn ($buffer) => preg_replace('~_sf2_meta.*$~m', '', str_replace(session_id(), 'random_session_id', $buffer))); diff --git a/Tests/Session/Storage/Handler/Fixtures/regenerate.php b/Tests/Session/Storage/Handler/Fixtures/regenerate.php index a0f635c87..b85849595 100644 --- a/Tests/Session/Storage/Handler/Fixtures/regenerate.php +++ b/Tests/Session/Storage/Handler/Fixtures/regenerate.php @@ -7,4 +7,4 @@ session_regenerate_id(true); -ob_start(function ($buffer) { return str_replace(session_id(), 'random_session_id', $buffer); }); +ob_start(fn ($buffer) => str_replace(session_id(), 'random_session_id', $buffer)); diff --git a/Tests/Session/Storage/Handler/Fixtures/storage.php b/Tests/Session/Storage/Handler/Fixtures/storage.php index 96dca3c2c..a86c82056 100644 --- a/Tests/Session/Storage/Handler/Fixtures/storage.php +++ b/Tests/Session/Storage/Handler/Fixtures/storage.php @@ -21,4 +21,4 @@ echo empty($_SESSION) ? '$_SESSION is empty' : '$_SESSION is not empty'; -ob_start(function ($buffer) { return str_replace(session_id(), 'random_session_id', $buffer); }); +ob_start(fn ($buffer) => str_replace(session_id(), 'random_session_id', $buffer)); diff --git a/Tests/Session/Storage/Handler/Fixtures/with_samesite.php b/Tests/Session/Storage/Handler/Fixtures/with_samesite.php index fc2c41828..a005362ce 100644 --- a/Tests/Session/Storage/Handler/Fixtures/with_samesite.php +++ b/Tests/Session/Storage/Handler/Fixtures/with_samesite.php @@ -10,4 +10,4 @@ $_SESSION = ['foo' => 'bar']; -ob_start(function ($buffer) { return str_replace(session_id(), 'random_session_id', $buffer); }); +ob_start(fn ($buffer) => str_replace(session_id(), 'random_session_id', $buffer)); diff --git a/Tests/Session/Storage/Handler/Fixtures/with_samesite_and_migration.php b/Tests/Session/Storage/Handler/Fixtures/with_samesite_and_migration.php index a28b6fedf..13c951ee3 100644 --- a/Tests/Session/Storage/Handler/Fixtures/with_samesite_and_migration.php +++ b/Tests/Session/Storage/Handler/Fixtures/with_samesite_and_migration.php @@ -12,4 +12,4 @@ $storage->regenerate(true); -ob_start(function ($buffer) { return preg_replace('~_sf2_meta.*$~m', '', str_replace(session_id(), 'random_session_id', $buffer)); }); +ob_start(fn ($buffer) => preg_replace('~_sf2_meta.*$~m', '', str_replace(session_id(), 'random_session_id', $buffer))); diff --git a/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php b/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php index 3e0e78441..8bb307f44 100644 --- a/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php @@ -40,7 +40,7 @@ protected function setUp(): void } $r = new \ReflectionClass(\Memcached::class); - $methodsToMock = array_map(function ($m) { return $m->name; }, $r->getMethods(\ReflectionMethod::IS_PUBLIC)); + $methodsToMock = array_map(fn ($m) => $m->name, $r->getMethods(\ReflectionMethod::IS_PUBLIC)); $methodsToMock = array_diff($methodsToMock, ['getDelayed', 'getDelayedByKey']); $this->memcached = $this->getMockBuilder(\Memcached::class) diff --git a/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php b/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php index 07a82d09a..18151df7d 100644 --- a/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php @@ -157,9 +157,7 @@ public function testReadLockedConvertsStreamToString() $selectStmt = $this->createMock(\PDOStatement::class); $insertStmt = $this->createMock(\PDOStatement::class); - $pdo->prepareResult = function ($statement) use ($selectStmt, $insertStmt) { - return str_starts_with($statement, 'INSERT') ? $insertStmt : $selectStmt; - }; + $pdo->prepareResult = fn ($statement) => str_starts_with($statement, 'INSERT') ? $insertStmt : $selectStmt; $content = 'foobar'; $stream = $this->createStream($content); @@ -333,7 +331,7 @@ public function testConfigureSchemaDifferentDatabase() $schema = new Schema(); $pdoSessionHandler = new PdoSessionHandler($this->getMemorySqlitePdo()); - $pdoSessionHandler->configureSchema($schema, fn() => false); + $pdoSessionHandler->configureSchema($schema, fn () => false); $this->assertFalse($schema->hasTable('sessions')); } @@ -342,7 +340,7 @@ public function testConfigureSchemaSameDatabase() $schema = new Schema(); $pdoSessionHandler = new PdoSessionHandler($this->getMemorySqlitePdo()); - $pdoSessionHandler->configureSchema($schema, fn() => true); + $pdoSessionHandler->configureSchema($schema, fn () => true); $this->assertTrue($schema->hasTable('sessions')); } @@ -352,7 +350,7 @@ public function testConfigureSchemaTableExistsPdo() $schema->createTable('sessions'); $pdoSessionHandler = new PdoSessionHandler($this->getMemorySqlitePdo()); - $pdoSessionHandler->configureSchema($schema, fn() => true); + $pdoSessionHandler->configureSchema($schema, fn () => true); $table = $schema->getTable('sessions'); $this->assertEmpty($table->getColumns(), 'The table was not overwritten'); } diff --git a/Tests/Session/Storage/Handler/SessionHandlerFactoryTest.php b/Tests/Session/Storage/Handler/SessionHandlerFactoryTest.php index 203c4b285..6b8b8f111 100644 --- a/Tests/Session/Storage/Handler/SessionHandlerFactoryTest.php +++ b/Tests/Session/Storage/Handler/SessionHandlerFactoryTest.php @@ -72,7 +72,7 @@ public function testCreateRedisHandlerFromDsn() $ttlProperty = $reflection->getProperty('ttl'); $this->assertSame(3600, $ttlProperty->getValue($handler)); - $handler = SessionHandlerFactory::createHandler('redis://localhost?prefix=foo&ttl=3600&ignored=bar', ['ttl' => function () { return 123; }]); + $handler = SessionHandlerFactory::createHandler('redis://localhost?prefix=foo&ttl=3600&ignored=bar', ['ttl' => fn () => 123]); $this->assertInstanceOf(\Closure::class, $reflection->getProperty('ttl')->getValue($handler)); } From 5bd0835d62ea8849b66f047cca57989c10efec5e Mon Sep 17 00:00:00 2001 From: mark burdett Date: Tue, 17 Jan 2023 13:39:30 -0800 Subject: [PATCH 008/111] Add #[\SensitiveParameter] to $sessionId --- Session/SessionUtils.php | 2 +- .../Storage/Handler/AbstractSessionHandler.php | 14 +++++++------- .../Handler/MarshallingSessionHandler.php | 10 +++++----- .../Handler/MemcachedSessionHandler.php | 8 ++++---- .../Handler/MigratingSessionHandler.php | 10 +++++----- .../Storage/Handler/MongoDbSessionHandler.php | 8 ++++---- Session/Storage/Handler/NullSessionHandler.php | 10 +++++----- Session/Storage/Handler/PdoSessionHandler.php | 18 +++++++++--------- .../Storage/Handler/RedisSessionHandler.php | 8 ++++---- .../Storage/Handler/StrictSessionHandler.php | 10 +++++----- Session/Storage/Proxy/SessionHandlerProxy.php | 10 +++++----- 11 files changed, 54 insertions(+), 54 deletions(-) diff --git a/Session/SessionUtils.php b/Session/SessionUtils.php index b5bce4a88..504c5848e 100644 --- a/Session/SessionUtils.php +++ b/Session/SessionUtils.php @@ -25,7 +25,7 @@ final class SessionUtils * Finds the session header amongst the headers that are to be sent, removes it, and returns * it so the caller can process it further. */ - public static function popSessionCookie(string $sessionName, string $sessionId): ?string + public static function popSessionCookie(string $sessionName, #[\SensitiveParameter] string $sessionId): ?string { $sessionCookie = null; $sessionCookiePrefix = sprintf(' %s=', urlencode($sessionName)); diff --git a/Session/Storage/Handler/AbstractSessionHandler.php b/Session/Storage/Handler/AbstractSessionHandler.php index 88e513c5d..288c24232 100644 --- a/Session/Storage/Handler/AbstractSessionHandler.php +++ b/Session/Storage/Handler/AbstractSessionHandler.php @@ -38,13 +38,13 @@ public function open(string $savePath, string $sessionName): bool return true; } - abstract protected function doRead(string $sessionId): string; + abstract protected function doRead(#[\SensitiveParameter] string $sessionId): string; - abstract protected function doWrite(string $sessionId, string $data): bool; + abstract protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool; - abstract protected function doDestroy(string $sessionId): bool; + abstract protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool; - public function validateId(string $sessionId): bool + public function validateId(#[\SensitiveParameter] string $sessionId): bool { $this->prefetchData = $this->read($sessionId); $this->prefetchId = $sessionId; @@ -52,7 +52,7 @@ public function validateId(string $sessionId): bool return '' !== $this->prefetchData; } - public function read(string $sessionId): string + public function read(#[\SensitiveParameter] string $sessionId): string { if (isset($this->prefetchId)) { $prefetchId = $this->prefetchId; @@ -72,7 +72,7 @@ public function read(string $sessionId): string return $data; } - public function write(string $sessionId, string $data): bool + public function write(#[\SensitiveParameter] string $sessionId, string $data): bool { // see https://github.com/igbinary/igbinary/issues/146 $this->igbinaryEmptyData ??= \function_exists('igbinary_serialize') ? igbinary_serialize([]) : ''; @@ -84,7 +84,7 @@ public function write(string $sessionId, string $data): bool return $this->doWrite($sessionId, $data); } - public function destroy(string $sessionId): bool + public function destroy(#[\SensitiveParameter] string $sessionId): bool { if (!headers_sent() && filter_var(\ini_get('session.use_cookies'), \FILTER_VALIDATE_BOOL)) { if (!isset($this->sessionName)) { diff --git a/Session/Storage/Handler/MarshallingSessionHandler.php b/Session/Storage/Handler/MarshallingSessionHandler.php index 9962fef3d..1567f5433 100644 --- a/Session/Storage/Handler/MarshallingSessionHandler.php +++ b/Session/Storage/Handler/MarshallingSessionHandler.php @@ -37,7 +37,7 @@ public function close(): bool return $this->handler->close(); } - public function destroy(string $sessionId): bool + public function destroy(#[\SensitiveParameter] string $sessionId): bool { return $this->handler->destroy($sessionId); } @@ -47,12 +47,12 @@ public function gc(int $maxlifetime): int|false return $this->handler->gc($maxlifetime); } - public function read(string $sessionId): string + public function read(#[\SensitiveParameter] string $sessionId): string { return $this->marshaller->unmarshall($this->handler->read($sessionId)); } - public function write(string $sessionId, string $data): bool + public function write(#[\SensitiveParameter] string $sessionId, string $data): bool { $failed = []; $marshalledData = $this->marshaller->marshall(['data' => $data], $failed); @@ -64,12 +64,12 @@ public function write(string $sessionId, string $data): bool return $this->handler->write($sessionId, $marshalledData['data']); } - public function validateId(string $sessionId): bool + public function validateId(#[\SensitiveParameter] string $sessionId): bool { return $this->handler->validateId($sessionId); } - public function updateTimestamp(string $sessionId, string $data): bool + public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool { return $this->handler->updateTimestamp($sessionId, $data); } diff --git a/Session/Storage/Handler/MemcachedSessionHandler.php b/Session/Storage/Handler/MemcachedSessionHandler.php index 2bc29b459..91a023ddb 100644 --- a/Session/Storage/Handler/MemcachedSessionHandler.php +++ b/Session/Storage/Handler/MemcachedSessionHandler.php @@ -59,19 +59,19 @@ public function close(): bool return $this->memcached->quit(); } - protected function doRead(string $sessionId): string + protected function doRead(#[\SensitiveParameter] string $sessionId): string { return $this->memcached->get($this->prefix.$sessionId) ?: ''; } - public function updateTimestamp(string $sessionId, string $data): bool + public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool { $this->memcached->touch($this->prefix.$sessionId, $this->getCompatibleTtl()); return true; } - protected function doWrite(string $sessionId, string $data): bool + protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool { return $this->memcached->set($this->prefix.$sessionId, $data, $this->getCompatibleTtl()); } @@ -89,7 +89,7 @@ private function getCompatibleTtl(): int return $ttl; } - protected function doDestroy(string $sessionId): bool + protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool { $result = $this->memcached->delete($this->prefix.$sessionId); diff --git a/Session/Storage/Handler/MigratingSessionHandler.php b/Session/Storage/Handler/MigratingSessionHandler.php index 46fbeb006..8ed6a7b3f 100644 --- a/Session/Storage/Handler/MigratingSessionHandler.php +++ b/Session/Storage/Handler/MigratingSessionHandler.php @@ -46,7 +46,7 @@ public function close(): bool return $result; } - public function destroy(string $sessionId): bool + public function destroy(#[\SensitiveParameter] string $sessionId): bool { $result = $this->currentHandler->destroy($sessionId); $this->writeOnlyHandler->destroy($sessionId); @@ -70,13 +70,13 @@ public function open(string $savePath, string $sessionName): bool return $result; } - public function read(string $sessionId): string + public function read(#[\SensitiveParameter] string $sessionId): string { // No reading from new handler until switch-over return $this->currentHandler->read($sessionId); } - public function write(string $sessionId, string $sessionData): bool + public function write(#[\SensitiveParameter] string $sessionId, string $sessionData): bool { $result = $this->currentHandler->write($sessionId, $sessionData); $this->writeOnlyHandler->write($sessionId, $sessionData); @@ -84,13 +84,13 @@ public function write(string $sessionId, string $sessionData): bool return $result; } - public function validateId(string $sessionId): bool + public function validateId(#[\SensitiveParameter] string $sessionId): bool { // No reading from new handler until switch-over return $this->currentHandler->validateId($sessionId); } - public function updateTimestamp(string $sessionId, string $sessionData): bool + public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $sessionData): bool { $result = $this->currentHandler->updateTimestamp($sessionId, $sessionData); $this->writeOnlyHandler->updateTimestamp($sessionId, $sessionData); diff --git a/Session/Storage/Handler/MongoDbSessionHandler.php b/Session/Storage/Handler/MongoDbSessionHandler.php index 63c609ae2..5ea5b4ae7 100644 --- a/Session/Storage/Handler/MongoDbSessionHandler.php +++ b/Session/Storage/Handler/MongoDbSessionHandler.php @@ -84,7 +84,7 @@ public function close(): bool return true; } - protected function doDestroy(string $sessionId): bool + protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool { $this->getCollection()->deleteOne([ $this->options['id_field'] => $sessionId, @@ -100,7 +100,7 @@ public function gc(int $maxlifetime): int|false ])->getDeletedCount(); } - protected function doWrite(string $sessionId, string $data): bool + protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool { $ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime'); $expiry = new UTCDateTime((time() + (int) $ttl) * 1000); @@ -120,7 +120,7 @@ protected function doWrite(string $sessionId, string $data): bool return true; } - public function updateTimestamp(string $sessionId, string $data): bool + public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool { $ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime'); $expiry = new UTCDateTime((time() + (int) $ttl) * 1000); @@ -136,7 +136,7 @@ public function updateTimestamp(string $sessionId, string $data): bool return true; } - protected function doRead(string $sessionId): string + protected function doRead(#[\SensitiveParameter] string $sessionId): string { $dbData = $this->getCollection()->findOne([ $this->options['id_field'] => $sessionId, diff --git a/Session/Storage/Handler/NullSessionHandler.php b/Session/Storage/Handler/NullSessionHandler.php index 790ac2fed..a77185e2e 100644 --- a/Session/Storage/Handler/NullSessionHandler.php +++ b/Session/Storage/Handler/NullSessionHandler.php @@ -23,27 +23,27 @@ public function close(): bool return true; } - public function validateId(string $sessionId): bool + public function validateId(#[\SensitiveParameter] string $sessionId): bool { return true; } - protected function doRead(string $sessionId): string + protected function doRead(#[\SensitiveParameter] string $sessionId): string { return ''; } - public function updateTimestamp(string $sessionId, string $data): bool + public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool { return true; } - protected function doWrite(string $sessionId, string $data): bool + protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool { return true; } - protected function doDestroy(string $sessionId): bool + protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool { return true; } diff --git a/Session/Storage/Handler/PdoSessionHandler.php b/Session/Storage/Handler/PdoSessionHandler.php index a046aef95..a5257b417 100644 --- a/Session/Storage/Handler/PdoSessionHandler.php +++ b/Session/Storage/Handler/PdoSessionHandler.php @@ -285,7 +285,7 @@ public function open(string $savePath, string $sessionName): bool return parent::open($savePath, $sessionName); } - public function read(string $sessionId): string + public function read(#[\SensitiveParameter] string $sessionId): string { try { return parent::read($sessionId); @@ -305,7 +305,7 @@ public function gc(int $maxlifetime): int|false return 0; } - protected function doDestroy(string $sessionId): bool + protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool { // delete the record associated with this id $sql = "DELETE FROM $this->table WHERE $this->idCol = :id"; @@ -323,7 +323,7 @@ protected function doDestroy(string $sessionId): bool return true; } - protected function doWrite(string $sessionId, string $data): bool + protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool { $maxlifetime = (int) (($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime')); @@ -366,7 +366,7 @@ protected function doWrite(string $sessionId, string $data): bool return true; } - public function updateTimestamp(string $sessionId, string $data): bool + public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool { $expiry = time() + (int) (($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime')); @@ -610,7 +610,7 @@ private function rollback(): void * We need to make sure we do not return session data that is already considered garbage according * to the session.gc_maxlifetime setting because gc() is called after read() and only sometimes. */ - protected function doRead(string $sessionId): string + protected function doRead(#[\SensitiveParameter] string $sessionId): string { if (self::LOCK_ADVISORY === $this->lockMode) { $this->unlockStatements[] = $this->doAdvisoryLock($sessionId); @@ -681,7 +681,7 @@ protected function doRead(string $sessionId): string * - for oci using DBMS_LOCK.REQUEST * - for sqlsrv using sp_getapplock with LockOwner = Session */ - private function doAdvisoryLock(string $sessionId): \PDOStatement + private function doAdvisoryLock(#[\SensitiveParameter] string $sessionId): \PDOStatement { switch ($this->driver) { case 'mysql': @@ -780,7 +780,7 @@ private function getSelectSql(): string /** * Returns an insert statement supported by the database for writing session data. */ - private function getInsertStatement(string $sessionId, string $sessionData, int $maxlifetime): \PDOStatement + private function getInsertStatement(#[\SensitiveParameter] string $sessionId, string $sessionData, int $maxlifetime): \PDOStatement { switch ($this->driver) { case 'oci': @@ -807,7 +807,7 @@ private function getInsertStatement(string $sessionId, string $sessionData, int /** * Returns an update statement supported by the database for writing session data. */ - private function getUpdateStatement(string $sessionId, string $sessionData, int $maxlifetime): \PDOStatement + private function getUpdateStatement(#[\SensitiveParameter] string $sessionId, string $sessionData, int $maxlifetime): \PDOStatement { switch ($this->driver) { case 'oci': @@ -834,7 +834,7 @@ private function getUpdateStatement(string $sessionId, string $sessionData, int /** * Returns a merge/upsert (i.e. insert or update) statement when supported by the database for writing session data. */ - private function getMergeStatement(string $sessionId, string $data, int $maxlifetime): ?\PDOStatement + private function getMergeStatement(#[\SensitiveParameter] string $sessionId, string $data, int $maxlifetime): ?\PDOStatement { switch (true) { case 'mysql' === $this->driver: diff --git a/Session/Storage/Handler/RedisSessionHandler.php b/Session/Storage/Handler/RedisSessionHandler.php index 38f488644..c4f1e0216 100644 --- a/Session/Storage/Handler/RedisSessionHandler.php +++ b/Session/Storage/Handler/RedisSessionHandler.php @@ -50,12 +50,12 @@ public function __construct( $this->ttl = $options['ttl'] ?? null; } - protected function doRead(string $sessionId): string + protected function doRead(#[\SensitiveParameter] string $sessionId): string { return $this->redis->get($this->prefix.$sessionId) ?: ''; } - protected function doWrite(string $sessionId, string $data): bool + protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool { $ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime'); $result = $this->redis->setEx($this->prefix.$sessionId, (int) $ttl, $data); @@ -63,7 +63,7 @@ protected function doWrite(string $sessionId, string $data): bool return $result && !$result instanceof ErrorInterface; } - protected function doDestroy(string $sessionId): bool + protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool { static $unlink = true; @@ -93,7 +93,7 @@ public function gc(int $maxlifetime): int|false return 0; } - public function updateTimestamp(string $sessionId, string $data): bool + public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool { $ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime'); diff --git a/Session/Storage/Handler/StrictSessionHandler.php b/Session/Storage/Handler/StrictSessionHandler.php index 1a1636481..1f8668744 100644 --- a/Session/Storage/Handler/StrictSessionHandler.php +++ b/Session/Storage/Handler/StrictSessionHandler.php @@ -47,22 +47,22 @@ public function open(string $savePath, string $sessionName): bool return $this->handler->open($savePath, $sessionName); } - protected function doRead(string $sessionId): string + protected function doRead(#[\SensitiveParameter] string $sessionId): string { return $this->handler->read($sessionId); } - public function updateTimestamp(string $sessionId, string $data): bool + public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool { return $this->write($sessionId, $data); } - protected function doWrite(string $sessionId, string $data): bool + protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool { return $this->handler->write($sessionId, $data); } - public function destroy(string $sessionId): bool + public function destroy(#[\SensitiveParameter] string $sessionId): bool { $this->doDestroy = true; $destroyed = parent::destroy($sessionId); @@ -70,7 +70,7 @@ public function destroy(string $sessionId): bool return $this->doDestroy ? $this->doDestroy($sessionId) : $destroyed; } - protected function doDestroy(string $sessionId): bool + protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool { $this->doDestroy = false; diff --git a/Session/Storage/Proxy/SessionHandlerProxy.php b/Session/Storage/Proxy/SessionHandlerProxy.php index c292e58f0..7bf3f9ff1 100644 --- a/Session/Storage/Proxy/SessionHandlerProxy.php +++ b/Session/Storage/Proxy/SessionHandlerProxy.php @@ -44,17 +44,17 @@ public function close(): bool return $this->handler->close(); } - public function read(string $sessionId): string|false + public function read(#[\SensitiveParameter] string $sessionId): string|false { return $this->handler->read($sessionId); } - public function write(string $sessionId, string $data): bool + public function write(#[\SensitiveParameter] string $sessionId, string $data): bool { return $this->handler->write($sessionId, $data); } - public function destroy(string $sessionId): bool + public function destroy(#[\SensitiveParameter] string $sessionId): bool { return $this->handler->destroy($sessionId); } @@ -64,12 +64,12 @@ public function gc(int $maxlifetime): int|false return $this->handler->gc($maxlifetime); } - public function validateId(string $sessionId): bool + public function validateId(#[\SensitiveParameter] string $sessionId): bool { return !$this->handler instanceof \SessionUpdateTimestampHandlerInterface || $this->handler->validateId($sessionId); } - public function updateTimestamp(string $sessionId, string $data): bool + public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool { return $this->handler instanceof \SessionUpdateTimestampHandlerInterface ? $this->handler->updateTimestamp($sessionId, $data) : $this->write($sessionId, $data); } From 10c7833c8d7b9d7f719f1686ef0a59635f26a6c8 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sun, 22 Jan 2023 15:14:08 +0100 Subject: [PATCH 009/111] Mark DSNs as #[SensitiveParameter] --- Session/Storage/Handler/PdoSessionHandler.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Session/Storage/Handler/PdoSessionHandler.php b/Session/Storage/Handler/PdoSessionHandler.php index a5257b417..84b3e336f 100644 --- a/Session/Storage/Handler/PdoSessionHandler.php +++ b/Session/Storage/Handler/PdoSessionHandler.php @@ -151,7 +151,7 @@ class PdoSessionHandler extends AbstractSessionHandler * * @throws \InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION */ - public function __construct(\PDO|string $pdoOrDsn = null, array $options = []) + public function __construct(#[\SensitiveParameter] \PDO|string $pdoOrDsn = null, #[\SensitiveParameter] array $options = []) { if ($pdoOrDsn instanceof \PDO) { if (\PDO::ERRMODE_EXCEPTION !== $pdoOrDsn->getAttribute(\PDO::ATTR_ERRMODE)) { @@ -415,7 +415,7 @@ public function close(): bool /** * Lazy-connects to the database. */ - private function connect(string $dsn): void + private function connect(#[\SensitiveParameter] string $dsn): void { $this->pdo = new \PDO($dsn, $this->username, $this->password, $this->connectionOptions); $this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); @@ -427,7 +427,7 @@ private function connect(string $dsn): void * * @todo implement missing support for oci DSN (which look totally different from other PDO ones) */ - private function buildDsnFromUrl(string $dsnOrUrl): string + private function buildDsnFromUrl(#[\SensitiveParameter] string $dsnOrUrl): string { // (pdo_)?sqlite3?:///... => (pdo_)?sqlite3?://localhost/... or else the URL will be invalid $url = preg_replace('#^((?:pdo_)?sqlite3?):///#', '$1://localhost/', $dsnOrUrl); From 4a3b869c9e02cdee47f34f6eb76fa91d5716f3ed Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 23 Jan 2023 14:36:51 +0100 Subject: [PATCH 010/111] Remove full DSNs from exception messages --- Session/Storage/Handler/SessionHandlerFactory.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Session/Storage/Handler/SessionHandlerFactory.php b/Session/Storage/Handler/SessionHandlerFactory.php index e390c8fee..33aaa7df5 100644 --- a/Session/Storage/Handler/SessionHandlerFactory.php +++ b/Session/Storage/Handler/SessionHandlerFactory.php @@ -54,7 +54,7 @@ public static function createHandler(object|string $connection, array $options = case str_starts_with($connection, 'rediss:'): case str_starts_with($connection, 'memcached:'): if (!class_exists(AbstractAdapter::class)) { - throw new \InvalidArgumentException(sprintf('Unsupported DSN "%s". Try running "composer require symfony/cache".', $connection)); + throw new \InvalidArgumentException('Unsupported Redis or Memcached DSN. Try running "composer require symfony/cache".'); } $handlerClass = str_starts_with($connection, 'memcached:') ? MemcachedSessionHandler::class : RedisSessionHandler::class; $connection = AbstractAdapter::createConnection($connection, ['lazy' => true]); @@ -63,7 +63,7 @@ public static function createHandler(object|string $connection, array $options = case str_starts_with($connection, 'pdo_oci://'): if (!class_exists(DriverManager::class)) { - throw new \InvalidArgumentException(sprintf('Unsupported DSN "%s". Try running "composer require doctrine/dbal".', $connection)); + throw new \InvalidArgumentException('Unsupported PDO OCI DSN. Try running "composer require doctrine/dbal".'); } $connection = DriverManager::getConnection(['url' => $connection])->getWrappedConnection(); // no break; From 6d8e13ef2f1d22cb899b78e4c07eef5229313ece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Ostroluck=C3=BD?= Date: Wed, 14 Dec 2022 20:24:34 +0100 Subject: [PATCH 011/111] [HttpFoundation] Accept Relay connection --- CHANGELOG.md | 1 + .../Storage/Handler/RedisSessionHandler.php | 3 +- .../Storage/Handler/SessionHandlerFactory.php | 2 ++ .../AbstractRedisSessionHandlerTestCase.php | 4 ++- .../Handler/RelaySessionHandlerTest.php | 28 +++++++++++++++++++ 5 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 Tests/Session/Storage/Handler/RelaySessionHandlerTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 208f466d7..8e70070fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * Add `ParameterBag::getEnum()` * Create migration for session table when pdo handler is used + * Add support for Relay PHP extension for Redis 6.2 --- diff --git a/Session/Storage/Handler/RedisSessionHandler.php b/Session/Storage/Handler/RedisSessionHandler.php index c4f1e0216..b696eee4b 100644 --- a/Session/Storage/Handler/RedisSessionHandler.php +++ b/Session/Storage/Handler/RedisSessionHandler.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; use Predis\Response\ErrorInterface; +use Relay\Relay; /** * Redis based session storage handler based on the Redis class @@ -39,7 +40,7 @@ class RedisSessionHandler extends AbstractSessionHandler * @throws \InvalidArgumentException When unsupported client or options are passed */ public function __construct( - private \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis, + private \Redis|Relay|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis, array $options = [], ) { if ($diff = array_diff(array_keys($options), ['prefix', 'ttl'])) { diff --git a/Session/Storage/Handler/SessionHandlerFactory.php b/Session/Storage/Handler/SessionHandlerFactory.php index 33aaa7df5..dbbe7dc88 100644 --- a/Session/Storage/Handler/SessionHandlerFactory.php +++ b/Session/Storage/Handler/SessionHandlerFactory.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; use Doctrine\DBAL\DriverManager; +use Relay\Relay; use Symfony\Component\Cache\Adapter\AbstractAdapter; /** @@ -32,6 +33,7 @@ public static function createHandler(object|string $connection, array $options = switch (true) { case $connection instanceof \Redis: + case $connection instanceof Relay: case $connection instanceof \RedisArray: case $connection instanceof \RedisCluster: case $connection instanceof \Predis\ClientInterface: diff --git a/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php b/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php index e5937b7df..e0c030310 100644 --- a/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php +++ b/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php @@ -12,10 +12,12 @@ namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler; use PHPUnit\Framework\TestCase; +use Relay\Relay; use Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler; /** * @requires extension redis + * * @group time-sensitive */ abstract class AbstractRedisSessionHandlerTestCase extends TestCase @@ -32,7 +34,7 @@ abstract class AbstractRedisSessionHandlerTestCase extends TestCase */ protected $redisClient; - abstract protected function createRedisClient(string $host): \Redis|\RedisArray|\RedisCluster|\Predis\Client; + abstract protected function createRedisClient(string $host): \Redis|Relay|\RedisArray|\RedisCluster|\Predis\Client; protected function setUp(): void { diff --git a/Tests/Session/Storage/Handler/RelaySessionHandlerTest.php b/Tests/Session/Storage/Handler/RelaySessionHandlerTest.php new file mode 100644 index 000000000..76553f96d --- /dev/null +++ b/Tests/Session/Storage/Handler/RelaySessionHandlerTest.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Session\Storage\Handler; + +use Relay\Relay; +use Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler\AbstractRedisSessionHandlerTestCase; + +/** + * @requires extension relay + * + * @group integration + */ +class RelaySessionHandlerTest extends AbstractRedisSessionHandlerTestCase +{ + protected function createRedisClient(string $host): Relay + { + return new Relay(...explode(':', $host)); + } +} From ff1d5432dd586901291de7cdb45745a71eaade3f Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Tue, 17 Jan 2023 11:00:01 +0100 Subject: [PATCH 012/111] [HttpFoundation] Improve return type of Header::all --- HeaderBag.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HeaderBag.php b/HeaderBag.php index 0883024b3..eb96b9dd9 100644 --- a/HeaderBag.php +++ b/HeaderBag.php @@ -63,7 +63,7 @@ public function __toString(): string * * @param string|null $key The name of the headers to return or null to get them all * - * @return array>|array + * @return ($key is null ? array> : array) */ public function all(string $key = null): array { From aff3a3cd8acda601a405c8541a64f47a8cc3ae4f Mon Sep 17 00:00:00 2001 From: Allison Guilhem Date: Tue, 24 Jan 2023 21:26:13 +0100 Subject: [PATCH 013/111] [HttpFoundation] inject SessionHandler in PdoSessionHandlerSchemaSubscriber --- Session/Storage/Handler/PdoSessionHandler.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Session/Storage/Handler/PdoSessionHandler.php b/Session/Storage/Handler/PdoSessionHandler.php index 84b3e336f..d6e60a729 100644 --- a/Session/Storage/Handler/PdoSessionHandler.php +++ b/Session/Storage/Handler/PdoSessionHandler.php @@ -178,9 +178,12 @@ public function __construct(#[\SensitiveParameter] \PDO|string $pdoOrDsn = null, $this->ttl = $options['ttl'] ?? null; } - public function configureSchema(Schema $schema, \Closure $isSameDatabase): void + /** + * Adds the Table to the Schema if it doesn't exist. + */ + public function configureSchema(Schema $schema, \Closure $isSameDatabase = null): void { - if ($schema->hasTable($this->table) || !$isSameDatabase($this->getConnection()->exec(...))) { + if ($schema->hasTable($this->table) || ($isSameDatabase && !$isSameDatabase($this->getConnection()->exec(...)))) { return; } From a579c079ba6edc5717bde3f06c162a151b4da835 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 31 Jan 2023 18:55:36 +0100 Subject: [PATCH 014/111] [HttpFoundation] Fix defining expiry index in PdoSessionHandler::configureSchema() --- Session/Storage/Handler/PdoSessionHandler.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Session/Storage/Handler/PdoSessionHandler.php b/Session/Storage/Handler/PdoSessionHandler.php index d6e60a729..bc7b22850 100644 --- a/Session/Storage/Handler/PdoSessionHandler.php +++ b/Session/Storage/Handler/PdoSessionHandler.php @@ -225,6 +225,7 @@ public function configureSchema(Schema $schema, \Closure $isSameDatabase = null) throw new \DomainException(sprintf('Creating the session table is currently not implemented for PDO driver "%s".', $this->driver)); } $table->setPrimaryKey([$this->idCol]); + $table->addIndex([$this->lifetimeCol], $this->lifetimeCol.'_idx'); } /** @@ -259,7 +260,7 @@ public function createTable() try { $this->pdo->exec($sql); - $this->pdo->exec("CREATE INDEX expiry ON $this->table ($this->lifetimeCol)"); + $this->pdo->exec("CREATE INDEX {$this->lifetimeCol}_idx ON $this->table ($this->lifetimeCol)"); } catch (\PDOException $e) { $this->rollback(); From 968650f5c469f412e88acb50bc4719c5f8ecfcea Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Mon, 13 Feb 2023 00:00:11 +0100 Subject: [PATCH 015/111] Add missing PHPdoc return types --- HeaderBag.php | 3 +++ ResponseHeaderBag.php | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/HeaderBag.php b/HeaderBag.php index eb96b9dd9..b7be1b382 100644 --- a/HeaderBag.php +++ b/HeaderBag.php @@ -254,6 +254,9 @@ public function count(): int return \count($this->headers); } + /** + * @return string + */ protected function getCacheControlHeader() { ksort($this->cacheControl); diff --git a/ResponseHeaderBag.php b/ResponseHeaderBag.php index 9e8c5793a..203a8b278 100644 --- a/ResponseHeaderBag.php +++ b/ResponseHeaderBag.php @@ -55,6 +55,9 @@ public function allPreserveCase(): array return $headers; } + /** + * @return array + */ public function allPreserveCaseWithoutCookies() { $headers = $this->allPreserveCase(); @@ -224,6 +227,8 @@ public function clearCookie(string $name, ?string $path = '/', string $domain = /** * @see HeaderUtils::makeDisposition() + * + * @return string */ public function makeDisposition(string $disposition, string $filename, string $filenameFallback = '') { From e29fb65ddf048dcae19d045a5c62d8844773ee2f Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sun, 12 Feb 2023 23:57:18 +0100 Subject: [PATCH 016/111] Add void return types --- BinaryFileResponse.php | 2 ++ ExpressionRequestMatcher.php | 3 ++ FileBag.php | 9 ++++++ HeaderBag.php | 12 ++++++++ InputBag.php | 6 ++-- ParameterBag.php | 9 ++++++ Request.php | 29 ++++++++++++++++++- RequestMatcher.php | 16 ++++++++++ RequestStack.php | 2 ++ ResponseHeaderBag.php | 16 ++++++++++ Session/Attribute/AttributeBag.php | 12 ++++++++ Session/Flash/AutoExpireFlashBag.php | 15 ++++++++++ Session/Flash/FlashBag.php | 15 ++++++++++ Session/Session.php | 21 ++++++++++++++ Session/Storage/Handler/PdoSessionHandler.php | 2 ++ Session/Storage/MetadataBag.php | 7 +++++ Session/Storage/MockArraySessionStorage.php | 24 +++++++++++++++ Session/Storage/MockFileSessionStorage.php | 3 ++ Session/Storage/NativeSessionStorage.php | 24 +++++++++++++++ Session/Storage/PhpBridgeSessionStorage.php | 3 ++ Session/Storage/Proxy/AbstractProxy.php | 4 +++ 21 files changed, 230 insertions(+), 4 deletions(-) diff --git a/BinaryFileResponse.php b/BinaryFileResponse.php index ad2cbda63..78958f1ae 100644 --- a/BinaryFileResponse.php +++ b/BinaryFileResponse.php @@ -351,6 +351,8 @@ public function getContent(): string|false /** * Trust X-Sendfile-Type header. + * + * @return void */ public static function trustXSendfileTypeHeader() { diff --git a/ExpressionRequestMatcher.php b/ExpressionRequestMatcher.php index 5628ea8bc..b09c167cf 100644 --- a/ExpressionRequestMatcher.php +++ b/ExpressionRequestMatcher.php @@ -29,6 +29,9 @@ class ExpressionRequestMatcher extends RequestMatcher private ExpressionLanguage $language; private Expression|string $expression; + /** + * @return void + */ public function setExpression(ExpressionLanguage $language, Expression|string $expression) { $this->language = $language; diff --git a/FileBag.php b/FileBag.php index 3ccc45d1d..b74a02e2e 100644 --- a/FileBag.php +++ b/FileBag.php @@ -31,12 +31,18 @@ public function __construct(array $parameters = []) $this->replace($parameters); } + /** + * @return void + */ public function replace(array $files = []) { $this->parameters = []; $this->add($files); } + /** + * @return void + */ public function set(string $key, mixed $value) { if (!\is_array($value) && !$value instanceof UploadedFile) { @@ -46,6 +52,9 @@ public function set(string $key, mixed $value) parent::set($key, $this->convertFileInformation($value)); } + /** + * @return void + */ public function add(array $files = []) { foreach ($files as $key => $file) { diff --git a/HeaderBag.php b/HeaderBag.php index b7be1b382..081f26a2d 100644 --- a/HeaderBag.php +++ b/HeaderBag.php @@ -86,6 +86,8 @@ public function keys(): array /** * Replaces the current HTTP headers by a new set. + * + * @return void */ public function replace(array $headers = []) { @@ -95,6 +97,8 @@ public function replace(array $headers = []) /** * Adds new headers the current HTTP headers set. + * + * @return void */ public function add(array $headers) { @@ -126,6 +130,8 @@ public function get(string $key, string $default = null): ?string * * @param string|string[]|null $values The value or an array of values * @param bool $replace Whether to replace the actual value or not (true by default) + * + * @return void */ public function set(string $key, string|array|null $values, bool $replace = true) { @@ -170,6 +176,8 @@ public function contains(string $key, string $value): bool /** * Removes a header. + * + * @return void */ public function remove(string $key) { @@ -202,6 +210,8 @@ public function getDate(string $key, \DateTime $default = null): ?\DateTimeInter /** * Adds a custom Cache-Control directive. + * + * @return void */ public function addCacheControlDirective(string $key, bool|string $value = true) { @@ -228,6 +238,8 @@ public function getCacheControlDirective(string $key): bool|string|null /** * Removes a Cache-Control directive. + * + * @return void */ public function removeCacheControlDirective(string $key) { diff --git a/InputBag.php b/InputBag.php index 446b82132..d0e069b50 100644 --- a/InputBag.php +++ b/InputBag.php @@ -43,7 +43,7 @@ public function get(string $key, mixed $default = null): string|int|float|bool|n /** * Replaces the current input values by a new set. */ - public function replace(array $inputs = []) + public function replace(array $inputs = []): void { $this->parameters = []; $this->add($inputs); @@ -52,7 +52,7 @@ public function replace(array $inputs = []) /** * Adds input values. */ - public function add(array $inputs = []) + public function add(array $inputs = []): void { foreach ($inputs as $input => $value) { $this->set($input, $value); @@ -64,7 +64,7 @@ public function add(array $inputs = []) * * @param string|int|float|bool|array|null $value */ - public function set(string $key, mixed $value) + public function set(string $key, mixed $value): void { if (null !== $value && !\is_scalar($value) && !\is_array($value) && !$value instanceof \Stringable) { throw new \InvalidArgumentException(sprintf('Expected a scalar, or an array as a 2nd argument to "%s()", "%s" given.', __METHOD__, get_debug_type($value))); diff --git a/ParameterBag.php b/ParameterBag.php index 9df9604e6..44d8d96d2 100644 --- a/ParameterBag.php +++ b/ParameterBag.php @@ -60,6 +60,8 @@ public function keys(): array /** * Replaces the current parameters by a new set. + * + * @return void */ public function replace(array $parameters = []) { @@ -68,6 +70,8 @@ public function replace(array $parameters = []) /** * Adds parameters. + * + * @return void */ public function add(array $parameters = []) { @@ -79,6 +83,9 @@ public function get(string $key, mixed $default = null): mixed return \array_key_exists($key, $this->parameters) ? $this->parameters[$key] : $default; } + /** + * @return void + */ public function set(string $key, mixed $value) { $this->parameters[$key] = $value; @@ -94,6 +101,8 @@ public function has(string $key): bool /** * Removes a parameter. + * + * @return void */ public function remove(string $key) { diff --git a/Request.php b/Request.php index 56d74874c..fdb701d00 100644 --- a/Request.php +++ b/Request.php @@ -263,6 +263,8 @@ public function __construct(array $query = [], array $request = [], array $attri * @param array $files The FILES parameters * @param array $server The SERVER parameters * @param string|resource|null $content The raw body data + * + * @return void */ public function initialize(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null) { @@ -417,6 +419,8 @@ public static function create(string $uri, string $method = 'GET', array $parame * This is mainly useful when you need to override the Request class * to keep BC with an existing system. It should not be used for any * other purpose. + * + * @return void */ public static function setFactory(?callable $callable) { @@ -521,6 +525,8 @@ public function __toString(): string * * It overrides $_GET, $_POST, $_REQUEST, $_SERVER, $_COOKIE. * $_FILES is never overridden, see rfc1867 + * + * @return void */ public function overrideGlobals() { @@ -561,6 +567,8 @@ public function overrideGlobals() * * @param array $proxies A list of trusted proxies, the string 'REMOTE_ADDR' will be replaced with $_SERVER['REMOTE_ADDR'] * @param int $trustedHeaderSet A bit field of Request::HEADER_*, to set which headers to trust from your proxies + * + * @return void */ public static function setTrustedProxies(array $proxies, int $trustedHeaderSet) { @@ -602,6 +610,8 @@ public static function getTrustedHeaderSet(): int * You should only list the hosts you manage using regexs. * * @param array $hostPatterns A list of trusted host patterns + * + * @return void */ public static function setTrustedHosts(array $hostPatterns) { @@ -648,6 +658,8 @@ public static function normalizeQueryString(?string $qs): string * If these methods are not protected against CSRF, this presents a possible vulnerability. * * The HTTP method can only be overridden when the real HTTP method is POST. + * + * @return void */ public static function enableHttpMethodParameterOverride() { @@ -733,6 +745,9 @@ public function hasSession(bool $skipIfUninitialized = false): bool return null !== $this->session && (!$skipIfUninitialized || $this->session instanceof SessionInterface); } + /** + * @return void + */ public function setSession(SessionInterface $session) { $this->session = $session; @@ -743,7 +758,7 @@ public function setSession(SessionInterface $session) * * @param callable(): SessionInterface $factory */ - public function setSessionFactory(callable $factory) + public function setSessionFactory(callable $factory): void { $this->session = $factory; } @@ -1153,6 +1168,8 @@ public function getHost(): string /** * Sets the request method. + * + * @return void */ public function setMethod(string $method) { @@ -1274,6 +1291,8 @@ public function getFormat(?string $mimeType): ?string * Associates a format with mime types. * * @param string|string[] $mimeTypes The associated mime types (the preferred one must be the first as it will be used as the content type) + * + * @return void */ public function setFormat(?string $format, string|array $mimeTypes) { @@ -1304,6 +1323,8 @@ public function getRequestFormat(?string $default = 'html'): ?string /** * Sets the request format. + * + * @return void */ public function setRequestFormat(?string $format) { @@ -1334,6 +1355,8 @@ public function getContentTypeFormat(): ?string /** * Sets the default locale. + * + * @return void */ public function setDefaultLocale(string $locale) { @@ -1354,6 +1377,8 @@ public function getDefaultLocale(): string /** * Sets the locale. + * + * @return void */ public function setLocale(string $locale) { @@ -1858,6 +1883,8 @@ protected function preparePathInfo(): string /** * Initializes HTTP request formats. + * + * @return void */ protected static function initializeFormats() { diff --git a/RequestMatcher.php b/RequestMatcher.php index 28cdd20c5..8c5f1d813 100644 --- a/RequestMatcher.php +++ b/RequestMatcher.php @@ -69,6 +69,8 @@ public function __construct(string $path = null, string $host = null, string|arr * Adds a check for the HTTP scheme. * * @param string|string[]|null $scheme An HTTP scheme or an array of HTTP schemes + * + * @return void */ public function matchScheme(string|array|null $scheme) { @@ -77,6 +79,8 @@ public function matchScheme(string|array|null $scheme) /** * Adds a check for the URL host name. + * + * @return void */ public function matchHost(?string $regexp) { @@ -87,6 +91,8 @@ public function matchHost(?string $regexp) * Adds a check for the the URL port. * * @param int|null $port The port number to connect to + * + * @return void */ public function matchPort(?int $port) { @@ -95,6 +101,8 @@ public function matchPort(?int $port) /** * Adds a check for the URL path info. + * + * @return void */ public function matchPath(?string $regexp) { @@ -105,6 +113,8 @@ public function matchPath(?string $regexp) * Adds a check for the client IP. * * @param string $ip A specific IP address or a range specified using IP/netmask like 192.168.1.0/24 + * + * @return void */ public function matchIp(string $ip) { @@ -115,6 +125,8 @@ public function matchIp(string $ip) * Adds a check for the client IP. * * @param string|string[]|null $ips A specific IP address or a range specified using IP/netmask like 192.168.1.0/24 + * + * @return void */ public function matchIps(string|array|null $ips) { @@ -127,6 +139,8 @@ public function matchIps(string|array|null $ips) * Adds a check for the HTTP method. * * @param string|string[]|null $method An HTTP method or an array of HTTP methods + * + * @return void */ public function matchMethod(string|array|null $method) { @@ -135,6 +149,8 @@ public function matchMethod(string|array|null $method) /** * Adds a check for request attribute. + * + * @return void */ public function matchAttribute(string $key, string $regexp) { diff --git a/RequestStack.php b/RequestStack.php index 6b13fa1e6..5aa8ba793 100644 --- a/RequestStack.php +++ b/RequestStack.php @@ -31,6 +31,8 @@ class RequestStack * * This method should generally not be called directly as the stack * management should be taken care of by the application itself. + * + * @return void */ public function push(Request $request) { diff --git a/ResponseHeaderBag.php b/ResponseHeaderBag.php index 203a8b278..10450ca5e 100644 --- a/ResponseHeaderBag.php +++ b/ResponseHeaderBag.php @@ -68,6 +68,9 @@ public function allPreserveCaseWithoutCookies() return $headers; } + /** + * @return void + */ public function replace(array $headers = []) { $this->headerNames = []; @@ -100,6 +103,9 @@ public function all(string $key = null): array return $headers; } + /** + * @return void + */ public function set(string $key, string|array|null $values, bool $replace = true) { $uniqueKey = strtr($key, self::UPPER, self::LOWER); @@ -128,6 +134,9 @@ public function set(string $key, string|array|null $values, bool $replace = true } } + /** + * @return void + */ public function remove(string $key) { $uniqueKey = strtr($key, self::UPPER, self::LOWER); @@ -160,6 +169,9 @@ public function getCacheControlDirective(string $key): bool|string|null return $this->computedCacheControl[$key] ?? null; } + /** + * @return void + */ public function setCookie(Cookie $cookie) { $this->cookies[$cookie->getDomain()][$cookie->getPath()][$cookie->getName()] = $cookie; @@ -168,6 +180,8 @@ public function setCookie(Cookie $cookie) /** * Removes a cookie from the array, but does not unset it in the browser. + * + * @return void */ public function removeCookie(string $name, ?string $path = '/', string $domain = null) { @@ -219,6 +233,8 @@ public function getCookies(string $format = self::COOKIES_FLAT): array /** * Clears a cookie in the browser. + * + * @return void */ public function clearCookie(string $name, ?string $path = '/', string $domain = null, bool $secure = false, bool $httpOnly = true, string $sameSite = null) { diff --git a/Session/Attribute/AttributeBag.php b/Session/Attribute/AttributeBag.php index 665961757..ad5a6590a 100644 --- a/Session/Attribute/AttributeBag.php +++ b/Session/Attribute/AttributeBag.php @@ -36,11 +36,17 @@ public function getName(): string return $this->name; } + /** + * @return void + */ public function setName(string $name) { $this->name = $name; } + /** + * @return void + */ public function initialize(array &$attributes) { $this->attributes = &$attributes; @@ -61,6 +67,9 @@ public function get(string $name, mixed $default = null): mixed return \array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default; } + /** + * @return void + */ public function set(string $name, mixed $value) { $this->attributes[$name] = $value; @@ -71,6 +80,9 @@ public function all(): array return $this->attributes; } + /** + * @return void + */ public function replace(array $attributes) { $this->attributes = []; diff --git a/Session/Flash/AutoExpireFlashBag.php b/Session/Flash/AutoExpireFlashBag.php index 00b1ac948..80bbeda0f 100644 --- a/Session/Flash/AutoExpireFlashBag.php +++ b/Session/Flash/AutoExpireFlashBag.php @@ -35,11 +35,17 @@ public function getName(): string return $this->name; } + /** + * @return void + */ public function setName(string $name) { $this->name = $name; } + /** + * @return void + */ public function initialize(array &$flashes) { $this->flashes = &$flashes; @@ -51,6 +57,9 @@ public function initialize(array &$flashes) $this->flashes['new'] = []; } + /** + * @return void + */ public function add(string $type, mixed $message) { $this->flashes['new'][$type][] = $message; @@ -90,11 +99,17 @@ public function all(): array return $return; } + /** + * @return void + */ public function setAll(array $messages) { $this->flashes['new'] = $messages; } + /** + * @return void + */ public function set(string $type, string|array $messages) { $this->flashes['new'][$type] = (array) $messages; diff --git a/Session/Flash/FlashBag.php b/Session/Flash/FlashBag.php index a30d9528d..659d59d18 100644 --- a/Session/Flash/FlashBag.php +++ b/Session/Flash/FlashBag.php @@ -35,16 +35,25 @@ public function getName(): string return $this->name; } + /** + * @return void + */ public function setName(string $name) { $this->name = $name; } + /** + * @return void + */ public function initialize(array &$flashes) { $this->flashes = &$flashes; } + /** + * @return void + */ public function add(string $type, mixed $message) { $this->flashes[$type][] = $message; @@ -81,11 +90,17 @@ public function all(): array return $return; } + /** + * @return void + */ public function set(string $type, string|array $messages) { $this->flashes[$type] = (array) $messages; } + /** + * @return void + */ public function setAll(array $messages) { $this->flashes = $messages; diff --git a/Session/Session.php b/Session/Session.php index a55dde482..b45be2f8c 100644 --- a/Session/Session.php +++ b/Session/Session.php @@ -69,6 +69,9 @@ public function get(string $name, mixed $default = null): mixed return $this->getAttributeBag()->get($name, $default); } + /** + * @return void + */ public function set(string $name, mixed $value) { $this->getAttributeBag()->set($name, $value); @@ -79,6 +82,9 @@ public function all(): array return $this->getAttributeBag()->all(); } + /** + * @return void + */ public function replace(array $attributes) { $this->getAttributeBag()->replace($attributes); @@ -89,6 +95,9 @@ public function remove(string $name): mixed return $this->getAttributeBag()->remove($name); } + /** + * @return void + */ public function clear() { $this->getAttributeBag()->clear(); @@ -154,6 +163,9 @@ public function migrate(bool $destroy = false, int $lifetime = null): bool return $this->storage->regenerate($destroy, $lifetime); } + /** + * @return void + */ public function save() { $this->storage->save(); @@ -164,6 +176,9 @@ public function getId(): string return $this->storage->getId(); } + /** + * @return void + */ public function setId(string $id) { if ($this->storage->getId() !== $id) { @@ -176,6 +191,9 @@ public function getName(): string return $this->storage->getName(); } + /** + * @return void + */ public function setName(string $name) { $this->storage->setName($name); @@ -191,6 +209,9 @@ public function getMetadataBag(): MetadataBag return $this->storage->getMetadataBag(); } + /** + * @return void + */ public function registerBag(SessionBagInterface $bag) { $this->storage->registerBag(new SessionBagProxy($bag, $this->data, $this->usageIndex, $this->usageReporter)); diff --git a/Session/Storage/Handler/PdoSessionHandler.php b/Session/Storage/Handler/PdoSessionHandler.php index bc7b22850..7d7d49ddb 100644 --- a/Session/Storage/Handler/PdoSessionHandler.php +++ b/Session/Storage/Handler/PdoSessionHandler.php @@ -238,6 +238,8 @@ public function configureSchema(Schema $schema, \Closure $isSameDatabase = null) * * @throws \PDOException When the table already exists * @throws \DomainException When an unsupported PDO driver is used + * + * @return void */ public function createTable() { diff --git a/Session/Storage/MetadataBag.php b/Session/Storage/MetadataBag.php index f2be617ce..79c03a8e0 100644 --- a/Session/Storage/MetadataBag.php +++ b/Session/Storage/MetadataBag.php @@ -51,6 +51,9 @@ public function __construct(string $storageKey = '_sf2_meta', int $updateThresho $this->updateThreshold = $updateThreshold; } + /** + * @return void + */ public function initialize(array &$array) { $this->meta = &$array; @@ -82,6 +85,8 @@ public function getLifetime(): int * will leave the system settings unchanged, 0 sets the cookie * to expire with browser session. Time is in seconds, and is * not a Unix timestamp. + * + * @return void */ public function stampNew(int $lifetime = null) { @@ -126,6 +131,8 @@ public function getName(): string /** * Sets name. + * + * @return void */ public function setName(string $name) { diff --git a/Session/Storage/MockArraySessionStorage.php b/Session/Storage/MockArraySessionStorage.php index 67fa0f95e..d30b56d69 100644 --- a/Session/Storage/MockArraySessionStorage.php +++ b/Session/Storage/MockArraySessionStorage.php @@ -68,6 +68,9 @@ public function __construct(string $name = 'MOCKSESSID', MetadataBag $metaBag = $this->setMetadataBag($metaBag); } + /** + * @return void + */ public function setSessionData(array $array) { $this->data = $array; @@ -105,6 +108,9 @@ public function getId(): string return $this->id; } + /** + * @return void + */ public function setId(string $id) { if ($this->started) { @@ -119,11 +125,17 @@ public function getName(): string return $this->name; } + /** + * @return void + */ public function setName(string $name) { $this->name = $name; } + /** + * @return void + */ public function save() { if (!$this->started || $this->closed) { @@ -134,6 +146,9 @@ public function save() $this->started = false; } + /** + * @return void + */ public function clear() { // clear out the bags @@ -148,6 +163,9 @@ public function clear() $this->loadSession(); } + /** + * @return void + */ public function registerBag(SessionBagInterface $bag) { $this->bags[$bag->getName()] = $bag; @@ -171,6 +189,9 @@ public function isStarted(): bool return $this->started; } + /** + * @return void + */ public function setMetadataBag(MetadataBag $bag = null) { if (1 > \func_num_args()) { @@ -198,6 +219,9 @@ protected function generateId(): string return hash('sha256', uniqid('ss_mock_', true)); } + /** + * @return void + */ protected function loadSession() { $bags = array_merge($this->bags, [$this->metadataBag]); diff --git a/Session/Storage/MockFileSessionStorage.php b/Session/Storage/MockFileSessionStorage.php index 28771ad54..95f69f2e1 100644 --- a/Session/Storage/MockFileSessionStorage.php +++ b/Session/Storage/MockFileSessionStorage.php @@ -73,6 +73,9 @@ public function regenerate(bool $destroy = false, int $lifetime = null): bool return parent::regenerate($destroy, $lifetime); } + /** + * @return void + */ public function save() { if (!$this->started) { diff --git a/Session/Storage/NativeSessionStorage.php b/Session/Storage/NativeSessionStorage.php index 834a5ebd6..2bf5e8dc1 100644 --- a/Session/Storage/NativeSessionStorage.php +++ b/Session/Storage/NativeSessionStorage.php @@ -183,6 +183,9 @@ public function getId(): string return $this->saveHandler->getId(); } + /** + * @return void + */ public function setId(string $id) { $this->saveHandler->setId($id); @@ -193,6 +196,9 @@ public function getName(): string return $this->saveHandler->getName(); } + /** + * @return void + */ public function setName(string $name) { $this->saveHandler->setName($name); @@ -222,6 +228,9 @@ public function regenerate(bool $destroy = false, int $lifetime = null): bool return session_regenerate_id($destroy); } + /** + * @return void + */ public function save() { // Store a copy so we can restore the bags in case the session was not left empty @@ -261,6 +270,9 @@ public function save() $this->started = false; } + /** + * @return void + */ public function clear() { // clear out the bags @@ -275,6 +287,9 @@ public function clear() $this->loadSession(); } + /** + * @return void + */ public function registerBag(SessionBagInterface $bag) { if ($this->started) { @@ -299,6 +314,9 @@ public function getBag(string $name): SessionBagInterface return $this->bags[$name]; } + /** + * @return void + */ public function setMetadataBag(MetadataBag $metaBag = null) { if (1 > \func_num_args()) { @@ -330,6 +348,8 @@ public function isStarted(): bool * @param array $options Session ini directives [key => value] * * @see https://php.net/session.configuration + * + * @return void */ public function setOptions(array $options) { @@ -374,6 +394,8 @@ public function setOptions(array $options) * @see https://php.net/sessionhandler * * @throws \InvalidArgumentException + * + * @return void */ public function setSaveHandler(AbstractProxy|\SessionHandlerInterface $saveHandler = null) { @@ -405,6 +427,8 @@ public function setSaveHandler(AbstractProxy|\SessionHandlerInterface $saveHandl * are set to (either PHP's internal, or a custom save handler set with session_set_save_handler()). * PHP takes the return value from the read() handler, unserializes it * and populates $_SESSION with the result automatically. + * + * @return void */ protected function loadSession(array &$session = null) { diff --git a/Session/Storage/PhpBridgeSessionStorage.php b/Session/Storage/PhpBridgeSessionStorage.php index eed748f81..28cb3c3d0 100644 --- a/Session/Storage/PhpBridgeSessionStorage.php +++ b/Session/Storage/PhpBridgeSessionStorage.php @@ -41,6 +41,9 @@ public function start(): bool return true; } + /** + * @return void + */ public function clear() { // clear out the bags and nothing else that may be set diff --git a/Session/Storage/Proxy/AbstractProxy.php b/Session/Storage/Proxy/AbstractProxy.php index 1845ee2c9..de4fca5fb 100644 --- a/Session/Storage/Proxy/AbstractProxy.php +++ b/Session/Storage/Proxy/AbstractProxy.php @@ -72,6 +72,8 @@ public function getId(): string * Sets the session ID. * * @throws \LogicException + * + * @return void */ public function setId(string $id) { @@ -94,6 +96,8 @@ public function getName(): string * Sets the session name. * * @throws \LogicException + * + * @return void */ public function setName(string $name) { From 153d1f3c27e866025780dc8aa646b5a43f337474 Mon Sep 17 00:00:00 2001 From: James Gilliland Date: Tue, 14 Feb 2023 10:18:19 -0600 Subject: [PATCH 017/111] [HttpFoundation] Deprecate passing invalid URI to Request::create Fixes: #47084 Passing an invalid URI to Request::create triggers an undefined code path. In PHP7 the false value returned by parse_url would quietly be treated as a an array through type coercion leading to unexpected results. In PHP8 this triggers a deprecation exposing the bug. --- Request.php | 4 ++++ Tests/RequestTest.php | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/Request.php b/Request.php index fdb701d00..054c155e6 100644 --- a/Request.php +++ b/Request.php @@ -342,6 +342,10 @@ public static function create(string $uri, string $method = 'GET', array $parame $server['REQUEST_METHOD'] = strtoupper($method); $components = parse_url($uri); + if (false === $components) { + trigger_deprecation('symfony/http-foundation', '6.3', 'Calling "%s()" with an invalid URI is deprecated.', __METHOD__); + $components = []; + } if (isset($components['host'])) { $server['SERVER_NAME'] = $components['host']; $server['HTTP_HOST'] = $components['host']; diff --git a/Tests/RequestTest.php b/Tests/RequestTest.php index dbba2b9af..2de9b5aeb 100644 --- a/Tests/RequestTest.php +++ b/Tests/RequestTest.php @@ -2554,6 +2554,15 @@ public function testReservedFlags() $this->assertNotSame(0b10000000, $value, sprintf('The constant "%s" should not use the reserved value "0b10000000".', $constant)); } } + + /** + * @group legacy + */ + public function testInvalidUriCreationDeprecated() + { + $this->expectDeprecation('Since symfony/http-foundation 6.3: Calling "Symfony\Component\HttpFoundation\Request::create()" with an invalid URI is deprecated.'); + Request::create('/invalid-path:123'); + } } class RequestContentProxy extends Request From 97bf9bbdf12ab316c1c4b90793cf4f091e58c070 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sat, 4 Mar 2023 15:47:35 +0100 Subject: [PATCH 018/111] [HttpFoundation][HttpKernel] Add missing void PHPdoc return types --- Session/Attribute/AttributeBagInterface.php | 5 +++++ Session/Flash/FlashBagInterface.php | 6 ++++++ Session/SessionBagInterface.php | 2 ++ Session/SessionInterface.php | 14 ++++++++++++++ Session/Storage/SessionStorageInterface.php | 10 ++++++++++ 5 files changed, 37 insertions(+) diff --git a/Session/Attribute/AttributeBagInterface.php b/Session/Attribute/AttributeBagInterface.php index 31a946444..e8cd0b5a4 100644 --- a/Session/Attribute/AttributeBagInterface.php +++ b/Session/Attribute/AttributeBagInterface.php @@ -32,6 +32,8 @@ public function get(string $name, mixed $default = null): mixed; /** * Sets an attribute. + * + * @return void */ public function set(string $name, mixed $value); @@ -42,6 +44,9 @@ public function set(string $name, mixed $value); */ public function all(): array; + /** + * @return void + */ public function replace(array $attributes); /** diff --git a/Session/Flash/FlashBagInterface.php b/Session/Flash/FlashBagInterface.php index cd10a23f3..bbcf7f8b7 100644 --- a/Session/Flash/FlashBagInterface.php +++ b/Session/Flash/FlashBagInterface.php @@ -22,11 +22,15 @@ interface FlashBagInterface extends SessionBagInterface { /** * Adds a flash message for the given type. + * + * @return void */ public function add(string $type, mixed $message); /** * Registers one or more messages for a given type. + * + * @return void */ public function set(string $type, string|array $messages); @@ -57,6 +61,8 @@ public function all(): array; /** * Sets all flash messages. + * + * @return void */ public function setAll(array $messages); diff --git a/Session/SessionBagInterface.php b/Session/SessionBagInterface.php index 821645d9b..e1c250554 100644 --- a/Session/SessionBagInterface.php +++ b/Session/SessionBagInterface.php @@ -25,6 +25,8 @@ public function getName(): string; /** * Initializes the Bag. + * + * @return void */ public function initialize(array &$array); diff --git a/Session/SessionInterface.php b/Session/SessionInterface.php index da2b3a37d..534883d2d 100644 --- a/Session/SessionInterface.php +++ b/Session/SessionInterface.php @@ -34,6 +34,8 @@ public function getId(): string; /** * Sets the session ID. + * + * @return void */ public function setId(string $id); @@ -44,6 +46,8 @@ public function getName(): string; /** * Sets the session name. + * + * @return void */ public function setName(string $name); @@ -78,6 +82,8 @@ public function migrate(bool $destroy = false, int $lifetime = null): bool; * This method is generally not required for real sessions as * the session will be automatically saved at the end of * code execution. + * + * @return void */ public function save(); @@ -93,6 +99,8 @@ public function get(string $name, mixed $default = null): mixed; /** * Sets an attribute. + * + * @return void */ public function set(string $name, mixed $value); @@ -103,6 +111,8 @@ public function all(): array; /** * Sets attributes. + * + * @return void */ public function replace(array $attributes); @@ -115,6 +125,8 @@ public function remove(string $name): mixed; /** * Clears all attributes. + * + * @return void */ public function clear(); @@ -125,6 +137,8 @@ public function isStarted(): bool; /** * Registers a SessionBagInterface with the session. + * + * @return void */ public function registerBag(SessionBagInterface $bag); diff --git a/Session/Storage/SessionStorageInterface.php b/Session/Storage/SessionStorageInterface.php index 8bd62a43a..ed2189e4e 100644 --- a/Session/Storage/SessionStorageInterface.php +++ b/Session/Storage/SessionStorageInterface.php @@ -40,6 +40,8 @@ public function getId(): string; /** * Sets the session ID. + * + * @return void */ public function setId(string $id); @@ -50,6 +52,8 @@ public function getName(): string; /** * Sets the session name. + * + * @return void */ public function setName(string $name); @@ -90,6 +94,8 @@ public function regenerate(bool $destroy = false, int $lifetime = null): bool; * a real PHP session would interfere with testing, in which case * it should actually persist the session data if required. * + * @return void + * * @throws \RuntimeException if the session is saved without being started, or if the session * is already closed */ @@ -97,6 +103,8 @@ public function save(); /** * Clear all session data in memory. + * + * @return void */ public function clear(); @@ -109,6 +117,8 @@ public function getBag(string $name): SessionBagInterface; /** * Registers a SessionBagInterface for use. + * + * @return void */ public function registerBag(SessionBagInterface $bag); From bdd8f1bb2cd5f7e0b38eb002a9324303d8415ef9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Sun, 6 Nov 2022 21:23:24 +0100 Subject: [PATCH 019/111] [HttpFoundation] Add support for the 103 status code (Early Hints) and other 1XX statuses --- CHANGELOG.md | 1 + Response.php | 54 +++++++++++++++++++++++++++++++--- StreamedResponse.php | 11 +++++-- Tests/ResponseTest.php | 11 +++++++ Tests/StreamedResponseTest.php | 11 +++++++ 5 files changed, 81 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e70070fc..9b2c0bcf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG * Add `ParameterBag::getEnum()` * Create migration for session table when pdo handler is used * Add support for Relay PHP extension for Redis + * The `Response::sendHeaders()` method now takes an optional HTTP status code as parameter, allowing to send informational responses such as Early Hints responses (103 status code) 6.2 --- diff --git a/Response.php b/Response.php index c141cbc0b..888c6ad85 100644 --- a/Response.php +++ b/Response.php @@ -211,6 +211,11 @@ class Response 511 => 'Network Authentication Required', // RFC6585 ]; + /** + * Tracks headers already sent in informational responses. + */ + private array $sentHeaders; + /** * @param int $status The HTTP status code (200 "OK" by default) * @@ -326,21 +331,54 @@ public function prepare(Request $request): static /** * Sends HTTP headers. * + * @param null|positive-int $statusCode The status code to use, override the statusCode property if set and not null + * * @return $this */ - public function sendHeaders(): static + public function sendHeaders(/* int $statusCode = null */): static { // headers have already been sent by the developer if (headers_sent()) { return $this; } + $statusCode = \func_num_args() > 0 ? func_get_arg(0) : null; + $informationalResponse = $statusCode >= 100 && $statusCode < 200; + if ($informationalResponse && !\function_exists('headers_send')) { + // skip informational responses if not supported by the SAPI + return $this; + } + // headers foreach ($this->headers->allPreserveCaseWithoutCookies() as $name => $values) { - $replace = 0 === strcasecmp($name, 'Content-Type'); - foreach ($values as $value) { + $newValues = $values; + $replace = false; + + // As recommended by RFC 8297, PHP automatically copies headers from previous 103 responses, we need to deal with that if headers changed + if (103 === $statusCode) { + $previousValues = $this->sentHeaders[$name] ?? null; + if ($previousValues === $values) { + // Header already sent in a previous response, it will be automatically copied in this response by PHP + continue; + } + + $replace = 0 === strcasecmp($name, 'Content-Type'); + + if (null !== $previousValues && array_diff($previousValues, $values)) { + header_remove($name); + $previousValues = null; + } + + $newValues = null === $previousValues ? $values : array_diff($values, $previousValues); + } + + foreach ($newValues as $value) { header($name.': '.$value, $replace, $this->statusCode); } + + if ($informationalResponse) { + $this->sentHeaders[$name] = $values; + } } // cookies @@ -348,8 +386,16 @@ public function sendHeaders(): static header('Set-Cookie: '.$cookie, false, $this->statusCode); } + if ($informationalResponse) { + headers_send($statusCode); + + return $this; + } + + $statusCode ??= $this->statusCode; + // status - header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode); + header(sprintf('HTTP/%s %s %s', $this->version, $statusCode, $this->statusText), true, $statusCode); return $this; } diff --git a/StreamedResponse.php b/StreamedResponse.php index 0bddcdc9b..2c8ff15f3 100644 --- a/StreamedResponse.php +++ b/StreamedResponse.php @@ -59,17 +59,22 @@ public function setCallback(callable $callback): static /** * This method only sends the headers once. * + * @param null|positive-int $statusCode The status code to use, override the statusCode property if set and not null + * * @return $this */ - public function sendHeaders(): static + public function sendHeaders(/* int $statusCode = null */): static { if ($this->headersSent) { return $this; } - $this->headersSent = true; + $statusCode = \func_num_args() > 0 ? func_get_arg(0) : null; + if ($statusCode < 100 || $statusCode >= 200) { + $this->headersSent = true; + } - return parent::sendHeaders(); + return parent::sendHeaders($statusCode); } /** diff --git a/Tests/ResponseTest.php b/Tests/ResponseTest.php index 7ab060ec1..bf126489d 100644 --- a/Tests/ResponseTest.php +++ b/Tests/ResponseTest.php @@ -42,6 +42,17 @@ public function testSendHeaders() $this->assertSame($response, $headers); } + public function testSendInformationalResponse() + { + $response = new Response(); + $response->sendHeaders(103); + + // Informational responses must not override the main status code + $this->assertSame(200, $response->getStatusCode()); + + $response->sendHeaders(); + } + public function testSend() { $response = new Response(); diff --git a/Tests/StreamedResponseTest.php b/Tests/StreamedResponseTest.php index 1ca1bb92a..2a2b7e731 100644 --- a/Tests/StreamedResponseTest.php +++ b/Tests/StreamedResponseTest.php @@ -124,4 +124,15 @@ public function testSetNotModified() $string = ob_get_clean(); $this->assertEmpty($string); } + + public function testSendInformationalResponse() + { + $response = new StreamedResponse(); + $response->sendHeaders(103); + + // Informational responses must not override the main status code + $this->assertSame(200, $response->getStatusCode()); + + $response->sendHeaders(); + } } From 94e1832a6e69efe60f148ab6baacc03973918da5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Tue, 6 Dec 2022 22:51:24 +0100 Subject: [PATCH 020/111] [HttpFoundation] Add `ParameterBag::getString()` and deprecate accepting invalid values --- CHANGELOG.md | 4 + InputBag.php | 27 +++++- ParameterBag.php | 50 +++++++++-- Tests/InputBagTest.php | 85 +++++++++++++++++++ Tests/ParameterBagTest.php | 165 +++++++++++++++++++++++++++++++++---- 5 files changed, 305 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b2c0bcf4..7b6a04888 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,14 @@ CHANGELOG 6.3 --- + * Calling `ParameterBag::getDigit()`, `getAlnum()`, `getAlpha()` on an `array` throws a `UnexpectedValueException` instead of a `TypeError` + * Add `ParameterBag::getString()` to convert a parameter into string and throw an exception if the value is invalid * Add `ParameterBag::getEnum()` * Create migration for session table when pdo handler is used * Add support for Relay PHP extension for Redis * The `Response::sendHeaders()` method now takes an optional HTTP status code as parameter, allowing to send informational responses such as Early Hints responses (103 status code) + * Deprecate conversion of invalid values in `ParameterBag::getInt()` and `ParameterBag::getBoolean()`, + * Deprecate ignoring invalid values when using `ParameterBag::filter()`, unless flag `FILTER_NULL_ON_FAILURE` is set 6.2 --- diff --git a/InputBag.php b/InputBag.php index d0e069b50..77990f571 100644 --- a/InputBag.php +++ b/InputBag.php @@ -92,6 +92,15 @@ public function getEnum(string $key, string $class, \BackedEnum $default = null) } } + /** + * Returns the parameter value converted to string. + */ + public function getString(string $key, string $default = ''): string + { + // Shortcuts the parent method because the validation on scalar is already done in get(). + return (string) $this->get($key, $default); + } + public function filter(string $key, mixed $default = null, int $filter = \FILTER_DEFAULT, mixed $options = []): mixed { $value = $this->has($key) ? $this->all()[$key] : $default; @@ -109,6 +118,22 @@ public function filter(string $key, mixed $default = null, int $filter = \FILTER throw new \InvalidArgumentException(sprintf('A Closure must be passed to "%s()" when FILTER_CALLBACK is used, "%s" given.', __METHOD__, get_debug_type($options['options'] ?? null))); } - return filter_var($value, $filter, $options); + $options['flags'] ??= 0; + $nullOnFailure = $options['flags'] & \FILTER_NULL_ON_FAILURE; + $options['flags'] |= \FILTER_NULL_ON_FAILURE; + + $value = filter_var($value, $filter, $options); + + if (null !== $value || $nullOnFailure) { + return $value; + } + + $method = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS | \DEBUG_BACKTRACE_PROVIDE_OBJECT, 2)[1]; + $method = ($method['object'] ?? null) === $this ? $method['function'] : 'filter'; + $hint = 'filter' === $method ? 'pass' : 'use method "filter()" with'; + + trigger_deprecation('symfony/http-foundation', '6.3', 'Ignoring invalid values when using "%s::%s(\'%s\')" is deprecated and will throw a "%s" in 7.0; '.$hint.' flag "FILTER_NULL_ON_FAILURE" to keep ignoring them.', $this::class, $method, $key, BadRequestException::class); + + return false; } } diff --git a/ParameterBag.php b/ParameterBag.php index 44d8d96d2..9d7012de3 100644 --- a/ParameterBag.php +++ b/ParameterBag.php @@ -114,7 +114,7 @@ public function remove(string $key) */ public function getAlpha(string $key, string $default = ''): string { - return preg_replace('/[^[:alpha:]]/', '', $this->get($key, $default)); + return preg_replace('/[^[:alpha:]]/', '', $this->getString($key, $default)); } /** @@ -122,7 +122,7 @@ public function getAlpha(string $key, string $default = ''): string */ public function getAlnum(string $key, string $default = ''): string { - return preg_replace('/[^[:alnum:]]/', '', $this->get($key, $default)); + return preg_replace('/[^[:alnum:]]/', '', $this->getString($key, $default)); } /** @@ -130,8 +130,20 @@ public function getAlnum(string $key, string $default = ''): string */ public function getDigits(string $key, string $default = ''): string { - // we need to remove - and + because they're allowed in the filter - return str_replace(['-', '+'], '', $this->filter($key, $default, \FILTER_SANITIZE_NUMBER_INT)); + return preg_replace('/[^[:digit:]]/', '', $this->getString($key, $default)); + } + + /** + * Returns the parameter as string. + */ + public function getString(string $key, string $default = ''): string + { + $value = $this->get($key, $default); + if (!\is_scalar($value) && !$value instanceof \Stringable) { + throw new \UnexpectedValueException(sprintf('Parameter value "%s" cannot be converted to "string".', $key)); + } + + return (string) $value; } /** @@ -139,7 +151,8 @@ public function getDigits(string $key, string $default = ''): string */ public function getInt(string $key, int $default = 0): int { - return (int) $this->get($key, $default); + // In 7.0 remove the fallback to 0, in case of failure an exception will be thrown + return $this->filter($key, $default, \FILTER_VALIDATE_INT, ['flags' => \FILTER_REQUIRE_SCALAR]) ?: 0; } /** @@ -147,7 +160,7 @@ public function getInt(string $key, int $default = 0): int */ public function getBoolean(string $key, bool $default = false): bool { - return $this->filter($key, $default, \FILTER_VALIDATE_BOOL); + return $this->filter($key, $default, \FILTER_VALIDATE_BOOL, ['flags' => \FILTER_REQUIRE_SCALAR]); } /** @@ -178,7 +191,8 @@ public function getEnum(string $key, string $class, \BackedEnum $default = null) /** * Filter key. * - * @param int $filter FILTER_* constant + * @param int $filter FILTER_* constant + * @param int|array{flags?: int, options?: array} $options Flags from FILTER_* constants * * @see https://php.net/filter-var */ @@ -196,11 +210,31 @@ public function filter(string $key, mixed $default = null, int $filter = \FILTER $options['flags'] = \FILTER_REQUIRE_ARRAY; } + if (\is_object($value) && !$value instanceof \Stringable) { + throw new \UnexpectedValueException(sprintf('Parameter value "%s" cannot be filtered.', $key)); + } + if ((\FILTER_CALLBACK & $filter) && !(($options['options'] ?? null) instanceof \Closure)) { throw new \InvalidArgumentException(sprintf('A Closure must be passed to "%s()" when FILTER_CALLBACK is used, "%s" given.', __METHOD__, get_debug_type($options['options'] ?? null))); } - return filter_var($value, $filter, $options); + $options['flags'] ??= 0; + $nullOnFailure = $options['flags'] & \FILTER_NULL_ON_FAILURE; + $options['flags'] |= \FILTER_NULL_ON_FAILURE; + + $value = filter_var($value, $filter, $options); + + if (null !== $value || $nullOnFailure) { + return $value; + } + + $method = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS | \DEBUG_BACKTRACE_PROVIDE_OBJECT, 2)[1]; + $method = ($method['object'] ?? null) === $this ? $method['function'] : 'filter'; + $hint = 'filter' === $method ? 'pass' : 'use method "filter()" with'; + + trigger_deprecation('symfony/http-foundation', '6.3', 'Ignoring invalid values when using "%s::%s(\'%s\')" is deprecated and will throw an "%s" in 7.0; '.$hint.' flag "FILTER_NULL_ON_FAILURE" to keep ignoring them.', $this::class, $method, $key, \UnexpectedValueException::class); + + return false; } /** diff --git a/Tests/InputBagTest.php b/Tests/InputBagTest.php index cdcdb3d0b..6a447a39c 100644 --- a/Tests/InputBagTest.php +++ b/Tests/InputBagTest.php @@ -12,12 +12,15 @@ namespace Symfony\Component\HttpFoundation\Tests; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\HttpFoundation\Exception\BadRequestException; use Symfony\Component\HttpFoundation\InputBag; use Symfony\Component\HttpFoundation\Tests\Fixtures\FooEnum; class InputBagTest extends TestCase { + use ExpectDeprecationTrait; + public function testGet() { $bag = new InputBag(['foo' => 'bar', 'null' => null, 'int' => 1, 'float' => 1.0, 'bool' => false, 'stringable' => new class() implements \Stringable { @@ -36,6 +39,58 @@ public function __toString(): string $this->assertFalse($bag->get('bool'), '->get() gets the value of a bool parameter'); } + /** + * @group legacy + */ + public function testGetIntError() + { + $this->expectDeprecation('Since symfony/http-foundation 6.3: Ignoring invalid values when using "Symfony\Component\HttpFoundation\InputBag::getInt(\'foo\')" is deprecated and will throw a "Symfony\Component\HttpFoundation\Exception\BadRequestException" in 7.0; use method "filter()" with flag "FILTER_NULL_ON_FAILURE" to keep ignoring them.'); + + $bag = new InputBag(['foo' => 'bar']); + $result = $bag->getInt('foo'); + $this->assertSame(0, $result); + } + + /** + * @group legacy + */ + public function testGetBooleanError() + { + $this->expectDeprecation('Since symfony/http-foundation 6.3: Ignoring invalid values when using "Symfony\Component\HttpFoundation\InputBag::getBoolean(\'foo\')" is deprecated and will throw a "Symfony\Component\HttpFoundation\Exception\BadRequestException" in 7.0; use method "filter()" with flag "FILTER_NULL_ON_FAILURE" to keep ignoring them.'); + + $bag = new InputBag(['foo' => 'bar']); + $result = $bag->getBoolean('foo'); + $this->assertFalse($result); + } + + public function testGetString() + { + $bag = new InputBag(['integer' => 123, 'bool_true' => true, 'bool_false' => false, 'string' => 'abc', 'stringable' => new class() implements \Stringable { + public function __toString(): string + { + return 'strval'; + } + }]); + + $this->assertSame('123', $bag->getString('integer'), '->getString() gets a value of parameter as string'); + $this->assertSame('abc', $bag->getString('string'), '->getString() gets a value of parameter as string'); + $this->assertSame('', $bag->getString('unknown'), '->getString() returns zero if a parameter is not defined'); + $this->assertSame('foo', $bag->getString('unknown', 'foo'), '->getString() returns the default if a parameter is not defined'); + $this->assertSame('1', $bag->getString('bool_true'), '->getString() returns "1" if a parameter is true'); + $this->assertSame('', $bag->getString('bool_false', 'foo'), '->getString() returns an empty empty string if a parameter is false'); + $this->assertSame('strval', $bag->getString('stringable'), '->getString() gets a value of a stringable paramater as string'); + } + + public function testGetStringExceptionWithArray() + { + $bag = new InputBag(['key' => ['abc']]); + + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('Input value "key" contains a non-scalar value.'); + + $bag->getString('key'); + } + public function testGetDoesNotUseDeepByDefault() { $bag = new InputBag(['foo' => ['bar' => 'moo']]); @@ -126,4 +181,34 @@ public function testGetEnumThrowsExceptionWithInvalidValue() $this->assertNull($bag->getEnum('invalid-value', FooEnum::class)); } + + public function testGetAlnumExceptionWithArray() + { + $bag = new InputBag(['word' => ['foo_BAR_012']]); + + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('Input value "word" contains a non-scalar value.'); + + $bag->getAlnum('word'); + } + + public function testGetAlphaExceptionWithArray() + { + $bag = new InputBag(['word' => ['foo_BAR_012']]); + + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('Input value "word" contains a non-scalar value.'); + + $bag->getAlpha('word'); + } + + public function testGetDigitsExceptionWithArray() + { + $bag = new InputBag(['word' => ['foo_BAR_012']]); + + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('Input value "word" contains a non-scalar value.'); + + $bag->getDigits('word'); + } } diff --git a/Tests/ParameterBagTest.php b/Tests/ParameterBagTest.php index b5f7e6006..62b95f42f 100644 --- a/Tests/ParameterBagTest.php +++ b/Tests/ParameterBagTest.php @@ -12,12 +12,15 @@ namespace Symfony\Component\HttpFoundation\Tests; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\HttpFoundation\Exception\BadRequestException; use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\Tests\Fixtures\FooEnum; class ParameterBagTest extends TestCase { + use ExpectDeprecationTrait; + public function testConstructor() { $this->testAll(); @@ -112,34 +115,137 @@ public function testHas() public function testGetAlpha() { - $bag = new ParameterBag(['word' => 'foo_BAR_012']); + $bag = new ParameterBag(['word' => 'foo_BAR_012', 'bool' => true, 'integer' => 123]); + + $this->assertSame('fooBAR', $bag->getAlpha('word'), '->getAlpha() gets only alphabetic characters'); + $this->assertSame('', $bag->getAlpha('unknown'), '->getAlpha() returns empty string if a parameter is not defined'); + $this->assertSame('abcDEF', $bag->getAlpha('unknown', 'abc_DEF_012'), '->getAlpha() returns filtered default if a parameter is not defined'); + $this->assertSame('', $bag->getAlpha('integer', 'abc_DEF_012'), '->getAlpha() returns empty string if a parameter is an integer'); + $this->assertSame('', $bag->getAlpha('bool', 'abc_DEF_012'), '->getAlpha() returns empty string if a parameter is a boolean'); + } + + public function testGetAlphaExceptionWithArray() + { + $bag = new ParameterBag(['word' => ['foo_BAR_012']]); + + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage('Parameter value "word" cannot be converted to "string".'); - $this->assertEquals('fooBAR', $bag->getAlpha('word'), '->getAlpha() gets only alphabetic characters'); - $this->assertEquals('', $bag->getAlpha('unknown'), '->getAlpha() returns empty string if a parameter is not defined'); + $bag->getAlpha('word'); } public function testGetAlnum() { - $bag = new ParameterBag(['word' => 'foo_BAR_012']); + $bag = new ParameterBag(['word' => 'foo_BAR_012', 'bool' => true, 'integer' => 123]); + + $this->assertSame('fooBAR012', $bag->getAlnum('word'), '->getAlnum() gets only alphanumeric characters'); + $this->assertSame('', $bag->getAlnum('unknown'), '->getAlnum() returns empty string if a parameter is not defined'); + $this->assertSame('abcDEF012', $bag->getAlnum('unknown', 'abc_DEF_012'), '->getAlnum() returns filtered default if a parameter is not defined'); + $this->assertSame('123', $bag->getAlnum('integer', 'abc_DEF_012'), '->getAlnum() returns the number as string if a parameter is an integer'); + $this->assertSame('1', $bag->getAlnum('bool', 'abc_DEF_012'), '->getAlnum() returns 1 if a parameter is true'); + } + + public function testGetAlnumExceptionWithArray() + { + $bag = new ParameterBag(['word' => ['foo_BAR_012']]); + + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage('Parameter value "word" cannot be converted to "string".'); - $this->assertEquals('fooBAR012', $bag->getAlnum('word'), '->getAlnum() gets only alphanumeric characters'); - $this->assertEquals('', $bag->getAlnum('unknown'), '->getAlnum() returns empty string if a parameter is not defined'); + $bag->getAlnum('word'); } public function testGetDigits() { - $bag = new ParameterBag(['word' => 'foo_BAR_012']); + $bag = new ParameterBag(['word' => 'foo_BAR_0+1-2', 'bool' => true, 'integer' => 123]); + + $this->assertSame('012', $bag->getDigits('word'), '->getDigits() gets only digits as string'); + $this->assertSame('', $bag->getDigits('unknown'), '->getDigits() returns empty string if a parameter is not defined'); + $this->assertSame('012', $bag->getDigits('unknown', 'abc_DEF_012'), '->getDigits() returns filtered default if a parameter is not defined'); + $this->assertSame('123', $bag->getDigits('integer', 'abc_DEF_012'), '->getDigits() returns the number as string if a parameter is an integer'); + $this->assertSame('1', $bag->getDigits('bool', 'abc_DEF_012'), '->getDigits() returns 1 if a parameter is true'); + } + + public function testGetDigitsExceptionWithArray() + { + $bag = new ParameterBag(['word' => ['foo_BAR_012']]); + + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage('Parameter value "word" cannot be converted to "string".'); - $this->assertEquals('012', $bag->getDigits('word'), '->getDigits() gets only digits as string'); - $this->assertEquals('', $bag->getDigits('unknown'), '->getDigits() returns empty string if a parameter is not defined'); + $bag->getDigits('word'); } public function testGetInt() { - $bag = new ParameterBag(['digits' => '0123']); + $bag = new ParameterBag(['digits' => '123', 'bool' => true]); + + $this->assertSame(123, $bag->getInt('digits', 0), '->getInt() gets a value of parameter as integer'); + $this->assertSame(0, $bag->getInt('unknown', 0), '->getInt() returns zero if a parameter is not defined'); + $this->assertSame(10, $bag->getInt('unknown', 10), '->getInt() returns the default if a parameter is not defined'); + $this->assertSame(1, $bag->getInt('bool', 0), '->getInt() returns 1 if a parameter is true'); + } + + /** + * @group legacy + */ + public function testGetIntExceptionWithArray() + { + $this->expectDeprecation('Since symfony/http-foundation 6.3: Ignoring invalid values when using "Symfony\Component\HttpFoundation\ParameterBag::getInt(\'digits\')" is deprecated and will throw an "UnexpectedValueException" in 7.0; use method "filter()" with flag "FILTER_NULL_ON_FAILURE" to keep ignoring them.'); + + $bag = new ParameterBag(['digits' => ['123']]); + $result = $bag->getInt('digits', 0); + $this->assertSame(0, $result); + } + + /** + * @group legacy + */ + public function testGetIntExceptionWithInvalid() + { + $this->expectDeprecation('Since symfony/http-foundation 6.3: Ignoring invalid values when using "Symfony\Component\HttpFoundation\ParameterBag::getInt(\'word\')" is deprecated and will throw an "UnexpectedValueException" in 7.0; use method "filter()" with flag "FILTER_NULL_ON_FAILURE" to keep ignoring them.'); + + $bag = new ParameterBag(['word' => 'foo_BAR_012']); + $result = $bag->getInt('word', 0); + $this->assertSame(0, $result); + } - $this->assertEquals(123, $bag->getInt('digits'), '->getInt() gets a value of parameter as integer'); - $this->assertEquals(0, $bag->getInt('unknown'), '->getInt() returns zero if a parameter is not defined'); + public function testGetString() + { + $bag = new ParameterBag(['integer' => 123, 'bool_true' => true, 'bool_false' => false, 'string' => 'abc', 'stringable' => new class() implements \Stringable { + public function __toString(): string + { + return 'strval'; + } + }]); + + $this->assertSame('123', $bag->getString('integer'), '->getString() gets a value of parameter as string'); + $this->assertSame('abc', $bag->getString('string'), '->getString() gets a value of parameter as string'); + $this->assertSame('', $bag->getString('unknown'), '->getString() returns zero if a parameter is not defined'); + $this->assertSame('foo', $bag->getString('unknown', 'foo'), '->getString() returns the default if a parameter is not defined'); + $this->assertSame('1', $bag->getString('bool_true'), '->getString() returns "1" if a parameter is true'); + $this->assertSame('', $bag->getString('bool_false', 'foo'), '->getString() returns an empty empty string if a parameter is false'); + $this->assertSame('strval', $bag->getString('stringable'), '->getString() gets a value of a stringable paramater as string'); + } + + public function testGetStringExceptionWithArray() + { + $bag = new ParameterBag(['key' => ['abc']]); + + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage('Parameter value "key" cannot be converted to "string".'); + + $bag->getString('key'); + } + + public function testGetStringExceptionWithObject() + { + $bag = new ParameterBag(['object' => $this]); + + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage('Parameter value "object" cannot be converted to "string".'); + + $bag->getString('object'); } public function testFilter() @@ -164,13 +270,13 @@ public function testFilter() // This test is repeated for code-coverage $this->assertEquals('http://example.com/foo', $bag->filter('url', '', \FILTER_VALIDATE_URL, \FILTER_FLAG_PATH_REQUIRED), '->filter() gets a value of parameter as URL with a path'); - $this->assertFalse($bag->filter('dec', '', \FILTER_VALIDATE_INT, [ - 'flags' => \FILTER_FLAG_ALLOW_HEX, + $this->assertNull($bag->filter('dec', '', \FILTER_VALIDATE_INT, [ + 'flags' => \FILTER_FLAG_ALLOW_HEX | \FILTER_NULL_ON_FAILURE, 'options' => ['min_range' => 1, 'max_range' => 0xFF], ]), '->filter() gets a value of parameter as integer between boundaries'); - $this->assertFalse($bag->filter('hex', '', \FILTER_VALIDATE_INT, [ - 'flags' => \FILTER_FLAG_ALLOW_HEX, + $this->assertNull($bag->filter('hex', '', \FILTER_VALIDATE_INT, [ + 'flags' => \FILTER_FLAG_ALLOW_HEX | \FILTER_NULL_ON_FAILURE, 'options' => ['min_range' => 1, 'max_range' => 0xFF], ]), '->filter() gets a value of parameter as integer between boundaries'); @@ -218,12 +324,25 @@ public function testCount() public function testGetBoolean() { - $parameters = ['string_true' => 'true', 'string_false' => 'false']; + $parameters = ['string_true' => 'true', 'string_false' => 'false', 'string' => 'abc']; $bag = new ParameterBag($parameters); $this->assertTrue($bag->getBoolean('string_true'), '->getBoolean() gets the string true as boolean true'); $this->assertFalse($bag->getBoolean('string_false'), '->getBoolean() gets the string false as boolean false'); $this->assertFalse($bag->getBoolean('unknown'), '->getBoolean() returns false if a parameter is not defined'); + $this->assertTrue($bag->getBoolean('unknown', true), '->getBoolean() returns default if a parameter is not defined'); + } + + /** + * @group legacy + */ + public function testGetBooleanExceptionWithInvalid() + { + $this->expectDeprecation('Since symfony/http-foundation 6.3: Ignoring invalid values when using "Symfony\Component\HttpFoundation\ParameterBag::getBoolean(\'invalid\')" is deprecated and will throw an "UnexpectedValueException" in 7.0; use method "filter()" with flag "FILTER_NULL_ON_FAILURE" to keep ignoring them.'); + + $bag = new ParameterBag(['invalid' => 'foo']); + $result = $bag->getBoolean('invalid', 0); + $this->assertFalse($result); } public function testGetEnum() @@ -260,3 +379,15 @@ public function testGetEnumThrowsExceptionWithInvalidValueType() $this->assertNull($bag->getEnum('invalid-value', FooEnum::class)); } } + +class InputStringable +{ + public function __construct(private string $value) + { + } + + public function __toString(): string + { + return $this->value; + } +} From 3665b191916aadc7411981de82a13795d43df35f Mon Sep 17 00:00:00 2001 From: Daniel Burger <48986191+danielburger1337@users.noreply.github.com> Date: Sat, 18 Mar 2023 15:45:21 +0100 Subject: [PATCH 021/111] [HttpFoundation] Add IpUtils::isPrivateIp --- CHANGELOG.md | 1 + IpUtils.php | 23 +++++++++++++++++++++++ Tests/IpUtilsTest.php | 31 +++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b6a04888..b40e8c7e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ CHANGELOG * Create migration for session table when pdo handler is used * Add support for Relay PHP extension for Redis * The `Response::sendHeaders()` method now takes an optional HTTP status code as parameter, allowing to send informational responses such as Early Hints responses (103 status code) + * Add `IpUtils::isPrivateIp` * Deprecate conversion of invalid values in `ParameterBag::getInt()` and `ParameterBag::getBoolean()`, * Deprecate ignoring invalid values when using `ParameterBag::filter()`, unless flag `FILTER_NULL_ON_FAILURE` is set diff --git a/IpUtils.php b/IpUtils.php index 8f78d1b1d..4f17be64a 100644 --- a/IpUtils.php +++ b/IpUtils.php @@ -18,6 +18,21 @@ */ class IpUtils { + public const PRIVATE_SUBNETS = [ + '127.0.0.0/8', // RFC1700 (Loopback) + '10.0.0.0/8', // RFC1918 + '192.168.0.0/16', // RFC1918 + '172.16.0.0/12', // RFC1918 + '169.254.0.0/16', // RFC3927 + '0.0.0.0/8', // RFC5735 + '240.0.0.0/4', // RFC1112 + '::1/128', // Loopback + 'fc00::/7', // Unique Local Address + 'fe80::/10', // Link Local Address + '::ffff:0:0/96', // IPv4 translations + '::/128', // Unspecified address + ]; + private static array $checkedIps = []; /** @@ -191,4 +206,12 @@ public static function anonymize(string $ip): string return $ip; } + + /** + * Checks if an IPv4 or IPv6 address is contained in the list of private IP subnets. + */ + public static function isPrivateIp(string $requestIp): bool + { + return self::checkIp($requestIp, self::PRIVATE_SUBNETS); + } } diff --git a/Tests/IpUtilsTest.php b/Tests/IpUtilsTest.php index a8416a513..7146f32dd 100644 --- a/Tests/IpUtilsTest.php +++ b/Tests/IpUtilsTest.php @@ -154,4 +154,35 @@ public static function getIp4SubnetMaskZeroData() [false, '1.2.3.4', '256.256.256/0'], // invalid CIDR notation ]; } + + /** + * @dataProvider getIsPrivateIpData + */ + public function testIsPrivateIp(string $ip, bool $matches) + { + $this->assertSame($matches, IpUtils::isPrivateIp($ip)); + } + + public static function getIsPrivateIpData(): array + { + return [ + // private + ['127.0.0.1', true], + ['10.0.0.1', true], + ['192.168.0.1', true], + ['172.16.0.1', true], + ['169.254.0.1', true], + ['0.0.0.1', true], + ['240.0.0.1', true], + ['::1', true], + ['fc00::1', true], + ['fe80::1', true], + ['::ffff:0:1', true], + ['fd00::1', true], + + // public + ['104.26.14.6', false], + ['2606:4700:20::681a:e06', false], + ]; + } } From 9d12f2294233336b992cbc135bbc795d95bddabf Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 21 Mar 2023 22:03:43 +0100 Subject: [PATCH 022/111] Replace "use-by-ref" by static vars when possible in closures --- AcceptHeader.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/AcceptHeader.php b/AcceptHeader.php index 5edf5f5f1..853c000e0 100644 --- a/AcceptHeader.php +++ b/AcceptHeader.php @@ -46,11 +46,10 @@ public function __construct(array $items) */ public static function fromString(?string $headerValue): self { - $index = 0; - $parts = HeaderUtils::split($headerValue ?? '', ',;='); - return new self(array_map(function ($subParts) use (&$index) { + return new self(array_map(function ($subParts) { + static $index = 0; $part = array_shift($subParts); $attributes = HeaderUtils::combine($subParts); From a384f87f94c121ddd65d4513b09d358bde412c02 Mon Sep 17 00:00:00 2001 From: mfettig Date: Fri, 24 Mar 2023 15:07:33 -0400 Subject: [PATCH 023/111] [Cache] Support Redis cluster connections with predis/predis:^2.0 --- .../Storage/Handler/PredisClusterSessionHandlerTest.php | 5 ++++- composer.json | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Tests/Session/Storage/Handler/PredisClusterSessionHandlerTest.php b/Tests/Session/Storage/Handler/PredisClusterSessionHandlerTest.php index fd4a13bef..492487766 100644 --- a/Tests/Session/Storage/Handler/PredisClusterSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/PredisClusterSessionHandlerTest.php @@ -20,6 +20,9 @@ class PredisClusterSessionHandlerTest extends AbstractRedisSessionHandlerTestCas { protected function createRedisClient(string $host): Client { - return new Client([array_combine(['host', 'port'], explode(':', getenv('REDIS_HOST')) + [1 => 6379])]); + return new Client( + [array_combine(['host', 'port'], explode(':', getenv('REDIS_HOST')) + [1 => 6379])], + ['cluster' => 'redis'] + ); } } diff --git a/composer.json b/composer.json index 944029655..245cde53f 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ }, "require-dev": { "doctrine/dbal": "^2.13.1|^3.0", - "predis/predis": "~1.0", + "predis/predis": "^1.1|^2.0", "symfony/cache": "^5.4|^6.0", "symfony/dependency-injection": "^5.4|^6.0", "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4", From 2b6fdfdc0e41bfc5eaf08add466c4e35b74ae2d6 Mon Sep 17 00:00:00 2001 From: Daniel Burger <48986191+danielburger1337@users.noreply.github.com> Date: Wed, 19 Apr 2023 22:43:24 +0200 Subject: [PATCH 024/111] [HttpFoundation] Clear IpUtils cache to prevent memory leaks --- IpUtils.php | 56 ++++++++++++++++++++++++++++++------------- Tests/IpUtilsTest.php | 19 +++++++++++++++ 2 files changed, 59 insertions(+), 16 deletions(-) diff --git a/IpUtils.php b/IpUtils.php index 2106f61d9..ceab620c2 100644 --- a/IpUtils.php +++ b/IpUtils.php @@ -75,23 +75,23 @@ public static function checkIp(string $requestIp, string|array $ips): bool public static function checkIp4(string $requestIp, string $ip): bool { $cacheKey = $requestIp.'-'.$ip.'-v4'; - if (isset(self::$checkedIps[$cacheKey])) { - return self::$checkedIps[$cacheKey]; + if (null !== $cacheValue = self::getCacheResult($cacheKey)) { + return $cacheValue; } if (!filter_var($requestIp, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)) { - return self::$checkedIps[$cacheKey] = false; + return self::setCacheResult($cacheKey, false); } if (str_contains($ip, '/')) { [$address, $netmask] = explode('/', $ip, 2); if ('0' === $netmask) { - return self::$checkedIps[$cacheKey] = false !== filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4); + return self::setCacheResult($cacheKey, false !== filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)); } if ($netmask < 0 || $netmask > 32) { - return self::$checkedIps[$cacheKey] = false; + return self::setCacheResult($cacheKey, false); } } else { $address = $ip; @@ -99,10 +99,10 @@ public static function checkIp4(string $requestIp, string $ip): bool } if (false === ip2long($address)) { - return self::$checkedIps[$cacheKey] = false; + return self::setCacheResult($cacheKey, false); } - return self::$checkedIps[$cacheKey] = 0 === substr_compare(sprintf('%032b', ip2long($requestIp)), sprintf('%032b', ip2long($address)), 0, $netmask); + return self::setCacheResult($cacheKey, 0 === substr_compare(sprintf('%032b', ip2long($requestIp)), sprintf('%032b', ip2long($address)), 0, $netmask)); } /** @@ -120,8 +120,8 @@ public static function checkIp4(string $requestIp, string $ip): bool public static function checkIp6(string $requestIp, string $ip): bool { $cacheKey = $requestIp.'-'.$ip.'-v6'; - if (isset(self::$checkedIps[$cacheKey])) { - return self::$checkedIps[$cacheKey]; + if (null !== $cacheValue = self::getCacheResult($cacheKey)) { + return $cacheValue; } if (!((\extension_loaded('sockets') && \defined('AF_INET6')) || @inet_pton('::1'))) { @@ -130,14 +130,14 @@ public static function checkIp6(string $requestIp, string $ip): bool // Check to see if we were given a IP4 $requestIp or $ip by mistake if (!filter_var($requestIp, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) { - return self::$checkedIps[$cacheKey] = false; + return self::setCacheResult($cacheKey, false); } if (str_contains($ip, '/')) { [$address, $netmask] = explode('/', $ip, 2); if (!filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) { - return self::$checkedIps[$cacheKey] = false; + return self::setCacheResult($cacheKey, false); } if ('0' === $netmask) { @@ -145,11 +145,11 @@ public static function checkIp6(string $requestIp, string $ip): bool } if ($netmask < 1 || $netmask > 128) { - return self::$checkedIps[$cacheKey] = false; + return self::setCacheResult($cacheKey, false); } } else { if (!filter_var($ip, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) { - return self::$checkedIps[$cacheKey] = false; + return self::setCacheResult($cacheKey, false); } $address = $ip; @@ -160,7 +160,7 @@ public static function checkIp6(string $requestIp, string $ip): bool $bytesTest = unpack('n*', @inet_pton($requestIp)); if (!$bytesAddr || !$bytesTest) { - return self::$checkedIps[$cacheKey] = false; + return self::setCacheResult($cacheKey, false); } for ($i = 1, $ceil = ceil($netmask / 16); $i <= $ceil; ++$i) { @@ -168,11 +168,11 @@ public static function checkIp6(string $requestIp, string $ip): bool $left = ($left <= 16) ? $left : 16; $mask = ~(0xFFFF >> $left) & 0xFFFF; if (($bytesAddr[$i] & $mask) != ($bytesTest[$i] & $mask)) { - return self::$checkedIps[$cacheKey] = false; + return self::setCacheResult($cacheKey, false); } } - return self::$checkedIps[$cacheKey] = true; + return self::setCacheResult($cacheKey, true); } /** @@ -214,4 +214,28 @@ public static function isPrivateIp(string $requestIp): bool { return self::checkIp($requestIp, self::PRIVATE_SUBNETS); } + + private static function getCacheResult(string $cacheKey): ?bool + { + if (isset(self::$checkedIps[$cacheKey])) { + // Move the item last in cache (LRU) + $value = self::$checkedIps[$cacheKey]; + unset(self::$checkedIps[$cacheKey]); + self::$checkedIps[$cacheKey] = $value; + + return self::$checkedIps[$cacheKey]; + } + + return null; + } + + private static function setCacheResult(string $cacheKey, bool $result): bool + { + if (1000 < \count(self::$checkedIps)) { + // stop memory leak if there are many keys + self::$checkedIps = \array_slice(self::$checkedIps, 500, null, true); + } + + return self::$checkedIps[$cacheKey] = $result; + } } diff --git a/Tests/IpUtilsTest.php b/Tests/IpUtilsTest.php index 014d272cd..8f6b869a1 100644 --- a/Tests/IpUtilsTest.php +++ b/Tests/IpUtilsTest.php @@ -200,4 +200,23 @@ public static function getIsPrivateIpData(): array ['2606:4700:20::681a:e06', false], ]; } + + public function testCacheSizeLimit() + { + $ref = new \ReflectionClass(IpUtils::class); + + /** @var array */ + $checkedIps = $ref->getStaticPropertyValue('checkedIps'); + $this->assertIsArray($checkedIps); + + $maxCheckedIps = 1000; + + for ($i = 1; $i < $maxCheckedIps * 1.5; ++$i) { + $ip = '192.168.1.'.str_pad((string) $i, 3, '0'); + + IpUtils::checkIp4($ip, '127.0.0.1'); + } + + $this->assertLessThan($maxCheckedIps, \count($checkedIps)); + } } From 622df5859f13ecfc8c52713c7f6ecef46c10d040 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Mon, 6 Mar 2023 10:01:30 -0500 Subject: [PATCH 025/111] [HttpFoundation] add `Request::getPayload()` --- CHANGELOG.md | 3 ++- Request.php | 8 ++++++++ Tests/RequestTest.php | 11 +++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b40e8c7e7..a98d23d9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,8 @@ CHANGELOG * Create migration for session table when pdo handler is used * Add support for Relay PHP extension for Redis * The `Response::sendHeaders()` method now takes an optional HTTP status code as parameter, allowing to send informational responses such as Early Hints responses (103 status code) - * Add `IpUtils::isPrivateIp` + * Add `IpUtils::isPrivateIp()` + * Add `Request::getPayload(): InputBag` * Deprecate conversion of invalid values in `ParameterBag::getInt()` and `ParameterBag::getBoolean()`, * Deprecate ignoring invalid values when using `ParameterBag::filter()`, unless flag `FILTER_NULL_ON_FAILURE` is set diff --git a/Request.php b/Request.php index 17eb95829..a6650c49a 100644 --- a/Request.php +++ b/Request.php @@ -1504,6 +1504,14 @@ public function getContent(bool $asResource = false) return $this->content; } + /** + * Gets the decoded form or json request body. + */ + public function getPayload(): InputBag + { + return $this->request->count() ? clone $this->request : new InputBag($this->toArray()); + } + /** * Gets the request body decoded as array, typically from a JSON payload. * diff --git a/Tests/RequestTest.php b/Tests/RequestTest.php index 2de9b5aeb..5e8df28bb 100644 --- a/Tests/RequestTest.php +++ b/Tests/RequestTest.php @@ -1300,6 +1300,17 @@ public function testToArray() $this->assertEquals(['foo' => 'bar'], $req->toArray()); } + public function testGetPayload() + { + $req = new Request([], [], [], [], [], [], json_encode(['foo' => 'bar'])); + $this->assertSame(['foo' => 'bar'], $req->getPayload()->all()); + $req->getPayload()->set('new', 'key'); + $this->assertSame(['foo' => 'bar'], $req->getPayload()->all()); + + $req = new Request([], ['foo' => 'bar'], [], [], [], [], json_encode(['baz' => 'qux'])); + $this->assertSame(['foo' => 'bar'], $req->getPayload()->all()); + } + /** * @dataProvider provideOverloadedMethods */ From 630d7b7ad8d2c981eeaec008c235b61a4771d6ec Mon Sep 17 00:00:00 2001 From: "Roland Franssen :)" Date: Thu, 20 Apr 2023 18:25:57 +0200 Subject: [PATCH 026/111] Update Request.php --- Request.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Request.php b/Request.php index a6650c49a..cf7e56f13 100644 --- a/Request.php +++ b/Request.php @@ -89,6 +89,8 @@ class Request /** * Request body parameters ($_POST). * + * @see getPayload() for portability between content types + * * @var InputBag */ public $request; @@ -1515,6 +1517,8 @@ public function getPayload(): InputBag /** * Gets the request body decoded as array, typically from a JSON payload. * + * @see getPayload() for portability between content types + * * @throws JsonException When the body cannot be decoded to an array */ public function toArray(): array From 531373672a99eec1c6dfecf8be5269bad413a103 Mon Sep 17 00:00:00 2001 From: Artyum Petrov <17199757+artyuum@users.noreply.github.com> Date: Thu, 20 Apr 2023 19:24:19 +0400 Subject: [PATCH 027/111] Add "composer require..." in all exception messages when needed --- ExpressionRequestMatcher.php | 2 +- composer.json | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/ExpressionRequestMatcher.php b/ExpressionRequestMatcher.php index b09c167cf..fe65e920d 100644 --- a/ExpressionRequestMatcher.php +++ b/ExpressionRequestMatcher.php @@ -41,7 +41,7 @@ public function setExpression(ExpressionLanguage $language, Expression|string $e public function matches(Request $request): bool { if (!isset($this->language)) { - throw new \LogicException('Unable to match the request as the expression language is not available.'); + throw new \LogicException('Unable to match the request as the expression language is not available. Try running "composer require symfony/expression-language".'); } return $this->language->evaluate($this->expression, [ diff --git a/composer.json b/composer.json index 245cde53f..248bcbb16 100644 --- a/composer.json +++ b/composer.json @@ -34,9 +34,6 @@ "conflict": { "symfony/cache": "<6.2" }, - "suggest" : { - "symfony/mime": "To use the file extension guesser" - }, "autoload": { "psr-4": { "Symfony\\Component\\HttpFoundation\\": "" }, "exclude-from-classmap": [ From 4e2744b1328316249b6f5691e264606a30910d4f Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Fri, 28 Apr 2023 14:21:20 +0200 Subject: [PATCH 028/111] Add missing return types --- Request.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Request.php b/Request.php index cf7e56f13..633b4a63e 100644 --- a/Request.php +++ b/Request.php @@ -1732,6 +1732,9 @@ public function preferSafeContent(): bool * Copyright (c) 2005-2010 Zend Technologies USA Inc. (https://www.zend.com/) */ + /** + * @return string + */ protected function prepareRequestUri() { $requestUri = ''; From e8a0efb127f7b1fc2766b69e7052c8f90ec9ee5f Mon Sep 17 00:00:00 2001 From: Kevin Mian Kraiker Date: Tue, 9 May 2023 19:17:09 -0300 Subject: [PATCH 029/111] Update HeaderBag::all PhpDoc For better compliance with code quality tools. --- HeaderBag.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HeaderBag.php b/HeaderBag.php index 0883024b3..dc5a9a60a 100644 --- a/HeaderBag.php +++ b/HeaderBag.php @@ -63,7 +63,7 @@ public function __toString(): string * * @param string|null $key The name of the headers to return or null to get them all * - * @return array>|array + * @return array>|list */ public function all(string $key = null): array { From c437f396668ac4d038bdee4487064aa8b794e379 Mon Sep 17 00:00:00 2001 From: Lane Shukhov Date: Mon, 8 May 2023 20:35:45 +0300 Subject: [PATCH 030/111] [HttpFoundation] Fix file streaming after connection aborted --- BinaryFileResponse.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BinaryFileResponse.php b/BinaryFileResponse.php index 03cf1a23e..d3caa36aa 100644 --- a/BinaryFileResponse.php +++ b/BinaryFileResponse.php @@ -349,7 +349,7 @@ public function sendContent() while ('' !== $data) { $read = fwrite($out, $data); if (false === $read || connection_aborted()) { - break; + break 2; } if (0 < $length) { $length -= $read; From 3dc6dfd164f69ad7b5d5464a0d9d4dd9bc947035 Mon Sep 17 00:00:00 2001 From: Alexander Schranz Date: Mon, 15 May 2023 21:14:36 +0200 Subject: [PATCH 031/111] Fix problem with empty generator in StreamedJsonResponse --- StreamedJsonResponse.php | 4 ++++ Tests/StreamedJsonResponseTest.php | 29 +++++++++++++++++++++-------- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/StreamedJsonResponse.php b/StreamedJsonResponse.php index 445bd77d7..cf858a5eb 100644 --- a/StreamedJsonResponse.php +++ b/StreamedJsonResponse.php @@ -130,6 +130,10 @@ private function stream(): void echo json_encode($item, $jsonEncodingOptions); } + if ($isFirstItem) { // indicates that the generator was empty + echo '['; + } + echo '[' === $startTag ? ']' : '}'; } diff --git a/Tests/StreamedJsonResponseTest.php b/Tests/StreamedJsonResponseTest.php index e142672fd..046f7dae4 100644 --- a/Tests/StreamedJsonResponseTest.php +++ b/Tests/StreamedJsonResponseTest.php @@ -30,6 +30,19 @@ public function testResponseSimpleList() $this->assertSame('{"_embedded":{"articles":["Article 1","Article 2","Article 3"],"news":["News 1","News 2","News 3"]}}', $content); } + public function testResponseEmptyList() + { + $content = $this->createSendResponse( + [ + '_embedded' => [ + 'articles' => $this->generatorSimple('Article', 0), + ], + ], + ); + + $this->assertSame('{"_embedded":{"articles":[]}}', $content); + } + public function testResponseObjectsList() { $content = $this->createSendResponse( @@ -222,20 +235,20 @@ private function createSendResponse(array $data): string /** * @return \Generator */ - private function generatorSimple(string $test): \Generator + private function generatorSimple(string $test, int $length = 3): \Generator { - yield $test.' 1'; - yield $test.' 2'; - yield $test.' 3'; + for ($i = 1; $i <= $length; ++$i) { + yield $test.' '.$i; + } } /** * @return \Generator */ - private function generatorArray(string $test): \Generator + private function generatorArray(string $test, int $length = 3): \Generator { - yield ['title' => $test.' 1']; - yield ['title' => $test.' 2']; - yield ['title' => $test.' 3']; + for ($i = 1; $i <= $length; ++$i) { + yield ['title' => $test.' '.$i]; + } } } From 3c59f97f6249ce552a44f01b93bfcbd786a954f5 Mon Sep 17 00:00:00 2001 From: Giorgio Premi Date: Fri, 12 May 2023 18:17:02 +0200 Subject: [PATCH 032/111] UrlHelper is now aware of RequestContext changes RequestContext in routing can change at runtime after the UrlHelper has been created, so using a RequestContextAwareInterface instance (i.e. the router) will workaround URL generation issues. --- Tests/UrlHelperTest.php | 36 ++++++++++++++++++++++++++++++++++++ UrlHelper.php | 38 +++++++++++++++++++++++++++----------- 2 files changed, 63 insertions(+), 11 deletions(-) diff --git a/Tests/UrlHelperTest.php b/Tests/UrlHelperTest.php index 2057dd709..080b5ffea 100644 --- a/Tests/UrlHelperTest.php +++ b/Tests/UrlHelperTest.php @@ -16,6 +16,7 @@ use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\UrlHelper; use Symfony\Component\Routing\RequestContext; +use Symfony\Component\Routing\RequestContextAwareInterface; class UrlHelperTest extends TestCase { @@ -64,11 +65,46 @@ public function testGenerateAbsoluteUrlWithRequestContext($path, $baseUrl, $host } $requestContext = new RequestContext($baseUrl, 'GET', $host, $scheme, $httpPort, $httpsPort, $path); + $helper = new UrlHelper(new RequestStack(), $requestContext); $this->assertEquals($expected, $helper->getAbsoluteUrl($path)); } + /** + * @dataProvider getGenerateAbsoluteUrlRequestContextData + */ + public function testGenerateAbsoluteUrlWithRequestContextAwareInterface($path, $baseUrl, $host, $scheme, $httpPort, $httpsPort, $expected) + { + if (!class_exists(RequestContext::class)) { + $this->markTestSkipped('The Routing component is needed to run tests that depend on its request context.'); + } + + $requestContext = new RequestContext($baseUrl, 'GET', $host, $scheme, $httpPort, $httpsPort, $path); + $contextAware = new class($requestContext) implements RequestContextAwareInterface { + private $requestContext; + + public function __construct($requestContext) + { + $this->requestContext = $requestContext; + } + + public function setContext(RequestContext $context) + { + $this->requestContext = $context; + } + + public function getContext() + { + return $this->requestContext; + } + }; + + $helper = new UrlHelper(new RequestStack(), $contextAware); + + $this->assertEquals($expected, $helper->getAbsoluteUrl($path)); + } + /** * @dataProvider getGenerateAbsoluteUrlRequestContextData */ diff --git a/UrlHelper.php b/UrlHelper.php index c15f101cd..90659947d 100644 --- a/UrlHelper.php +++ b/UrlHelper.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpFoundation; use Symfony\Component\Routing\RequestContext; +use Symfony\Component\Routing\RequestContextAwareInterface; /** * A helper service for manipulating URLs within and outside the request scope. @@ -23,8 +24,15 @@ final class UrlHelper private $requestStack; private $requestContext; - public function __construct(RequestStack $requestStack, RequestContext $requestContext = null) + /** + * @param RequestContextAwareInterface|RequestContext|null $requestContext + */ + public function __construct(RequestStack $requestStack, $requestContext = null) { + if (null !== $requestContext && !$requestContext instanceof RequestContext && !$requestContext instanceof RequestContextAwareInterface) { + throw new \TypeError(__METHOD__.': Argument #2 ($requestContext) must of type Symfony\Component\Routing\RequestContextAwareInterface|Symfony\Component\Routing\RequestContext|null, '.get_debug_type($requestContext).' given.'); + } + $this->requestStack = $requestStack; $this->requestContext = $requestContext; } @@ -73,28 +81,36 @@ public function getRelativePath(string $path): string private function getAbsoluteUrlFromContext(string $path): string { - if (null === $this->requestContext || '' === $host = $this->requestContext->getHost()) { + if (null === $context = $this->requestContext) { + return $path; + } + + if ($context instanceof RequestContextAwareInterface) { + $context = $context->getContext(); + } + + if ('' === $host = $context->getHost()) { return $path; } - $scheme = $this->requestContext->getScheme(); + $scheme = $context->getScheme(); $port = ''; - if ('http' === $scheme && 80 !== $this->requestContext->getHttpPort()) { - $port = ':'.$this->requestContext->getHttpPort(); - } elseif ('https' === $scheme && 443 !== $this->requestContext->getHttpsPort()) { - $port = ':'.$this->requestContext->getHttpsPort(); + if ('http' === $scheme && 80 !== $context->getHttpPort()) { + $port = ':'.$context->getHttpPort(); + } elseif ('https' === $scheme && 443 !== $context->getHttpsPort()) { + $port = ':'.$context->getHttpsPort(); } if ('#' === $path[0]) { - $queryString = $this->requestContext->getQueryString(); - $path = $this->requestContext->getPathInfo().($queryString ? '?'.$queryString : '').$path; + $queryString = $context->getQueryString(); + $path = $context->getPathInfo().($queryString ? '?'.$queryString : '').$path; } elseif ('?' === $path[0]) { - $path = $this->requestContext->getPathInfo().$path; + $path = $context->getPathInfo().$path; } if ('/' !== $path[0]) { - $path = rtrim($this->requestContext->getBaseUrl(), '/').'/'.$path; + $path = rtrim($context->getBaseUrl(), '/').'/'.$path; } return $scheme.'://'.$host.$port.$path; From df27f4191a4292d01fd062296e09cbc8b657cb57 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 19 May 2023 14:39:53 +0200 Subject: [PATCH 033/111] Fix merge --- Tests/UrlHelperTest.php | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Tests/UrlHelperTest.php b/Tests/UrlHelperTest.php index a1a11a9aa..02f6c64cf 100644 --- a/Tests/UrlHelperTest.php +++ b/Tests/UrlHelperTest.php @@ -82,19 +82,17 @@ public function testGenerateAbsoluteUrlWithRequestContextAwareInterface($path, $ $requestContext = new RequestContext($baseUrl, 'GET', $host, $scheme, $httpPort, $httpsPort, $path); $contextAware = new class($requestContext) implements RequestContextAwareInterface { - private $requestContext; - - public function __construct($requestContext) - { - $this->requestContext = $requestContext; + public function __construct( + private RequestContext $requestContext, + ) { } - public function setContext(RequestContext $context) + public function setContext(RequestContext $context): void { $this->requestContext = $context; } - public function getContext() + public function getContext(): RequestContext { return $this->requestContext; } From 2cbca2b2c7f40f60d45cd3be5e4834d5cc07ac8b Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 11 May 2023 10:20:44 +0200 Subject: [PATCH 034/111] Remove unnecessary usages of DateTime --- BinaryFileResponse.php | 2 +- CHANGELOG.md | 5 +++++ HeaderBag.php | 8 +++++--- Response.php | 23 +++++++---------------- Tests/HeaderBagTest.php | 2 +- 5 files changed, 19 insertions(+), 21 deletions(-) diff --git a/BinaryFileResponse.php b/BinaryFileResponse.php index cd716e590..71e806fc1 100644 --- a/BinaryFileResponse.php +++ b/BinaryFileResponse.php @@ -125,7 +125,7 @@ public function setChunkSize(int $chunkSize): static */ public function setAutoLastModified(): static { - $this->setLastModified(\DateTime::createFromFormat('U', $this->file->getMTime())); + $this->setLastModified(\DateTimeImmutable::createFromFormat('U', $this->file->getMTime())); return $this; } diff --git a/CHANGELOG.md b/CHANGELOG.md index a98d23d9d..5f1f6d5ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +6.4 +--- + + * Make `HeaderBag::getDate()`, `Response::getDate()`, `getExpires()` and `getLastModified()` return a `DateTimeImmutable` + 6.3 --- diff --git a/HeaderBag.php b/HeaderBag.php index c49a23972..3128a1d83 100644 --- a/HeaderBag.php +++ b/HeaderBag.php @@ -193,15 +193,17 @@ public function remove(string $key) /** * Returns the HTTP header value converted to a date. * + * @return \DateTimeImmutable|null + * * @throws \RuntimeException When the HTTP header is not parseable */ - public function getDate(string $key, \DateTime $default = null): ?\DateTimeInterface + public function getDate(string $key, \DateTimeInterface $default = null): ?\DateTimeInterface { if (null === $value = $this->get($key)) { - return $default; + return null !== $default ? \DateTimeImmutable::createFromInterface($default) : null; } - if (false === $date = \DateTime::createFromFormat(\DATE_RFC2822, $value)) { + if (false === $date = \DateTimeImmutable::createFromFormat(\DATE_RFC2822, $value)) { throw new \RuntimeException(sprintf('The "%s" HTTP header is not parseable (%s).', $key, $value)); } diff --git a/Response.php b/Response.php index 888c6ad85..8e09c46d4 100644 --- a/Response.php +++ b/Response.php @@ -687,7 +687,7 @@ public function mustRevalidate(): bool * * @final */ - public function getDate(): ?\DateTimeInterface + public function getDate(): ?\DateTimeImmutable { return $this->headers->getDate('Date'); } @@ -701,10 +701,7 @@ public function getDate(): ?\DateTimeInterface */ public function setDate(\DateTimeInterface $date): static { - if ($date instanceof \DateTime) { - $date = \DateTimeImmutable::createFromMutable($date); - } - + $date = \DateTimeImmutable::createFromInterface($date); $date = $date->setTimezone(new \DateTimeZone('UTC')); $this->headers->set('Date', $date->format('D, d M Y H:i:s').' GMT'); @@ -745,13 +742,13 @@ public function expire(): static * * @final */ - public function getExpires(): ?\DateTimeInterface + public function getExpires(): ?\DateTimeImmutable { try { return $this->headers->getDate('Expires'); } catch (\RuntimeException) { // according to RFC 2616 invalid date formats (e.g. "0" and "-1") must be treated as in the past - return \DateTime::createFromFormat('U', time() - 172800); + return \DateTimeImmutable::createFromFormat('U', time() - 172800); } } @@ -775,10 +772,7 @@ public function setExpires(\DateTimeInterface $date = null): static return $this; } - if ($date instanceof \DateTime) { - $date = \DateTimeImmutable::createFromMutable($date); - } - + $date = \DateTimeImmutable::createFromInterface($date); $date = $date->setTimezone(new \DateTimeZone('UTC')); $this->headers->set('Expires', $date->format('D, d M Y H:i:s').' GMT'); @@ -934,7 +928,7 @@ public function setClientTtl(int $seconds): static * * @final */ - public function getLastModified(): ?\DateTimeInterface + public function getLastModified(): ?\DateTimeImmutable { return $this->headers->getDate('Last-Modified'); } @@ -959,10 +953,7 @@ public function setLastModified(\DateTimeInterface $date = null): static return $this; } - if ($date instanceof \DateTime) { - $date = \DateTimeImmutable::createFromMutable($date); - } - + $date = \DateTimeImmutable::createFromInterface($date); $date = $date->setTimezone(new \DateTimeZone('UTC')); $this->headers->set('Last-Modified', $date->format('D, d M Y H:i:s').' GMT'); diff --git a/Tests/HeaderBagTest.php b/Tests/HeaderBagTest.php index 50546311a..d7507fc03 100644 --- a/Tests/HeaderBagTest.php +++ b/Tests/HeaderBagTest.php @@ -45,7 +45,7 @@ public function testGetDate() { $bag = new HeaderBag(['foo' => 'Tue, 4 Sep 2012 20:00:00 +0200']); $headerDate = $bag->getDate('foo'); - $this->assertInstanceOf(\DateTime::class, $headerDate); + $this->assertInstanceOf(\DateTimeImmutable::class, $headerDate); } public function testGetDateNull() From 86c012b4cc8dca1b03003ede87a44ffa2a15387b Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 23 May 2023 17:38:00 +0200 Subject: [PATCH 035/111] [6.4] Allow 7.0 deps --- composer.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 248bcbb16..80fa409cb 100644 --- a/composer.json +++ b/composer.json @@ -24,12 +24,12 @@ "require-dev": { "doctrine/dbal": "^2.13.1|^3.0", "predis/predis": "^1.1|^2.0", - "symfony/cache": "^5.4|^6.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4", - "symfony/mime": "^5.4|^6.0", - "symfony/expression-language": "^5.4|^6.0", - "symfony/rate-limiter": "^5.2|^6.0" + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/rate-limiter": "^5.4|^6.0|^7.0" }, "conflict": { "symfony/cache": "<6.2" From cdcec48c87ce9b318ebc2a9c1b252c9aa9c65112 Mon Sep 17 00:00:00 2001 From: wiseguy1394 Date: Wed, 31 May 2023 12:34:12 +0200 Subject: [PATCH 036/111] add @throws to getPayload --- Request.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Request.php b/Request.php index 633b4a63e..375b3b4fb 100644 --- a/Request.php +++ b/Request.php @@ -1508,6 +1508,8 @@ public function getContent(bool $asResource = false) /** * Gets the decoded form or json request body. + * + * @throws JsonException When the body cannot be decoded to an array */ public function getPayload(): InputBag { From f64671e2512ce72d6bbb263436e9e6c80a18dfff Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 1 Jun 2023 12:58:59 +0200 Subject: [PATCH 037/111] Fix Doctrine deprecations --- Session/Storage/Handler/PdoSessionHandler.php | 4 ++-- Session/Storage/Handler/SessionHandlerFactory.php | 14 +++++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Session/Storage/Handler/PdoSessionHandler.php b/Session/Storage/Handler/PdoSessionHandler.php index 2d830200b..cad7e0a72 100644 --- a/Session/Storage/Handler/PdoSessionHandler.php +++ b/Session/Storage/Handler/PdoSessionHandler.php @@ -397,8 +397,8 @@ public function updateTimestamp($sessionId, $data) $updateStmt = $this->pdo->prepare( "UPDATE $this->table SET $this->lifetimeCol = :expiry, $this->timeCol = :time WHERE $this->idCol = :id" ); - $updateStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); - $updateStmt->bindParam(':expiry', $expiry, \PDO::PARAM_INT); + $updateStmt->bindValue(':id', $sessionId, \PDO::PARAM_STR); + $updateStmt->bindValue(':expiry', $expiry, \PDO::PARAM_INT); $updateStmt->bindValue(':time', time(), \PDO::PARAM_INT); $updateStmt->execute(); } catch (\PDOException $e) { diff --git a/Session/Storage/Handler/SessionHandlerFactory.php b/Session/Storage/Handler/SessionHandlerFactory.php index f3f7b201d..3e838d8a4 100644 --- a/Session/Storage/Handler/SessionHandlerFactory.php +++ b/Session/Storage/Handler/SessionHandlerFactory.php @@ -11,7 +11,11 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; +use Doctrine\DBAL\Configuration; use Doctrine\DBAL\DriverManager; +use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory; +use Doctrine\DBAL\Tools\DsnParser; +use Doctrine\ORM\ORMSetup; use Symfony\Component\Cache\Adapter\AbstractAdapter; use Symfony\Component\Cache\Traits\RedisClusterProxy; use Symfony\Component\Cache\Traits\RedisProxy; @@ -71,7 +75,15 @@ public static function createHandler($connection): AbstractSessionHandler if (!class_exists(DriverManager::class)) { throw new \InvalidArgumentException(sprintf('Unsupported DSN "%s". Try running "composer require doctrine/dbal".', $connection)); } - $connection = DriverManager::getConnection(['url' => $connection])->getWrappedConnection(); + $connection[3] = '-'; + $params = class_exists(DsnParser::class) ? (new DsnParser())->parse($connection) : ['url' => $connection]; + $config = class_exists(ORMSetup::class) ? ORMSetup::createConfiguration(true) : new Configuration(); + if (class_exists(DefaultSchemaManagerFactory::class)) { + $config->setSchemaManagerFactory(new DefaultSchemaManagerFactory()); + } + + $connection = DriverManager::getConnection($params, $config); + $connection = method_exists($connection, 'getNativeConnection') ? $connection->getNativeConnection() : $connection->getWrappedConnection(); // no break; case str_starts_with($connection, 'mssql://'): From 87b8bfbde9c0174549989b8c404791be3ba28d6b Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 9 Jun 2023 12:27:40 +0200 Subject: [PATCH 038/111] CS fix --- Session/Storage/Handler/SessionHandlerFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Session/Storage/Handler/SessionHandlerFactory.php b/Session/Storage/Handler/SessionHandlerFactory.php index 7550002bd..e590fce65 100644 --- a/Session/Storage/Handler/SessionHandlerFactory.php +++ b/Session/Storage/Handler/SessionHandlerFactory.php @@ -13,10 +13,10 @@ use Doctrine\DBAL\Configuration; use Doctrine\DBAL\DriverManager; -use Relay\Relay; use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory; use Doctrine\DBAL\Tools\DsnParser; use Doctrine\ORM\ORMSetup; +use Relay\Relay; use Symfony\Component\Cache\Adapter\AbstractAdapter; /** From 76002b1213314cb1f123f67eee2e8ddfe6b53997 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 21 Jun 2023 17:59:30 +0200 Subject: [PATCH 039/111] [HttpFoundation] Make Request::getPayload() return an empty InputBag if request body is empty --- Request.php | 20 +++++++++++++++++++- Tests/RequestTest.php | 3 +++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Request.php b/Request.php index 9445a4b28..0bef6f8d7 100644 --- a/Request.php +++ b/Request.php @@ -1513,7 +1513,25 @@ public function getContent(bool $asResource = false) */ public function getPayload(): InputBag { - return $this->request->count() ? clone $this->request : new InputBag($this->toArray()); + if ($this->request->count()) { + return clone $this->request; + } + + if ('' === $content = $this->getContent()) { + return new InputBag([]); + } + + try { + $content = json_decode($content, true, 512, \JSON_BIGINT_AS_STRING | \JSON_THROW_ON_ERROR); + } catch (\JsonException $e) { + throw new JsonException('Could not decode request body.', $e->getCode(), $e); + } + + if (!\is_array($content)) { + throw new JsonException(sprintf('JSON content was expected to decode to an array, "%s" returned.', get_debug_type($content))); + } + + return new InputBag($content); } /** diff --git a/Tests/RequestTest.php b/Tests/RequestTest.php index 5e8df28bb..308e9e6fd 100644 --- a/Tests/RequestTest.php +++ b/Tests/RequestTest.php @@ -1309,6 +1309,9 @@ public function testGetPayload() $req = new Request([], ['foo' => 'bar'], [], [], [], [], json_encode(['baz' => 'qux'])); $this->assertSame(['foo' => 'bar'], $req->getPayload()->all()); + + $req = new Request([], [], [], [], [], [], ''); + $this->assertSame([], $req->getPayload()->all()); } /** From f66be2706075c5f6325d2fe2b743a57fb5d23f6b Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 22 Jun 2023 09:45:19 +0200 Subject: [PATCH 040/111] [Messenger] Preserve existing Doctrine schema --- Session/Storage/Handler/SessionHandlerFactory.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Session/Storage/Handler/SessionHandlerFactory.php b/Session/Storage/Handler/SessionHandlerFactory.php index 3e838d8a4..39dc30c6f 100644 --- a/Session/Storage/Handler/SessionHandlerFactory.php +++ b/Session/Storage/Handler/SessionHandlerFactory.php @@ -15,7 +15,6 @@ use Doctrine\DBAL\DriverManager; use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory; use Doctrine\DBAL\Tools\DsnParser; -use Doctrine\ORM\ORMSetup; use Symfony\Component\Cache\Adapter\AbstractAdapter; use Symfony\Component\Cache\Traits\RedisClusterProxy; use Symfony\Component\Cache\Traits\RedisProxy; @@ -77,7 +76,7 @@ public static function createHandler($connection): AbstractSessionHandler } $connection[3] = '-'; $params = class_exists(DsnParser::class) ? (new DsnParser())->parse($connection) : ['url' => $connection]; - $config = class_exists(ORMSetup::class) ? ORMSetup::createConfiguration(true) : new Configuration(); + $config = new Configuration(); if (class_exists(DefaultSchemaManagerFactory::class)) { $config->setSchemaManagerFactory(new DefaultSchemaManagerFactory()); } From b834230f373fe11d8b31a3df2d52e139d88451e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 30 Jun 2023 02:47:40 +0200 Subject: [PATCH 041/111] Remove ExpectDeprecationTrait where it is not used --- Tests/IpUtilsTest.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/Tests/IpUtilsTest.php b/Tests/IpUtilsTest.php index 8f6b869a1..ce93c69e9 100644 --- a/Tests/IpUtilsTest.php +++ b/Tests/IpUtilsTest.php @@ -12,13 +12,10 @@ namespace Symfony\Component\HttpFoundation\Tests; use PHPUnit\Framework\TestCase; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\HttpFoundation\IpUtils; class IpUtilsTest extends TestCase { - use ExpectDeprecationTrait; - public function testSeparateCachesPerProtocol() { $ip = '192.168.52.1'; From 0c29a601d9ad1aa5970e88fabb17503df9591109 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 19 Jul 2023 22:11:33 +0200 Subject: [PATCH 042/111] Fix deprecations on PHP 8.3 --- Tests/RequestTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/RequestTest.php b/Tests/RequestTest.php index e5360807b..a6d0b25b5 100644 --- a/Tests/RequestTest.php +++ b/Tests/RequestTest.php @@ -1882,7 +1882,7 @@ private function disableHttpMethodParameterOverride() $class = new \ReflectionClass(Request::class); $property = $class->getProperty('httpMethodParameterOverride'); $property->setAccessible(true); - $property->setValue(false); + $property->setValue(null, false); } private function getRequestInstanceForClientIpTests(string $remoteAddr, ?string $httpForwardedFor, ?array $trustedProxies): Request From 947abb150dae2e10e4129d3563e076b70c4066ee Mon Sep 17 00:00:00 2001 From: HypeMC Date: Wed, 19 Jul 2023 22:24:03 +0200 Subject: [PATCH 043/111] [HttpFoundation][HttpKernel] Fix deprecations when `Content-Type` is `null` --- Response.php | 2 +- Tests/ResponseTest.php | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Response.php b/Response.php index 59e974d62..23bfb2199 100644 --- a/Response.php +++ b/Response.php @@ -298,7 +298,7 @@ public function prepare(Request $request) $charset = $this->charset ?: 'UTF-8'; if (!$headers->has('Content-Type')) { $headers->set('Content-Type', 'text/html; charset='.$charset); - } elseif (0 === stripos($headers->get('Content-Type'), 'text/') && false === stripos($headers->get('Content-Type'), 'charset')) { + } elseif (0 === stripos($headers->get('Content-Type') ?? '', 'text/') && false === stripos($headers->get('Content-Type') ?? '', 'charset')) { // add the charset $headers->set('Content-Type', $headers->get('Content-Type').'; charset='.$charset); } diff --git a/Tests/ResponseTest.php b/Tests/ResponseTest.php index 50198c2d7..a78e359b6 100644 --- a/Tests/ResponseTest.php +++ b/Tests/ResponseTest.php @@ -511,6 +511,16 @@ public function testContentTypeCharset() $this->assertEquals('text/css; charset=UTF-8', $response->headers->get('Content-Type')); } + public function testContentTypeIsNull() + { + $response = new Response('foo'); + $response->headers->set('Content-Type', null); + + $response->prepare(new Request()); + + $this->expectNotToPerformAssertions(); + } + public function testPrepareDoesNothingIfContentTypeIsSet() { $response = new Response('foo'); From e7793151e99dc2ac1352ff3735d100fb3b3bfc08 Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Fri, 21 Jul 2023 13:30:15 +0200 Subject: [PATCH 044/111] Fix htaccess sample to use index.php instead of app.php --- ServerBag.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ServerBag.php b/ServerBag.php index 25688d523..004af5708 100644 --- a/ServerBag.php +++ b/ServerBag.php @@ -51,7 +51,7 @@ public function getHeaders() * RewriteCond %{HTTP:Authorization} .+ * RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0] * RewriteCond %{REQUEST_FILENAME} !-f - * RewriteRule ^(.*)$ app.php [QSA,L] + * RewriteRule ^(.*)$ index.php [QSA,L] */ $authorizationHeader = null; From 43afd1ed0f1679be4249cd023e2acb12881cd8f1 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 21 Jul 2023 15:28:24 +0200 Subject: [PATCH 045/111] Use typed properties in tests as much as possible --- Tests/File/FakeFile.php | 2 +- Tests/File/FileTest.php | 2 -- .../response-functional/deleted_cookie.php | 5 +--- .../MockAbstractRequestRateLimiter.php | 2 +- Tests/Session/Attribute/AttributeBagTest.php | 7 ++--- .../Session/Flash/AutoExpireFlashBagTest.php | 9 ++---- Tests/Session/Flash/FlashBagTest.php | 9 ++---- Tests/Session/SessionTest.php | 19 +++---------- .../AbstractRedisSessionHandlerTestCase.php | 19 ++----------- .../Handler/MarshallingSessionHandlerTest.php | 11 ++------ .../Handler/MemcachedSessionHandlerTest.php | 16 ++--------- .../Handler/MigratingSessionHandlerTest.php | 7 +++-- .../Handler/MongoDbSessionHandlerTest.php | 9 ++---- .../Storage/Handler/PdoSessionHandlerTest.php | 8 +++--- Tests/Session/Storage/MetadataBagTest.php | 15 ++-------- .../Storage/MockArraySessionStorageTest.php | 28 +++---------------- .../Storage/MockFileSessionStorageTest.php | 12 ++------ .../Storage/NativeSessionStorageTest.php | 4 +-- .../Storage/PhpBridgeSessionStorageTest.php | 4 +-- .../Storage/Proxy/AbstractProxyTest.php | 11 ++------ .../Storage/Proxy/SessionHandlerProxyTest.php | 17 ++--------- 21 files changed, 48 insertions(+), 168 deletions(-) diff --git a/Tests/File/FakeFile.php b/Tests/File/FakeFile.php index 8b2f12f41..9bac076ca 100644 --- a/Tests/File/FakeFile.php +++ b/Tests/File/FakeFile.php @@ -15,7 +15,7 @@ class FakeFile extends OrigFile { - private $realpath; + private string $realpath; public function __construct(string $realpath, string $path) { diff --git a/Tests/File/FileTest.php b/Tests/File/FileTest.php index fc806e951..65ce2308f 100644 --- a/Tests/File/FileTest.php +++ b/Tests/File/FileTest.php @@ -20,8 +20,6 @@ */ class FileTest extends TestCase { - protected $file; - public function testGetMimeTypeUsesMimeTypeGuessers() { $file = new File(__DIR__.'/Fixtures/test.gif'); diff --git a/Tests/Fixtures/response-functional/deleted_cookie.php b/Tests/Fixtures/response-functional/deleted_cookie.php index 003b0c121..6b54f6614 100644 --- a/Tests/Fixtures/response-functional/deleted_cookie.php +++ b/Tests/Fixtures/response-functional/deleted_cookie.php @@ -34,10 +34,7 @@ $listener = new SessionListener($container); $kernel = new class($r) implements HttpKernelInterface { - /** - * @var Response - */ - private $response; + private Response $response; public function __construct(Response $response) { diff --git a/Tests/RateLimiter/MockAbstractRequestRateLimiter.php b/Tests/RateLimiter/MockAbstractRequestRateLimiter.php index 0acc918bf..31f03c816 100644 --- a/Tests/RateLimiter/MockAbstractRequestRateLimiter.php +++ b/Tests/RateLimiter/MockAbstractRequestRateLimiter.php @@ -20,7 +20,7 @@ class MockAbstractRequestRateLimiter extends AbstractRequestRateLimiter /** * @var LimiterInterface[] */ - private $limiters; + private array $limiters; public function __construct(array $limiters) { diff --git a/Tests/Session/Attribute/AttributeBagTest.php b/Tests/Session/Attribute/AttributeBagTest.php index 273efddf1..afd281f0a 100644 --- a/Tests/Session/Attribute/AttributeBagTest.php +++ b/Tests/Session/Attribute/AttributeBagTest.php @@ -21,12 +21,9 @@ */ class AttributeBagTest extends TestCase { - private $array = []; + private array $array = []; - /** - * @var AttributeBag - */ - private $bag; + private ?AttributeBag $bag = null; protected function setUp(): void { diff --git a/Tests/Session/Flash/AutoExpireFlashBagTest.php b/Tests/Session/Flash/AutoExpireFlashBagTest.php index ba2687199..6a6510a57 100644 --- a/Tests/Session/Flash/AutoExpireFlashBagTest.php +++ b/Tests/Session/Flash/AutoExpireFlashBagTest.php @@ -21,12 +21,9 @@ */ class AutoExpireFlashBagTest extends TestCase { - /** - * @var \Symfony\Component\HttpFoundation\Session\Flash\AutoExpireFlashBag - */ - private $bag; + protected array $array = []; - protected $array = []; + private FlashBag $bag; protected function setUp(): void { @@ -38,7 +35,7 @@ protected function setUp(): void protected function tearDown(): void { - $this->bag = null; + unset($this->bag); parent::tearDown(); } diff --git a/Tests/Session/Flash/FlashBagTest.php b/Tests/Session/Flash/FlashBagTest.php index 24dbbfe98..59e3f1f0e 100644 --- a/Tests/Session/Flash/FlashBagTest.php +++ b/Tests/Session/Flash/FlashBagTest.php @@ -21,12 +21,9 @@ */ class FlashBagTest extends TestCase { - /** - * @var \Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface - */ - private $bag; + protected array $array = []; - protected $array = []; + private FlashBag $bag; protected function setUp(): void { @@ -38,7 +35,7 @@ protected function setUp(): void protected function tearDown(): void { - $this->bag = null; + unset($this->bag); parent::tearDown(); } diff --git a/Tests/Session/SessionTest.php b/Tests/Session/SessionTest.php index 56011ddb5..56ef60806 100644 --- a/Tests/Session/SessionTest.php +++ b/Tests/Session/SessionTest.php @@ -17,8 +17,10 @@ use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface; use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\SessionBagProxy; +use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag; use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; +use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface; /** * SessionTest. @@ -29,15 +31,8 @@ */ class SessionTest extends TestCase { - /** - * @var \Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface - */ - protected $storage; - - /** - * @var \Symfony\Component\HttpFoundation\Session\SessionInterface - */ - protected $session; + protected SessionStorageInterface $storage; + protected SessionInterface $session; protected function setUp(): void { @@ -45,12 +40,6 @@ protected function setUp(): void $this->session = new Session($this->storage, new AttributeBag(), new FlashBag()); } - protected function tearDown(): void - { - $this->storage = null; - $this->session = null; - } - public function testStart() { $this->assertEquals('', $this->session->getId()); diff --git a/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php b/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php index 52f8a4cb0..4df1553c8 100644 --- a/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php +++ b/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php @@ -24,15 +24,8 @@ abstract class AbstractRedisSessionHandlerTestCase extends TestCase { protected const PREFIX = 'prefix_'; - /** - * @var RedisSessionHandler - */ - protected $storage; - - /** - * @var \Redis|\RedisArray|\RedisCluster|\Predis\Client - */ - protected $redisClient; + protected RedisSessionHandler $storage; + protected \Redis|Relay|\RedisArray|\RedisCluster|\Predis\Client $redisClient; abstract protected function createRedisClient(string $host): \Redis|Relay|\RedisArray|\RedisCluster|\Predis\Client; @@ -58,14 +51,6 @@ protected function setUp(): void ); } - protected function tearDown(): void - { - $this->redisClient = null; - $this->storage = null; - - parent::tearDown(); - } - public function testOpenSession() { $this->assertTrue($this->storage->open('', '')); diff --git a/Tests/Session/Storage/Handler/MarshallingSessionHandlerTest.php b/Tests/Session/Storage/Handler/MarshallingSessionHandlerTest.php index 7216cdd1e..894a71589 100644 --- a/Tests/Session/Storage/Handler/MarshallingSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/MarshallingSessionHandlerTest.php @@ -22,15 +22,8 @@ */ class MarshallingSessionHandlerTest extends TestCase { - /** - * @var MockObject|\SessionHandlerInterface - */ - protected $handler; - - /** - * @var MockObject|MarshallerInterface - */ - protected $marshaller; + protected MockObject&\SessionHandlerInterface $handler; + protected MockObject&MarshallerInterface $marshaller; protected function setUp(): void { diff --git a/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php b/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php index 0b25c37d9..379fcb0d1 100644 --- a/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Storage\Handler\MemcachedSessionHandler; @@ -24,12 +25,8 @@ class MemcachedSessionHandlerTest extends TestCase private const PREFIX = 'prefix_'; private const TTL = 1000; - /** - * @var MemcachedSessionHandler - */ - protected $storage; - - protected $memcached; + protected MemcachedSessionHandler $storage; + protected MockObject&\Memcached $memcached; protected function setUp(): void { @@ -54,13 +51,6 @@ protected function setUp(): void ); } - protected function tearDown(): void - { - $this->memcached = null; - $this->storage = null; - parent::tearDown(); - } - public function testOpenSession() { $this->assertTrue($this->storage->open('', '')); diff --git a/Tests/Session/Storage/Handler/MigratingSessionHandlerTest.php b/Tests/Session/Storage/Handler/MigratingSessionHandlerTest.php index f56f753af..eb988dfd6 100644 --- a/Tests/Session/Storage/Handler/MigratingSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/MigratingSessionHandlerTest.php @@ -11,14 +11,15 @@ namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Storage\Handler\MigratingSessionHandler; class MigratingSessionHandlerTest extends TestCase { - private $dualHandler; - private $currentHandler; - private $writeOnlyHandler; + private MigratingSessionHandler $dualHandler; + private MockObject&\SessionHandlerInterface $currentHandler; + private MockObject&\SessionHandlerInterface $writeOnlyHandler; protected function setUp(): void { diff --git a/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php b/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php index 30e8cc0f9..c37f0c3af 100644 --- a/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php @@ -25,12 +25,9 @@ */ class MongoDbSessionHandlerTest extends TestCase { - /** - * @var MockObject&Client - */ - private $mongo; - private $storage; - public $options; + public array $options; + private MockObject&Client $mongo; + private MongoDbSessionHandler $storage; protected function setUp(): void { diff --git a/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php b/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php index ce8e77874..cd34c72e3 100644 --- a/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php @@ -22,7 +22,7 @@ */ class PdoSessionHandlerTest extends TestCase { - private $dbFile; + private ?string $dbFile = null; protected function tearDown(): void { @@ -404,9 +404,9 @@ private function createStream($content) class MockPdo extends \PDO { - public $prepareResult; - private $driverName; - private $errorMode; + public \Closure|\PDOStatement|false $prepareResult; + private ?string $driverName; + private bool|int $errorMode; public function __construct(string $driverName = null, int $errorMode = null) { diff --git a/Tests/Session/Storage/MetadataBagTest.php b/Tests/Session/Storage/MetadataBagTest.php index 51a1b6472..b2f3de42b 100644 --- a/Tests/Session/Storage/MetadataBagTest.php +++ b/Tests/Session/Storage/MetadataBagTest.php @@ -21,12 +21,8 @@ */ class MetadataBagTest extends TestCase { - /** - * @var MetadataBag - */ - protected $bag; - - protected $array = []; + protected MetadataBag $bag; + protected array $array = []; protected function setUp(): void { @@ -36,13 +32,6 @@ protected function setUp(): void $this->bag->initialize($this->array); } - protected function tearDown(): void - { - $this->array = []; - $this->bag = null; - parent::tearDown(); - } - public function testInitialize() { $sessionMetadata = []; diff --git a/Tests/Session/Storage/MockArraySessionStorageTest.php b/Tests/Session/Storage/MockArraySessionStorageTest.php index 2428e9fc2..0fc991058 100644 --- a/Tests/Session/Storage/MockArraySessionStorageTest.php +++ b/Tests/Session/Storage/MockArraySessionStorageTest.php @@ -23,22 +23,10 @@ */ class MockArraySessionStorageTest extends TestCase { - /** - * @var MockArraySessionStorage - */ - private $storage; - - /** - * @var AttributeBag - */ - private $attributes; - - /** - * @var FlashBag - */ - private $flashes; - - private $data; + private MockArraySessionStorage $storage; + private AttributeBag $attributes; + private FlashBag $flashes; + private array $data; protected function setUp(): void { @@ -56,14 +44,6 @@ protected function setUp(): void $this->storage->setSessionData($this->data); } - protected function tearDown(): void - { - $this->data = null; - $this->flashes = null; - $this->attributes = null; - $this->storage = null; - } - public function testStart() { $this->assertEquals('', $this->storage->getId()); diff --git a/Tests/Session/Storage/MockFileSessionStorageTest.php b/Tests/Session/Storage/MockFileSessionStorageTest.php index 3f994cb2a..61804c268 100644 --- a/Tests/Session/Storage/MockFileSessionStorageTest.php +++ b/Tests/Session/Storage/MockFileSessionStorageTest.php @@ -23,15 +23,9 @@ */ class MockFileSessionStorageTest extends TestCase { - /** - * @var string - */ - private $sessionDir; + protected MockFileSessionStorage $storage; - /** - * @var MockFileSessionStorage - */ - protected $storage; + private string $sessionDir; protected function setUp(): void { @@ -45,8 +39,6 @@ protected function tearDown(): void if (is_dir($this->sessionDir)) { @rmdir($this->sessionDir); } - $this->sessionDir = null; - $this->storage = null; } public function testStart() diff --git a/Tests/Session/Storage/NativeSessionStorageTest.php b/Tests/Session/Storage/NativeSessionStorageTest.php index 9234e2b40..a59c8de5f 100644 --- a/Tests/Session/Storage/NativeSessionStorageTest.php +++ b/Tests/Session/Storage/NativeSessionStorageTest.php @@ -32,7 +32,7 @@ */ class NativeSessionStorageTest extends TestCase { - private $savePath; + private string $savePath; protected function setUp(): void { @@ -50,8 +50,6 @@ protected function tearDown(): void if (is_dir($this->savePath)) { @rmdir($this->savePath); } - - $this->savePath = null; } protected function getStorage(array $options = []): NativeSessionStorage diff --git a/Tests/Session/Storage/PhpBridgeSessionStorageTest.php b/Tests/Session/Storage/PhpBridgeSessionStorageTest.php index e2fb93ebc..5a777be9c 100644 --- a/Tests/Session/Storage/PhpBridgeSessionStorageTest.php +++ b/Tests/Session/Storage/PhpBridgeSessionStorageTest.php @@ -28,7 +28,7 @@ */ class PhpBridgeSessionStorageTest extends TestCase { - private $savePath; + private string $savePath; protected function setUp(): void { @@ -46,8 +46,6 @@ protected function tearDown(): void if (is_dir($this->savePath)) { @rmdir($this->savePath); } - - $this->savePath = null; } protected function getStorage(): PhpBridgeSessionStorage diff --git a/Tests/Session/Storage/Proxy/AbstractProxyTest.php b/Tests/Session/Storage/Proxy/AbstractProxyTest.php index fde7a4a0a..0d9eb56ae 100644 --- a/Tests/Session/Storage/Proxy/AbstractProxyTest.php +++ b/Tests/Session/Storage/Proxy/AbstractProxyTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Proxy; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy; use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy; @@ -22,21 +23,13 @@ */ class AbstractProxyTest extends TestCase { - /** - * @var AbstractProxy - */ - protected $proxy; + protected MockObject&AbstractProxy $proxy; protected function setUp(): void { $this->proxy = $this->getMockForAbstractClass(AbstractProxy::class); } - protected function tearDown(): void - { - $this->proxy = null; - } - public function testGetSaveHandlerName() { $this->assertNull($this->proxy->getSaveHandlerName()); diff --git a/Tests/Session/Storage/Proxy/SessionHandlerProxyTest.php b/Tests/Session/Storage/Proxy/SessionHandlerProxyTest.php index eed23fe0b..d9c4974ef 100644 --- a/Tests/Session/Storage/Proxy/SessionHandlerProxyTest.php +++ b/Tests/Session/Storage/Proxy/SessionHandlerProxyTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Proxy; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler; use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; @@ -27,15 +28,9 @@ */ class SessionHandlerProxyTest extends TestCase { - /** - * @var \PHPUnit\Framework\MockObject\Matcher - */ - private $mock; + private MockObject&\SessionHandlerInterface $mock; - /** - * @var SessionHandlerProxy - */ - private $proxy; + private SessionHandlerProxy $proxy; protected function setUp(): void { @@ -43,12 +38,6 @@ protected function setUp(): void $this->proxy = new SessionHandlerProxy($this->mock); } - protected function tearDown(): void - { - $this->mock = null; - $this->proxy = null; - } - public function testOpenTrue() { $this->mock->expects($this->once()) From 3470e2867a67279b7915d271ff4e194390bda831 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 21 Jul 2023 18:41:43 +0200 Subject: [PATCH 046/111] Add types to private and internal properties --- Request.php | 2 +- Response.php | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/Request.php b/Request.php index 0bef6f8d7..776449c86 100644 --- a/Request.php +++ b/Request.php @@ -766,7 +766,7 @@ public function setSession(SessionInterface $session) */ public function setSessionFactory(callable $factory): void { - $this->session = $factory; + $this->session = $factory(...); } /** diff --git a/Response.php b/Response.php index abb47583c..6acf11f09 100644 --- a/Response.php +++ b/Response.php @@ -503,12 +503,6 @@ public function setStatusCode(int $code, string $text = null): static return $this; } - if (false === $text) { - $this->statusText = ''; - - return $this; - } - $this->statusText = $text; return $this; From 6ebf2e86b61febe4464551aade4fdb1a54faa7a4 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 26 Jul 2023 17:12:55 +0200 Subject: [PATCH 047/111] More short closures + isset instead of null checks + etc. --- BinaryFileResponse.php | 2 +- Request.php | 80 ++++++++++++++++++------------------------ StreamedResponse.php | 6 ++-- 3 files changed, 38 insertions(+), 50 deletions(-) diff --git a/BinaryFileResponse.php b/BinaryFileResponse.php index 71e806fc1..ca18c92f1 100644 --- a/BinaryFileResponse.php +++ b/BinaryFileResponse.php @@ -293,7 +293,7 @@ public function sendContent(): static { try { if (!$this->isSuccessful()) { - return parent::sendContent(); + return $this; } if (0 === $this->maxlen) { diff --git a/Request.php b/Request.php index 776449c86..285e4db79 100644 --- a/Request.php +++ b/Request.php @@ -279,16 +279,16 @@ public function initialize(array $query = [], array $request = [], array $attrib $this->headers = new HeaderBag($this->server->getHeaders()); $this->content = $content; - $this->languages = null; - $this->charsets = null; - $this->encodings = null; - $this->acceptableContentTypes = null; - $this->pathInfo = null; - $this->requestUri = null; - $this->baseUrl = null; - $this->basePath = null; - $this->method = null; - $this->format = null; + unset($this->languages); + unset($this->charsets); + unset($this->encodings); + unset($this->acceptableContentTypes); + unset($this->pathInfo); + unset($this->requestUri); + unset($this->baseUrl); + unset($this->basePath); + unset($this->method); + unset($this->format); } /** @@ -465,16 +465,16 @@ public function duplicate(array $query = null, array $request = null, array $att $dup->server = new ServerBag($server); $dup->headers = new HeaderBag($dup->server->getHeaders()); } - $dup->languages = null; - $dup->charsets = null; - $dup->encodings = null; - $dup->acceptableContentTypes = null; - $dup->pathInfo = null; - $dup->requestUri = null; - $dup->baseUrl = null; - $dup->basePath = null; - $dup->method = null; - $dup->format = null; + unset($dup->languages); + unset($dup->charsets); + unset($dup->encodings); + unset($dup->acceptableContentTypes); + unset($dup->pathInfo); + unset($dup->requestUri); + unset($dup->baseUrl); + unset($dup->basePath); + unset($dup->method); + unset($dup->format); if (!$dup->get('_format') && $this->get('_format')) { $dup->attributes->set('_format', $this->get('_format')); @@ -1179,7 +1179,7 @@ public function getHost(): string */ public function setMethod(string $method) { - $this->method = null; + unset($this->method); $this->server->set('REQUEST_METHOD', $method); } @@ -1198,7 +1198,7 @@ public function setMethod(string $method) */ public function getMethod(): string { - if (null !== $this->method) { + if (isset($this->method)) { return $this->method; } @@ -1246,7 +1246,7 @@ public function getRealMethod(): string */ public function getMimeType(string $format): ?string { - if (null === static::$formats) { + if (!isset(static::$formats)) { static::initializeFormats(); } @@ -1260,7 +1260,7 @@ public function getMimeType(string $format): ?string */ public static function getMimeTypes(string $format): array { - if (null === static::$formats) { + if (!isset(static::$formats)) { static::initializeFormats(); } @@ -1277,7 +1277,7 @@ public function getFormat(?string $mimeType): ?string $canonicalMimeType = trim(substr($mimeType, 0, $pos)); } - if (null === static::$formats) { + if (!isset(static::$formats)) { static::initializeFormats(); } @@ -1302,7 +1302,7 @@ public function getFormat(?string $mimeType): ?string */ public function setFormat(?string $format, string|array $mimeTypes) { - if (null === static::$formats) { + if (!isset(static::$formats)) { static::initializeFormats(); } @@ -1583,13 +1583,13 @@ public function isNoCache(): bool */ public function getPreferredFormat(?string $default = 'html'): ?string { - if (null !== $this->preferredFormat || null !== $this->preferredFormat = $this->getRequestFormat(null)) { - return $this->preferredFormat; + if (isset($this->preferredFormat) || null !== $preferredFormat = $this->getRequestFormat(null)) { + return $this->preferredFormat ??= $preferredFormat; } foreach ($this->getAcceptableContentTypes() as $mimeType) { - if ($this->preferredFormat = $this->getFormat($mimeType)) { - return $this->preferredFormat; + if ($preferredFormat = $this->getFormat($mimeType)) { + return $this->preferredFormat = $preferredFormat; } } @@ -1636,7 +1636,7 @@ public function getPreferredLanguage(array $locales = null): ?string */ public function getLanguages(): array { - if (null !== $this->languages) { + if (isset($this->languages)) { return $this->languages; } @@ -1677,11 +1677,7 @@ public function getLanguages(): array */ public function getCharsets(): array { - if (null !== $this->charsets) { - return $this->charsets; - } - - return $this->charsets = array_map('strval', array_keys(AcceptHeader::fromString($this->headers->get('Accept-Charset'))->all())); + return $this->charsets ??= array_map('strval', array_keys(AcceptHeader::fromString($this->headers->get('Accept-Charset'))->all())); } /** @@ -1691,11 +1687,7 @@ public function getCharsets(): array */ public function getEncodings(): array { - if (null !== $this->encodings) { - return $this->encodings; - } - - return $this->encodings = array_map('strval', array_keys(AcceptHeader::fromString($this->headers->get('Accept-Encoding'))->all())); + return $this->encodings ??= array_map('strval', array_keys(AcceptHeader::fromString($this->headers->get('Accept-Encoding'))->all())); } /** @@ -1705,11 +1697,7 @@ public function getEncodings(): array */ public function getAcceptableContentTypes(): array { - if (null !== $this->acceptableContentTypes) { - return $this->acceptableContentTypes; - } - - return $this->acceptableContentTypes = array_map('strval', array_keys(AcceptHeader::fromString($this->headers->get('Accept'))->all())); + return $this->acceptableContentTypes ??= array_map('strval', array_keys(AcceptHeader::fromString($this->headers->get('Accept'))->all())); } /** diff --git a/StreamedResponse.php b/StreamedResponse.php index 2c8ff15f3..7f54783ec 100644 --- a/StreamedResponse.php +++ b/StreamedResponse.php @@ -51,7 +51,7 @@ public function __construct(callable $callback = null, int $status = 200, array */ public function setCallback(callable $callback): static { - $this->callback = $callback; + $this->callback = $callback(...); return $this; } @@ -90,8 +90,8 @@ public function sendContent(): static $this->streamed = true; - if (null === $this->callback) { - throw new \LogicException('The Response callback must not be null.'); + if (!isset($this->callback)) { + throw new \LogicException('The Response callback must be set.'); } ($this->callback)(); From 6f241289b39bff5312b136723ce4913334c4694e Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 27 Jul 2023 11:02:28 +0200 Subject: [PATCH 048/111] Ensure all properties have a type --- Tests/ResponseFunctionalTest.php | 1 + Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php | 1 + 2 files changed, 2 insertions(+) diff --git a/Tests/ResponseFunctionalTest.php b/Tests/ResponseFunctionalTest.php index 841b7a50f..c89adcd3c 100644 --- a/Tests/ResponseFunctionalTest.php +++ b/Tests/ResponseFunctionalTest.php @@ -16,6 +16,7 @@ class ResponseFunctionalTest extends TestCase { + /** @var resource|false */ private static $server; public static function setUpBeforeClass(): void diff --git a/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php b/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php index fa5119cf3..aabeba900 100644 --- a/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php @@ -16,6 +16,7 @@ class AbstractSessionHandlerTest extends TestCase { + /** @var resource|false */ private static $server; public static function setUpBeforeClass(): void From b3d3fceca8931c7ef54528f407db72bd16a83075 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 28 Jul 2023 16:03:16 +0200 Subject: [PATCH 049/111] Use Stringable interface as much as possible --- HeaderBag.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HeaderBag.php b/HeaderBag.php index c49a23972..d0c415a84 100644 --- a/HeaderBag.php +++ b/HeaderBag.php @@ -18,7 +18,7 @@ * * @implements \IteratorAggregate> */ -class HeaderBag implements \IteratorAggregate, \Countable +class HeaderBag implements \IteratorAggregate, \Countable, \Stringable { protected const UPPER = '_ABCDEFGHIJKLMNOPQRSTUVWXYZ'; protected const LOWER = '-abcdefghijklmnopqrstuvwxyz'; From 0ffd65a1de66f3e13ce01b8866ba58722a6575ae Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 3 Aug 2023 14:18:46 +0200 Subject: [PATCH 050/111] [HttpFoundation] Use Symfony exception for request unexpected values --- Exception/BadRequestException.php | 2 +- Exception/ConflictingHeadersException.php | 2 +- Exception/JsonException.php | 2 +- Exception/SuspiciousOperationException.php | 2 +- Exception/UnexpectedValueException.php | 16 ++++++++++++++++ InputBag.php | 3 ++- ParameterBag.php | 9 +++++---- Tests/ParameterBagTest.php | 15 ++++++++------- 8 files changed, 35 insertions(+), 16 deletions(-) create mode 100644 Exception/UnexpectedValueException.php diff --git a/Exception/BadRequestException.php b/Exception/BadRequestException.php index e4bb309c4..505e1cfde 100644 --- a/Exception/BadRequestException.php +++ b/Exception/BadRequestException.php @@ -14,6 +14,6 @@ /** * Raised when a user sends a malformed request. */ -class BadRequestException extends \UnexpectedValueException implements RequestExceptionInterface +class BadRequestException extends UnexpectedValueException implements RequestExceptionInterface { } diff --git a/Exception/ConflictingHeadersException.php b/Exception/ConflictingHeadersException.php index 5fcf5b426..77aa0e1ee 100644 --- a/Exception/ConflictingHeadersException.php +++ b/Exception/ConflictingHeadersException.php @@ -16,6 +16,6 @@ * * @author Magnus Nordlander */ -class ConflictingHeadersException extends \UnexpectedValueException implements RequestExceptionInterface +class ConflictingHeadersException extends UnexpectedValueException implements RequestExceptionInterface { } diff --git a/Exception/JsonException.php b/Exception/JsonException.php index 5990e760e..6d1e0aecb 100644 --- a/Exception/JsonException.php +++ b/Exception/JsonException.php @@ -16,6 +16,6 @@ * * @author Tobias Nyholm */ -final class JsonException extends \UnexpectedValueException implements RequestExceptionInterface +final class JsonException extends UnexpectedValueException implements RequestExceptionInterface { } diff --git a/Exception/SuspiciousOperationException.php b/Exception/SuspiciousOperationException.php index ae7a5f133..4818ef2c8 100644 --- a/Exception/SuspiciousOperationException.php +++ b/Exception/SuspiciousOperationException.php @@ -15,6 +15,6 @@ * Raised when a user has performed an operation that should be considered * suspicious from a security perspective. */ -class SuspiciousOperationException extends \UnexpectedValueException implements RequestExceptionInterface +class SuspiciousOperationException extends UnexpectedValueException implements RequestExceptionInterface { } diff --git a/Exception/UnexpectedValueException.php b/Exception/UnexpectedValueException.php new file mode 100644 index 000000000..c3e6c9d6d --- /dev/null +++ b/Exception/UnexpectedValueException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Exception; + +class UnexpectedValueException extends \UnexpectedValueException +{ +} diff --git a/InputBag.php b/InputBag.php index 77990f571..7676d9fe7 100644 --- a/InputBag.php +++ b/InputBag.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpFoundation; use Symfony\Component\HttpFoundation\Exception\BadRequestException; +use Symfony\Component\HttpFoundation\Exception\UnexpectedValueException; /** * InputBag is a container for user input values such as $_GET, $_POST, $_REQUEST, and $_COOKIE. @@ -87,7 +88,7 @@ public function getEnum(string $key, string $class, \BackedEnum $default = null) { try { return parent::getEnum($key, $class, $default); - } catch (\UnexpectedValueException $e) { + } catch (UnexpectedValueException $e) { throw new BadRequestException($e->getMessage(), $e->getCode(), $e); } } diff --git a/ParameterBag.php b/ParameterBag.php index 9d7012de3..0456e474c 100644 --- a/ParameterBag.php +++ b/ParameterBag.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpFoundation; use Symfony\Component\HttpFoundation\Exception\BadRequestException; +use Symfony\Component\HttpFoundation\Exception\UnexpectedValueException; /** * ParameterBag is a container for key/value pairs. @@ -140,7 +141,7 @@ public function getString(string $key, string $default = ''): string { $value = $this->get($key, $default); if (!\is_scalar($value) && !$value instanceof \Stringable) { - throw new \UnexpectedValueException(sprintf('Parameter value "%s" cannot be converted to "string".', $key)); + throw new UnexpectedValueException(sprintf('Parameter value "%s" cannot be converted to "string".', $key)); } return (string) $value; @@ -184,7 +185,7 @@ public function getEnum(string $key, string $class, \BackedEnum $default = null) try { return $class::from($value); } catch (\ValueError|\TypeError $e) { - throw new \UnexpectedValueException(sprintf('Parameter "%s" cannot be converted to enum: %s.', $key, $e->getMessage()), $e->getCode(), $e); + throw new UnexpectedValueException(sprintf('Parameter "%s" cannot be converted to enum: %s.', $key, $e->getMessage()), $e->getCode(), $e); } } @@ -211,7 +212,7 @@ public function filter(string $key, mixed $default = null, int $filter = \FILTER } if (\is_object($value) && !$value instanceof \Stringable) { - throw new \UnexpectedValueException(sprintf('Parameter value "%s" cannot be filtered.', $key)); + throw new UnexpectedValueException(sprintf('Parameter value "%s" cannot be filtered.', $key)); } if ((\FILTER_CALLBACK & $filter) && !(($options['options'] ?? null) instanceof \Closure)) { @@ -232,7 +233,7 @@ public function filter(string $key, mixed $default = null, int $filter = \FILTER $method = ($method['object'] ?? null) === $this ? $method['function'] : 'filter'; $hint = 'filter' === $method ? 'pass' : 'use method "filter()" with'; - trigger_deprecation('symfony/http-foundation', '6.3', 'Ignoring invalid values when using "%s::%s(\'%s\')" is deprecated and will throw an "%s" in 7.0; '.$hint.' flag "FILTER_NULL_ON_FAILURE" to keep ignoring them.', $this::class, $method, $key, \UnexpectedValueException::class); + trigger_deprecation('symfony/http-foundation', '6.3', 'Ignoring invalid values when using "%s::%s(\'%s\')" is deprecated and will throw an "%s" in 7.0; '.$hint.' flag "FILTER_NULL_ON_FAILURE" to keep ignoring them.', $this::class, $method, $key, UnexpectedValueException::class); return false; } diff --git a/Tests/ParameterBagTest.php b/Tests/ParameterBagTest.php index 62b95f42f..dd946aa60 100644 --- a/Tests/ParameterBagTest.php +++ b/Tests/ParameterBagTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\HttpFoundation\Exception\BadRequestException; +use Symfony\Component\HttpFoundation\Exception\UnexpectedValueException; use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\Tests\Fixtures\FooEnum; @@ -128,7 +129,7 @@ public function testGetAlphaExceptionWithArray() { $bag = new ParameterBag(['word' => ['foo_BAR_012']]); - $this->expectException(\UnexpectedValueException::class); + $this->expectException(UnexpectedValueException::class); $this->expectExceptionMessage('Parameter value "word" cannot be converted to "string".'); $bag->getAlpha('word'); @@ -149,7 +150,7 @@ public function testGetAlnumExceptionWithArray() { $bag = new ParameterBag(['word' => ['foo_BAR_012']]); - $this->expectException(\UnexpectedValueException::class); + $this->expectException(UnexpectedValueException::class); $this->expectExceptionMessage('Parameter value "word" cannot be converted to "string".'); $bag->getAlnum('word'); @@ -170,7 +171,7 @@ public function testGetDigitsExceptionWithArray() { $bag = new ParameterBag(['word' => ['foo_BAR_012']]); - $this->expectException(\UnexpectedValueException::class); + $this->expectException(UnexpectedValueException::class); $this->expectExceptionMessage('Parameter value "word" cannot be converted to "string".'); $bag->getDigits('word'); @@ -232,7 +233,7 @@ public function testGetStringExceptionWithArray() { $bag = new ParameterBag(['key' => ['abc']]); - $this->expectException(\UnexpectedValueException::class); + $this->expectException(UnexpectedValueException::class); $this->expectExceptionMessage('Parameter value "key" cannot be converted to "string".'); $bag->getString('key'); @@ -242,7 +243,7 @@ public function testGetStringExceptionWithObject() { $bag = new ParameterBag(['object' => $this]); - $this->expectException(\UnexpectedValueException::class); + $this->expectException(UnexpectedValueException::class); $this->expectExceptionMessage('Parameter value "object" cannot be converted to "string".'); $bag->getString('object'); @@ -359,7 +360,7 @@ public function testGetEnumThrowsExceptionWithNotBackingValue() { $bag = new ParameterBag(['invalid-value' => 2]); - $this->expectException(\UnexpectedValueException::class); + $this->expectException(UnexpectedValueException::class); if (\PHP_VERSION_ID >= 80200) { $this->expectExceptionMessage('Parameter "invalid-value" cannot be converted to enum: 2 is not a valid backing value for enum Symfony\Component\HttpFoundation\Tests\Fixtures\FooEnum.'); } else { @@ -373,7 +374,7 @@ public function testGetEnumThrowsExceptionWithInvalidValueType() { $bag = new ParameterBag(['invalid-value' => ['foo']]); - $this->expectException(\UnexpectedValueException::class); + $this->expectException(UnexpectedValueException::class); $this->expectExceptionMessage('Parameter "invalid-value" cannot be converted to enum: Symfony\Component\HttpFoundation\Tests\Fixtures\FooEnum::from(): Argument #1 ($value) must be of type int, array given.'); $this->assertNull($bag->getEnum('invalid-value', FooEnum::class)); From f88645a1f5e1500b628f810cdde7dcb6418aee80 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 4 Aug 2023 11:42:13 +0200 Subject: [PATCH 051/111] fix tests --- Tests/ParameterBagTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/ParameterBagTest.php b/Tests/ParameterBagTest.php index dd946aa60..b12946a3b 100644 --- a/Tests/ParameterBagTest.php +++ b/Tests/ParameterBagTest.php @@ -192,7 +192,7 @@ public function testGetInt() */ public function testGetIntExceptionWithArray() { - $this->expectDeprecation('Since symfony/http-foundation 6.3: Ignoring invalid values when using "Symfony\Component\HttpFoundation\ParameterBag::getInt(\'digits\')" is deprecated and will throw an "UnexpectedValueException" in 7.0; use method "filter()" with flag "FILTER_NULL_ON_FAILURE" to keep ignoring them.'); + $this->expectDeprecation(sprintf('Since symfony/http-foundation 6.3: Ignoring invalid values when using "%s::getInt(\'digits\')" is deprecated and will throw an "%s" in 7.0; use method "filter()" with flag "FILTER_NULL_ON_FAILURE" to keep ignoring them.', ParameterBag::class, UnexpectedValueException::class)); $bag = new ParameterBag(['digits' => ['123']]); $result = $bag->getInt('digits', 0); @@ -204,7 +204,7 @@ public function testGetIntExceptionWithArray() */ public function testGetIntExceptionWithInvalid() { - $this->expectDeprecation('Since symfony/http-foundation 6.3: Ignoring invalid values when using "Symfony\Component\HttpFoundation\ParameterBag::getInt(\'word\')" is deprecated and will throw an "UnexpectedValueException" in 7.0; use method "filter()" with flag "FILTER_NULL_ON_FAILURE" to keep ignoring them.'); + $this->expectDeprecation(sprintf('Since symfony/http-foundation 6.3: Ignoring invalid values when using "%s::getInt(\'word\')" is deprecated and will throw an "%s" in 7.0; use method "filter()" with flag "FILTER_NULL_ON_FAILURE" to keep ignoring them.', ParameterBag::class, UnexpectedValueException::class)); $bag = new ParameterBag(['word' => 'foo_BAR_012']); $result = $bag->getInt('word', 0); @@ -339,7 +339,7 @@ public function testGetBoolean() */ public function testGetBooleanExceptionWithInvalid() { - $this->expectDeprecation('Since symfony/http-foundation 6.3: Ignoring invalid values when using "Symfony\Component\HttpFoundation\ParameterBag::getBoolean(\'invalid\')" is deprecated and will throw an "UnexpectedValueException" in 7.0; use method "filter()" with flag "FILTER_NULL_ON_FAILURE" to keep ignoring them.'); + $this->expectDeprecation(sprintf('Since symfony/http-foundation 6.3: Ignoring invalid values when using "%s::getBoolean(\'invalid\')" is deprecated and will throw an "%s" in 7.0; use method "filter()" with flag "FILTER_NULL_ON_FAILURE" to keep ignoring them.', ParameterBag::class, UnexpectedValueException::class)); $bag = new ParameterBag(['invalid' => 'foo']); $result = $bag->getBoolean('invalid', 0); From da86b7d1a905139261ed7a73fef344c9e7bece4d Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 12 Aug 2023 18:38:40 +0200 Subject: [PATCH 052/111] [HttpFoundation] Add a slightly more verbose comment about a warning on UploadedFile --- File/UploadedFile.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/File/UploadedFile.php b/File/UploadedFile.php index fcc629913..1161556c4 100644 --- a/File/UploadedFile.php +++ b/File/UploadedFile.php @@ -74,7 +74,7 @@ public function __construct(string $path, string $originalName, string $mimeType * Returns the original file name. * * It is extracted from the request from which the file has been uploaded. - * Then it should not be considered as a safe value. + * This should not be considered as a safe value to use for a file name on your servers. * * @return string */ @@ -87,7 +87,7 @@ public function getClientOriginalName() * Returns the original file extension. * * It is extracted from the original file name that was uploaded. - * Then it should not be considered as a safe value. + * This should not be considered as a safe value to use for a file name on your servers. * * @return string */ From cded17582714f82a7374826c49665d04e13f969f Mon Sep 17 00:00:00 2001 From: Ismail Turan Date: Fri, 28 Jul 2023 08:26:11 +0200 Subject: [PATCH 053/111] [HttpKernel] Fix missing Request in RequestStack for StreamedResponse --- StreamedResponse.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/StreamedResponse.php b/StreamedResponse.php index 2c8ff15f3..5c7817e3c 100644 --- a/StreamedResponse.php +++ b/StreamedResponse.php @@ -56,6 +56,11 @@ public function setCallback(callable $callback): static return $this; } + public function getCallback(): \Closure + { + return ($this->callback)(...); + } + /** * This method only sends the headers once. * From 365992c83a836dfe635f1e903ccca43ee03d3dd2 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Fri, 18 Aug 2023 12:46:26 +0200 Subject: [PATCH 054/111] [HttpFoundation] Fix base URI detection on IIS with UrlRewriteModule --- Request.php | 30 ++++++++++++++++++++--- Tests/RequestTest.php | 56 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/Request.php b/Request.php index 28cebad16..f8e342154 100644 --- a/Request.php +++ b/Request.php @@ -246,6 +246,9 @@ class Request self::HEADER_X_FORWARDED_PREFIX => 'X_FORWARDED_PREFIX', ]; + /** @var bool */ + private $isIisRewrite = false; + /** * @param array $query The GET parameters * @param array $request The POST parameters @@ -1805,11 +1808,10 @@ protected function prepareRequestUri() { $requestUri = ''; - if ('1' == $this->server->get('IIS_WasUrlRewritten') && '' != $this->server->get('UNENCODED_URL')) { + if ($this->isIisRewrite() && '' != $this->server->get('UNENCODED_URL')) { // IIS7 with URL Rewrite: make sure we get the unencoded URL (double slash problem) $requestUri = $this->server->get('UNENCODED_URL'); $this->server->remove('UNENCODED_URL'); - $this->server->remove('IIS_WasUrlRewritten'); } elseif ($this->server->has('REQUEST_URI')) { $requestUri = $this->server->get('REQUEST_URI'); @@ -2012,7 +2014,13 @@ private function setPhpDefaultLocale(string $locale): void */ private function getUrlencodedPrefix(string $string, string $prefix): ?string { - if (!str_starts_with(rawurldecode($string), $prefix)) { + if ($this->isIisRewrite()) { + // ISS with UrlRewriteModule might report SCRIPT_NAME/PHP_SELF with wrong case + // see https://github.com/php/php-src/issues/11981 + if (0 !== stripos(rawurldecode($string), $prefix)) { + return null; + } + } elseif (!str_starts_with(rawurldecode($string), $prefix)) { return null; } @@ -2145,4 +2153,20 @@ private function normalizeAndFilterClientIps(array $clientIps, string $ip): arra // Now the IP chain contains only untrusted proxies and the client IP return $clientIps ? array_reverse($clientIps) : [$firstTrustedIp]; } + + /** + * Is this IIS with UrlRewriteModule? + * + * This method consumes, caches and removed the IIS_WasUrlRewritten env var, + * so we don't inherit it to sub-requests. + */ + private function isIisRewrite(): bool + { + if (1 === $this->server->getInt('IIS_WasUrlRewritten')) { + $this->isIisRewrite = true; + $this->server->remove('IIS_WasUrlRewritten'); + } + + return $this->isIisRewrite; + } } diff --git a/Tests/RequestTest.php b/Tests/RequestTest.php index a6d0b25b5..395df09c5 100644 --- a/Tests/RequestTest.php +++ b/Tests/RequestTest.php @@ -1850,6 +1850,62 @@ public static function getBaseUrlData() ]; } + /** + * @dataProvider baseUriDetectionOnIisWithRewriteData + */ + public function testBaseUriDetectionOnIisWithRewrite(array $server, string $expectedBaseUrl, string $expectedPathInfo) + { + $request = new Request([], [], [], [], [], $server); + + self::assertSame($expectedBaseUrl, $request->getBaseUrl()); + self::assertSame($expectedPathInfo, $request->getPathInfo()); + } + + public static function baseUriDetectionOnIisWithRewriteData(): \Generator + { + yield 'No rewrite' => [ + [ + 'PATH_INFO' => '/foo/bar', + 'PHP_SELF' => '/routingtest/index.php/foo/bar', + 'REQUEST_URI' => '/routingtest/index.php/foo/bar', + 'SCRIPT_FILENAME' => 'C:/Users/derrabus/Projects/routing-test/public/index.php', + 'SCRIPT_NAME' => '/routingtest/index.php', + ], + '/routingtest/index.php', + '/foo/bar', + ]; + + yield 'Rewrite with correct case' => [ + [ + 'IIS_WasUrlRewritten' => '1', + 'PATH_INFO' => '/foo/bar', + 'PHP_SELF' => '/routingtest/index.php/foo/bar', + 'REQUEST_URI' => '/routingtest/foo/bar', + 'SCRIPT_FILENAME' => 'C:/Users/derrabus/Projects/routing-test/public/index.php', + 'SCRIPT_NAME' => '/routingtest/index.php', + 'UNENCODED_URL' => '/routingtest/foo/bar', + ], + '/routingtest', + '/foo/bar', + ]; + + // ISS with UrlRewriteModule might report SCRIPT_NAME/PHP_SELF with wrong case + // see https://github.com/php/php-src/issues/11981 + yield 'Rewrite with case mismatch' => [ + [ + 'IIS_WasUrlRewritten' => '1', + 'PATH_INFO' => '/foo/bar', + 'PHP_SELF' => '/routingtest/index.php/foo/bar', + 'REQUEST_URI' => '/RoutingTest/foo/bar', + 'SCRIPT_FILENAME' => 'C:/Users/derrabus/Projects/routing-test/public/index.php', + 'SCRIPT_NAME' => '/routingtest/index.php', + 'UNENCODED_URL' => '/RoutingTest/foo/bar', + ], + '/RoutingTest', + '/foo/bar', + ]; + } + /** * @dataProvider urlencodedStringPrefixData */ From b50f5e281d722cb0f4c296f908bacc3e2b721957 Mon Sep 17 00:00:00 2001 From: Marko Kaznovac Date: Mon, 4 Sep 2023 23:33:54 +0200 Subject: [PATCH 055/111] UrlHelper: minor code style fix --- UrlHelper.php | 1 - 1 file changed, 1 deletion(-) diff --git a/UrlHelper.php b/UrlHelper.php index d5641eff8..f971cf662 100644 --- a/UrlHelper.php +++ b/UrlHelper.php @@ -21,7 +21,6 @@ */ final class UrlHelper { - public function __construct( private RequestStack $requestStack, private RequestContextAwareInterface|RequestContext|null $requestContext = null, From d62604da9ed303d5655f0b668c9b1fc999ad1bbc Mon Sep 17 00:00:00 2001 From: Jeroeny Date: Sat, 2 Sep 2023 16:16:41 +0200 Subject: [PATCH 056/111] support root-level Generator in StreamedJsonResponse --- CHANGELOG.md | 1 + StreamedJsonResponse.php | 99 ++++++++++++++++++------------ Tests/StreamedJsonResponseTest.php | 21 ++++++- 3 files changed, 79 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f1f6d5ce..04267c3e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Make `HeaderBag::getDate()`, `Response::getDate()`, `getExpires()` and `getLastModified()` return a `DateTimeImmutable` + * Support root-level `Generator` in `StreamedJsonResponse` 6.3 --- diff --git a/StreamedJsonResponse.php b/StreamedJsonResponse.php index cf858a5eb..5b20ce910 100644 --- a/StreamedJsonResponse.php +++ b/StreamedJsonResponse.php @@ -47,13 +47,13 @@ class StreamedJsonResponse extends StreamedResponse private const PLACEHOLDER = '__symfony_json__'; /** - * @param mixed[] $data JSON Data containing PHP generators which will be streamed as list of data + * @param mixed[] $data JSON Data containing PHP generators which will be streamed as list of data or a Generator * @param int $status The HTTP status code (200 "OK" by default) * @param array $headers An array of HTTP headers * @param int $encodingOptions Flags for the json_encode() function */ public function __construct( - private readonly array $data, + private readonly iterable $data, int $status = 200, array $headers = [], private int $encodingOptions = JsonResponse::DEFAULT_ENCODING_OPTIONS, @@ -66,11 +66,35 @@ public function __construct( } private function stream(): void + { + $jsonEncodingOptions = \JSON_THROW_ON_ERROR | $this->encodingOptions; + $keyEncodingOptions = $jsonEncodingOptions & ~\JSON_NUMERIC_CHECK; + + $this->streamData($this->data, $jsonEncodingOptions, $keyEncodingOptions); + } + + private function streamData(mixed $data, int $jsonEncodingOptions, int $keyEncodingOptions): void + { + if (\is_array($data)) { + $this->streamArray($data, $jsonEncodingOptions, $keyEncodingOptions); + + return; + } + + if (is_iterable($data) && !$data instanceof \JsonSerializable) { + $this->streamIterable($data, $jsonEncodingOptions, $keyEncodingOptions); + + return; + } + + echo json_encode($data, $jsonEncodingOptions); + } + + private function streamArray(array $data, int $jsonEncodingOptions, int $keyEncodingOptions): void { $generators = []; - $structure = $this->data; - array_walk_recursive($structure, function (&$item, $key) use (&$generators) { + array_walk_recursive($data, function (&$item, $key) use (&$generators) { if (self::PLACEHOLDER === $key) { // if the placeholder is already in the structure it should be replaced with a new one that explode // works like expected for the structure @@ -88,56 +112,51 @@ private function stream(): void } }); - $jsonEncodingOptions = \JSON_THROW_ON_ERROR | $this->encodingOptions; - $keyEncodingOptions = $jsonEncodingOptions & ~\JSON_NUMERIC_CHECK; - - $jsonParts = explode('"'.self::PLACEHOLDER.'"', json_encode($structure, $jsonEncodingOptions)); + $jsonParts = explode('"'.self::PLACEHOLDER.'"', json_encode($data, $jsonEncodingOptions)); foreach ($generators as $index => $generator) { // send first and between parts of the structure echo $jsonParts[$index]; - if ($generator instanceof \JsonSerializable || !$generator instanceof \Traversable) { - // the placeholders, JsonSerializable and none traversable items in the structure are rendered here - echo json_encode($generator, $jsonEncodingOptions); - - continue; - } + $this->streamData($generator, $jsonEncodingOptions, $keyEncodingOptions); + } - $isFirstItem = true; - $startTag = '['; - - foreach ($generator as $key => $item) { - if ($isFirstItem) { - $isFirstItem = false; - // depending on the first elements key the generator is detected as a list or map - // we can not check for a whole list or map because that would hurt the performance - // of the streamed response which is the main goal of this response class - if (0 !== $key) { - $startTag = '{'; - } - - echo $startTag; - } else { - // if not first element of the generic, a separator is required between the elements - echo ','; - } + // send last part of the structure + echo $jsonParts[array_key_last($jsonParts)]; + } - if ('{' === $startTag) { - echo json_encode((string) $key, $keyEncodingOptions).':'; + private function streamIterable(iterable $iterable, int $jsonEncodingOptions, int $keyEncodingOptions): void + { + $isFirstItem = true; + $startTag = '['; + + foreach ($iterable as $key => $item) { + if ($isFirstItem) { + $isFirstItem = false; + // depending on the first elements key the generator is detected as a list or map + // we can not check for a whole list or map because that would hurt the performance + // of the streamed response which is the main goal of this response class + if (0 !== $key) { + $startTag = '{'; } - echo json_encode($item, $jsonEncodingOptions); + echo $startTag; + } else { + // if not first element of the generic, a separator is required between the elements + echo ','; } - if ($isFirstItem) { // indicates that the generator was empty - echo '['; + if ('{' === $startTag) { + echo json_encode((string) $key, $keyEncodingOptions).':'; } - echo '[' === $startTag ? ']' : '}'; + $this->streamData($item, $jsonEncodingOptions, $keyEncodingOptions); } - // send last part of the structure - echo $jsonParts[array_key_last($jsonParts)]; + if ($isFirstItem) { // indicates that the generator was empty + echo '['; + } + + echo '[' === $startTag ? ']' : '}'; } } diff --git a/Tests/StreamedJsonResponseTest.php b/Tests/StreamedJsonResponseTest.php index 046f7dae4..db76cd3ae 100644 --- a/Tests/StreamedJsonResponseTest.php +++ b/Tests/StreamedJsonResponseTest.php @@ -30,6 +30,23 @@ public function testResponseSimpleList() $this->assertSame('{"_embedded":{"articles":["Article 1","Article 2","Article 3"],"news":["News 1","News 2","News 3"]}}', $content); } + public function testResponseSimpleGenerator() + { + $content = $this->createSendResponse($this->generatorSimple('Article')); + + $this->assertSame('["Article 1","Article 2","Article 3"]', $content); + } + + public function testResponseNestedGenerator() + { + $content = $this->createSendResponse((function (): iterable { + yield 'articles' => $this->generatorSimple('Article'); + yield 'news' => $this->generatorSimple('News'); + })()); + + $this->assertSame('{"articles":["Article 1","Article 2","Article 3"],"news":["News 1","News 2","News 3"]}', $content); + } + public function testResponseEmptyList() { $content = $this->createSendResponse( @@ -220,9 +237,9 @@ public function testEncodingOptions() } /** - * @param mixed[] $data + * @param iterable $data */ - private function createSendResponse(array $data): string + private function createSendResponse(iterable $data): string { $response = new StreamedJsonResponse($data); From 563abd838358afdd5ad09f2418801116671cc099 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 25 Sep 2023 14:52:38 +0200 Subject: [PATCH 057/111] Minor CS fixes --- Response.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Response.php b/Response.php index 6acf11f09..4b56449e0 100644 --- a/Response.php +++ b/Response.php @@ -372,7 +372,7 @@ public function sendHeaders(/* int $statusCode = null */): static $newValues = null === $previousValues ? $values : array_diff($values, $previousValues); } - foreach ($newValues as $value) { + foreach ($newValues as $value) { header($name.': '.$value, $replace, $this->statusCode); } From f2ab4acdccefb7e60dfdb309d8e60d3e069b0270 Mon Sep 17 00:00:00 2001 From: Markus Fasselt Date: Thu, 28 Sep 2023 16:54:56 +0200 Subject: [PATCH 058/111] Replace usages of SkippedTestSuiteError with markTestSkipped() call --- Tests/ResponseFunctionalTest.php | 3 +-- .../Session/Storage/Handler/AbstractSessionHandlerTest.php | 3 +-- .../Storage/Handler/RedisClusterSessionHandlerTest.php | 6 ++---- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/Tests/ResponseFunctionalTest.php b/Tests/ResponseFunctionalTest.php index c89adcd3c..ccda147df 100644 --- a/Tests/ResponseFunctionalTest.php +++ b/Tests/ResponseFunctionalTest.php @@ -11,7 +11,6 @@ namespace Symfony\Component\HttpFoundation\Tests; -use PHPUnit\Framework\SkippedTestSuiteError; use PHPUnit\Framework\TestCase; class ResponseFunctionalTest extends TestCase @@ -26,7 +25,7 @@ public static function setUpBeforeClass(): void 2 => ['file', '/dev/null', 'w'], ]; if (!self::$server = @proc_open('exec '.\PHP_BINARY.' -S localhost:8054', $spec, $pipes, __DIR__.'/Fixtures/response-functional')) { - throw new SkippedTestSuiteError('PHP server unable to start.'); + self::markTestSkipped('PHP server unable to start.'); } sleep(1); } diff --git a/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php b/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php index aabeba900..27fb57da4 100644 --- a/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php @@ -11,7 +11,6 @@ namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler; -use PHPUnit\Framework\SkippedTestSuiteError; use PHPUnit\Framework\TestCase; class AbstractSessionHandlerTest extends TestCase @@ -26,7 +25,7 @@ public static function setUpBeforeClass(): void 2 => ['file', '/dev/null', 'w'], ]; if (!self::$server = @proc_open('exec '.\PHP_BINARY.' -S localhost:8053', $spec, $pipes, __DIR__.'/Fixtures')) { - throw new SkippedTestSuiteError('PHP server unable to start.'); + self::markTestSkipped('PHP server unable to start.'); } sleep(1); } diff --git a/Tests/Session/Storage/Handler/RedisClusterSessionHandlerTest.php b/Tests/Session/Storage/Handler/RedisClusterSessionHandlerTest.php index 031629501..6a30f558f 100644 --- a/Tests/Session/Storage/Handler/RedisClusterSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/RedisClusterSessionHandlerTest.php @@ -11,8 +11,6 @@ namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler; -use PHPUnit\Framework\SkippedTestSuiteError; - /** * @group integration */ @@ -21,11 +19,11 @@ class RedisClusterSessionHandlerTest extends AbstractRedisSessionHandlerTestCase public static function setUpBeforeClass(): void { if (!class_exists(\RedisCluster::class)) { - throw new SkippedTestSuiteError('The RedisCluster class is required.'); + self::markTestSkipped('The RedisCluster class is required.'); } if (!$hosts = getenv('REDIS_CLUSTER_HOSTS')) { - throw new SkippedTestSuiteError('REDIS_CLUSTER_HOSTS env var is not defined.'); + self::markTestSkipped('REDIS_CLUSTER_HOSTS env var is not defined.'); } } From 3b22d275fcb8960553e64a7a5ba0f6abb2ebfa78 Mon Sep 17 00:00:00 2001 From: Julien Falque Date: Tue, 14 Dec 2021 18:20:42 +0100 Subject: [PATCH 059/111] [FrameworkBundle] Allow BrowserKit relative URL redirect assert --- .../Constraint/ResponseHeaderLocationSame.php | 65 +++++++++ .../ResponseHeaderLocationSameTest.php | 137 ++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 Test/Constraint/ResponseHeaderLocationSame.php create mode 100644 Tests/Test/Constraint/ResponseHeaderLocationSameTest.php diff --git a/Test/Constraint/ResponseHeaderLocationSame.php b/Test/Constraint/ResponseHeaderLocationSame.php new file mode 100644 index 000000000..4f8c431ae --- /dev/null +++ b/Test/Constraint/ResponseHeaderLocationSame.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Test\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +final class ResponseHeaderLocationSame extends Constraint +{ + public function __construct(private Request $request, private string $expectedValue) + { + } + + public function toString(): string + { + return sprintf('has header "Location" matching "%s"', $this->expectedValue); + } + + protected function matches($other): bool + { + if (!$other instanceof Response) { + return false; + } + + $location = $other->headers->get('Location'); + + if (null === $location) { + return false; + } + + return $this->toFullUrl($this->expectedValue) === $this->toFullUrl($location); + } + + protected function failureDescription($other): string + { + return 'the Response '.$this->toString(); + } + + private function toFullUrl(string $url): string + { + if (null === parse_url($url, \PHP_URL_PATH)) { + $url .= '/'; + } + + if (str_starts_with($url, '//')) { + return "{$this->request->getScheme()}:{$url}"; + } + + if (str_starts_with($url, '/')) { + return "{$this->request->getSchemeAndHttpHost()}{$url}"; + } + + return $url; + } +} diff --git a/Tests/Test/Constraint/ResponseHeaderLocationSameTest.php b/Tests/Test/Constraint/ResponseHeaderLocationSameTest.php new file mode 100644 index 000000000..5754befbc --- /dev/null +++ b/Tests/Test/Constraint/ResponseHeaderLocationSameTest.php @@ -0,0 +1,137 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Tests\Test\Constraint; + +use PHPUnit\Framework\ExpectationFailedException; +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Test\Constraint\ResponseHeaderLocationSame; + +class ResponseHeaderLocationSameTest extends TestCase +{ + /** + * @dataProvider provideSuccessCases + */ + public function testConstraintSuccess(string $requestUrl, ?string $location, string $expectedLocation) + { + $request = Request::create($requestUrl); + + $response = new Response(); + if (null !== $location) { + $response->headers->set('Location', $location); + } + + $constraint = new ResponseHeaderLocationSame($request, $expectedLocation); + + self::assertTrue($constraint->evaluate($response, '', true)); + } + + public function provideSuccessCases(): iterable + { + yield ['http://example.com', 'http://example.com', 'http://example.com']; + yield ['http://example.com', 'http://example.com', '//example.com']; + yield ['http://example.com', 'http://example.com', '/']; + yield ['http://example.com', '//example.com', 'http://example.com']; + yield ['http://example.com', '//example.com', '//example.com']; + yield ['http://example.com', '//example.com', '/']; + yield ['http://example.com', '/', 'http://example.com']; + yield ['http://example.com', '/', '//example.com']; + yield ['http://example.com', '/', '/']; + + yield ['http://example.com/', 'http://example.com/', 'http://example.com/']; + yield ['http://example.com/', 'http://example.com/', '//example.com/']; + yield ['http://example.com/', 'http://example.com/', '/']; + yield ['http://example.com/', '//example.com/', 'http://example.com/']; + yield ['http://example.com/', '//example.com/', '//example.com/']; + yield ['http://example.com/', '//example.com/', '/']; + yield ['http://example.com/', '/', 'http://example.com/']; + yield ['http://example.com/', '/', '//example.com/']; + yield ['http://example.com/', '/', '/']; + + yield ['http://example.com/foo', 'http://example.com/', 'http://example.com/']; + yield ['http://example.com/foo', 'http://example.com/', '//example.com/']; + yield ['http://example.com/foo', 'http://example.com/', '/']; + yield ['http://example.com/foo', '//example.com/', 'http://example.com/']; + yield ['http://example.com/foo', '//example.com/', '//example.com/']; + yield ['http://example.com/foo', '//example.com/', '/']; + yield ['http://example.com/foo', '/', 'http://example.com/']; + yield ['http://example.com/foo', '/', '//example.com/']; + yield ['http://example.com/foo', '/', '/']; + + yield ['http://example.com/foo', 'http://example.com/bar', 'http://example.com/bar']; + yield ['http://example.com/foo', 'http://example.com/bar', '//example.com/bar']; + yield ['http://example.com/foo', 'http://example.com/bar', '/bar']; + yield ['http://example.com/foo', '//example.com/bar', 'http://example.com/bar']; + yield ['http://example.com/foo', '//example.com/bar', '//example.com/bar']; + yield ['http://example.com/foo', '//example.com/bar', '/bar']; + yield ['http://example.com/foo', '/bar', 'http://example.com/bar']; + yield ['http://example.com/foo', '/bar', '//example.com/bar']; + yield ['http://example.com/foo', '/bar', '/bar']; + + yield ['http://example.com', 'http://example.com/bar', 'http://example.com/bar']; + yield ['http://example.com', 'http://example.com/bar', '//example.com/bar']; + yield ['http://example.com', 'http://example.com/bar', '/bar']; + yield ['http://example.com', '//example.com/bar', 'http://example.com/bar']; + yield ['http://example.com', '//example.com/bar', '//example.com/bar']; + yield ['http://example.com', '//example.com/bar', '/bar']; + yield ['http://example.com', '/bar', 'http://example.com/bar']; + yield ['http://example.com', '/bar', '//example.com/bar']; + yield ['http://example.com', '/bar', '/bar']; + + yield ['http://example.com/', 'http://another-example.com', 'http://another-example.com']; + } + + /** + * @dataProvider provideFailureCases + */ + public function testConstraintFailure(string $requestUrl, ?string $location, string $expectedLocation) + { + $request = Request::create($requestUrl); + + $response = new Response(); + if (null !== $location) { + $response->headers->set('Location', $location); + } + + $constraint = new ResponseHeaderLocationSame($request, $expectedLocation); + + self::assertFalse($constraint->evaluate($response, '', true)); + + $this->expectException(ExpectationFailedException::class); + + $constraint->evaluate($response); + } + + public function provideFailureCases(): iterable + { + yield ['http://example.com', null, 'http://example.com']; + yield ['http://example.com', null, '//example.com']; + yield ['http://example.com', null, '/']; + + yield ['http://example.com', 'http://another-example.com', 'http://example.com']; + yield ['http://example.com', 'http://another-example.com', '//example.com']; + yield ['http://example.com', 'http://another-example.com', '/']; + + yield ['http://example.com', 'http://example.com/bar', 'http://example.com']; + yield ['http://example.com', 'http://example.com/bar', '//example.com']; + yield ['http://example.com', 'http://example.com/bar', '/']; + + yield ['http://example.com/foo', 'http://example.com/bar', 'http://example.com']; + yield ['http://example.com/foo', 'http://example.com/bar', '//example.com']; + yield ['http://example.com/foo', 'http://example.com/bar', '/']; + + yield ['http://example.com/foo', 'http://example.com/bar', 'http://example.com/foo']; + yield ['http://example.com/foo', 'http://example.com/bar', '//example.com/foo']; + yield ['http://example.com/foo', 'http://example.com/bar', '/foo']; + } +} From 0a4e1511a0de62cb80ababf4dea97061e4f913e6 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 1 Oct 2023 09:07:20 +0200 Subject: [PATCH 060/111] Fix CS --- Test/Constraint/ResponseHeaderLocationSame.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Test/Constraint/ResponseHeaderLocationSame.php b/Test/Constraint/ResponseHeaderLocationSame.php index 4f8c431ae..9286ec715 100644 --- a/Test/Constraint/ResponseHeaderLocationSame.php +++ b/Test/Constraint/ResponseHeaderLocationSame.php @@ -53,11 +53,11 @@ private function toFullUrl(string $url): string } if (str_starts_with($url, '//')) { - return "{$this->request->getScheme()}:{$url}"; + return sprintf('%s:%s', $this->request->getScheme(), $url); } if (str_starts_with($url, '/')) { - return "{$this->request->getSchemeAndHttpHost()}{$url}"; + return $this->request->getSchemeAndHttpHost().$url; } return $url; From 65e9432d70768c4206a2b37c89bb2469bcdc07bf Mon Sep 17 00:00:00 2001 From: Alexander Schranz Date: Tue, 23 May 2023 01:51:38 +0200 Subject: [PATCH 061/111] Move UriSigner from HttpKernel to HttpFoundation package --- CHANGELOG.md | 1 + Tests/UriSignerTest.php | 86 +++++++++++++++++++++++++++++++ UriSigner.php | 111 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 198 insertions(+) create mode 100644 Tests/UriSignerTest.php create mode 100644 UriSigner.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 04267c3e9..603314b00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * Make `HeaderBag::getDate()`, `Response::getDate()`, `getExpires()` and `getLastModified()` return a `DateTimeImmutable` * Support root-level `Generator` in `StreamedJsonResponse` + * Add `UriSigner` from the HttpKernel component 6.3 --- diff --git a/Tests/UriSignerTest.php b/Tests/UriSignerTest.php new file mode 100644 index 000000000..dfbe81e88 --- /dev/null +++ b/Tests/UriSignerTest.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\UriSigner; + +class UriSignerTest extends TestCase +{ + public function testSign() + { + $signer = new UriSigner('foobar'); + + $this->assertStringContainsString('?_hash=', $signer->sign('http://example.com/foo')); + $this->assertStringContainsString('?_hash=', $signer->sign('http://example.com/foo?foo=bar')); + $this->assertStringContainsString('&foo=', $signer->sign('http://example.com/foo?foo=bar')); + } + + public function testCheck() + { + $signer = new UriSigner('foobar'); + + $this->assertFalse($signer->check('http://example.com/foo?_hash=foo')); + $this->assertFalse($signer->check('http://example.com/foo?foo=bar&_hash=foo')); + $this->assertFalse($signer->check('http://example.com/foo?foo=bar&_hash=foo&bar=foo')); + + $this->assertTrue($signer->check($signer->sign('http://example.com/foo'))); + $this->assertTrue($signer->check($signer->sign('http://example.com/foo?foo=bar'))); + $this->assertTrue($signer->check($signer->sign('http://example.com/foo?foo=bar&0=integer'))); + + $this->assertSame($signer->sign('http://example.com/foo?foo=bar&bar=foo'), $signer->sign('http://example.com/foo?bar=foo&foo=bar')); + } + + public function testCheckWithDifferentArgSeparator() + { + $this->iniSet('arg_separator.output', '&'); + $signer = new UriSigner('foobar'); + + $this->assertSame( + 'http://example.com/foo?_hash=rIOcC%2FF3DoEGo%2FvnESjSp7uU9zA9S%2F%2BOLhxgMexoPUM%3D&baz=bay&foo=bar', + $signer->sign('http://example.com/foo?foo=bar&baz=bay') + ); + $this->assertTrue($signer->check($signer->sign('http://example.com/foo?foo=bar&baz=bay'))); + } + + public function testCheckWithRequest() + { + $signer = new UriSigner('foobar'); + + $this->assertTrue($signer->checkRequest(Request::create($signer->sign('http://example.com/foo')))); + $this->assertTrue($signer->checkRequest(Request::create($signer->sign('http://example.com/foo?foo=bar')))); + $this->assertTrue($signer->checkRequest(Request::create($signer->sign('http://example.com/foo?foo=bar&0=integer')))); + } + + public function testCheckWithDifferentParameter() + { + $signer = new UriSigner('foobar', 'qux'); + + $this->assertSame( + 'http://example.com/foo?baz=bay&foo=bar&qux=rIOcC%2FF3DoEGo%2FvnESjSp7uU9zA9S%2F%2BOLhxgMexoPUM%3D', + $signer->sign('http://example.com/foo?foo=bar&baz=bay') + ); + $this->assertTrue($signer->check($signer->sign('http://example.com/foo?foo=bar&baz=bay'))); + } + + public function testSignerWorksWithFragments() + { + $signer = new UriSigner('foobar'); + + $this->assertSame( + 'http://example.com/foo?_hash=EhpAUyEobiM3QTrKxoLOtQq5IsWyWedoXDPqIjzNj5o%3D&bar=foo&foo=bar#foobar', + $signer->sign('http://example.com/foo?bar=foo&foo=bar#foobar') + ); + $this->assertTrue($signer->check($signer->sign('http://example.com/foo?bar=foo&foo=bar#foobar'))); + } +} diff --git a/UriSigner.php b/UriSigner.php new file mode 100644 index 000000000..091ac03e4 --- /dev/null +++ b/UriSigner.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * Signs URIs. + * + * @author Fabien Potencier + */ +class UriSigner +{ + private string $secret; + private string $parameter; + + /** + * @param string $secret A secret + * @param string $parameter Query string parameter to use + */ + public function __construct(#[\SensitiveParameter] string $secret, string $parameter = '_hash') + { + $this->secret = $secret; + $this->parameter = $parameter; + } + + /** + * Signs a URI. + * + * The given URI is signed by adding the query string parameter + * which value depends on the URI and the secret. + */ + public function sign(string $uri): string + { + $url = parse_url($uri); + $params = []; + + if (isset($url['query'])) { + parse_str($url['query'], $params); + } + + $uri = $this->buildUrl($url, $params); + $params[$this->parameter] = $this->computeHash($uri); + + return $this->buildUrl($url, $params); + } + + /** + * Checks that a URI contains the correct hash. + */ + public function check(string $uri): bool + { + $url = parse_url($uri); + $params = []; + + if (isset($url['query'])) { + parse_str($url['query'], $params); + } + + if (empty($params[$this->parameter])) { + return false; + } + + $hash = $params[$this->parameter]; + unset($params[$this->parameter]); + + return hash_equals($this->computeHash($this->buildUrl($url, $params)), $hash); + } + + public function checkRequest(Request $request): bool + { + $qs = ($qs = $request->server->get('QUERY_STRING')) ? '?'.$qs : ''; + + // we cannot use $request->getUri() here as we want to work with the original URI (no query string reordering) + return $this->check($request->getSchemeAndHttpHost().$request->getBaseUrl().$request->getPathInfo().$qs); + } + + private function computeHash(string $uri): string + { + return base64_encode(hash_hmac('sha256', $uri, $this->secret, true)); + } + + private function buildUrl(array $url, array $params = []): string + { + ksort($params, \SORT_STRING); + $url['query'] = http_build_query($params, '', '&'); + + $scheme = isset($url['scheme']) ? $url['scheme'].'://' : ''; + $host = $url['host'] ?? ''; + $port = isset($url['port']) ? ':'.$url['port'] : ''; + $user = $url['user'] ?? ''; + $pass = isset($url['pass']) ? ':'.$url['pass'] : ''; + $pass = ($user || $pass) ? "$pass@" : ''; + $path = $url['path'] ?? ''; + $query = $url['query'] ? '?'.$url['query'] : ''; + $fragment = isset($url['fragment']) ? '#'.$url['fragment'] : ''; + + return $scheme.$user.$pass.$host.$port.$path.$query.$fragment; + } +} + +if (!class_exists(\Symfony\Component\HttpKernel\UriSigner::class, false)) { + class_alias(UriSigner::class, \Symfony\Component\HttpKernel\UriSigner::class); +} From 5c932c375f2308405a8f0d0cda920fa7848a1e4b Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 2 Oct 2023 15:52:10 +0200 Subject: [PATCH 062/111] [HttpFoundation] Fix type of properties in Request class --- Request.php | 84 ++++++++++++++++++++++++++--------------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/Request.php b/Request.php index e19d5c584..9356e1ff1 100644 --- a/Request.php +++ b/Request.php @@ -136,57 +136,57 @@ class Request protected $content; /** - * @var string[] + * @var string[]|null */ protected $languages; /** - * @var string[] + * @var string[]|null */ protected $charsets; /** - * @var string[] + * @var string[]|null */ protected $encodings; /** - * @var string[] + * @var string[]|null */ protected $acceptableContentTypes; /** - * @var string + * @var string|null */ protected $pathInfo; /** - * @var string + * @var string|null */ protected $requestUri; /** - * @var string + * @var string|null */ protected $baseUrl; /** - * @var string + * @var string|null */ protected $basePath; /** - * @var string + * @var string|null */ protected $method; /** - * @var string + * @var string|null */ protected $format; /** - * @var SessionInterface|callable(): SessionInterface + * @var SessionInterface|callable():SessionInterface|null */ protected $session; @@ -282,16 +282,16 @@ public function initialize(array $query = [], array $request = [], array $attrib $this->headers = new HeaderBag($this->server->getHeaders()); $this->content = $content; - unset($this->languages); - unset($this->charsets); - unset($this->encodings); - unset($this->acceptableContentTypes); - unset($this->pathInfo); - unset($this->requestUri); - unset($this->baseUrl); - unset($this->basePath); - unset($this->method); - unset($this->format); + $this->languages = null; + $this->charsets = null; + $this->encodings = null; + $this->acceptableContentTypes = null; + $this->pathInfo = null; + $this->requestUri = null; + $this->baseUrl = null; + $this->basePath = null; + $this->method = null; + $this->format = null; } /** @@ -468,16 +468,16 @@ public function duplicate(array $query = null, array $request = null, array $att $dup->server = new ServerBag($server); $dup->headers = new HeaderBag($dup->server->getHeaders()); } - unset($dup->languages); - unset($dup->charsets); - unset($dup->encodings); - unset($dup->acceptableContentTypes); - unset($dup->pathInfo); - unset($dup->requestUri); - unset($dup->baseUrl); - unset($dup->basePath); - unset($dup->method); - unset($dup->format); + $dup->languages = null; + $dup->charsets = null; + $dup->encodings = null; + $dup->acceptableContentTypes = null; + $dup->pathInfo = null; + $dup->requestUri = null; + $dup->baseUrl = null; + $dup->basePath = null; + $dup->method = null; + $dup->format = null; if (!$dup->get('_format') && $this->get('_format')) { $dup->attributes->set('_format', $this->get('_format')); @@ -1182,7 +1182,7 @@ public function getHost(): string */ public function setMethod(string $method) { - unset($this->method); + $this->method = null; $this->server->set('REQUEST_METHOD', $method); } @@ -1201,7 +1201,7 @@ public function setMethod(string $method) */ public function getMethod(): string { - if (isset($this->method)) { + if (null !== $this->method) { return $this->method; } @@ -1249,7 +1249,7 @@ public function getRealMethod(): string */ public function getMimeType(string $format): ?string { - if (!isset(static::$formats)) { + if (null === static::$formats) { static::initializeFormats(); } @@ -1263,7 +1263,7 @@ public function getMimeType(string $format): ?string */ public static function getMimeTypes(string $format): array { - if (!isset(static::$formats)) { + if (null === static::$formats) { static::initializeFormats(); } @@ -1280,7 +1280,7 @@ public function getFormat(?string $mimeType): ?string $canonicalMimeType = trim(substr($mimeType, 0, $pos)); } - if (!isset(static::$formats)) { + if (null === static::$formats) { static::initializeFormats(); } @@ -1305,7 +1305,7 @@ public function getFormat(?string $mimeType): ?string */ public function setFormat(?string $format, string|array $mimeTypes) { - if (!isset(static::$formats)) { + if (null === static::$formats) { static::initializeFormats(); } @@ -1586,13 +1586,13 @@ public function isNoCache(): bool */ public function getPreferredFormat(?string $default = 'html'): ?string { - if (isset($this->preferredFormat) || null !== $preferredFormat = $this->getRequestFormat(null)) { - return $this->preferredFormat ??= $preferredFormat; + if ($this->preferredFormat ??= $this->getRequestFormat(null)) { + return $this->preferredFormat; } foreach ($this->getAcceptableContentTypes() as $mimeType) { - if ($preferredFormat = $this->getFormat($mimeType)) { - return $this->preferredFormat = $preferredFormat; + if ($this->preferredFormat = $this->getFormat($mimeType)) { + return $this->preferredFormat; } } @@ -1639,7 +1639,7 @@ public function getPreferredLanguage(array $locales = null): ?string */ public function getLanguages(): array { - if (isset($this->languages)) { + if (null !== $this->languages) { return $this->languages; } From 688f1a9220295a3e0fbf35c0e8a8d42383756ec4 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Sun, 8 Oct 2023 09:46:22 +0800 Subject: [PATCH 063/111] [HttpFoundation] Fix type of properties in Request class --- Request.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Request.php b/Request.php index 9356e1ff1..981fd24bd 100644 --- a/Request.php +++ b/Request.php @@ -201,7 +201,7 @@ class Request protected $defaultLocale = 'en'; /** - * @var array + * @var array|null */ protected static $formats; From 7dceaa11d594e5fbb4dcc84d19157d282de61249 Mon Sep 17 00:00:00 2001 From: Jordane Vaspard Date: Tue, 10 Oct 2023 13:26:09 +0200 Subject: [PATCH 064/111] [HttpKernel] Handle nullable callback of StreamedResponse --- StreamedResponse.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/StreamedResponse.php b/StreamedResponse.php index 5c7817e3c..37b6510b9 100644 --- a/StreamedResponse.php +++ b/StreamedResponse.php @@ -56,8 +56,12 @@ public function setCallback(callable $callback): static return $this; } - public function getCallback(): \Closure + public function getCallback(): ?\Closure { + if (!isset($this->callback)) { + return null; + } + return ($this->callback)(...); } From 3a2291d7842169147fb69a99cd85071e76d6b5a2 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 12 Oct 2023 18:09:12 +0200 Subject: [PATCH 065/111] Fix deps=low --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 248bcbb16..6b4c4a364 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "require-dev": { "doctrine/dbal": "^2.13.1|^3.0", "predis/predis": "^1.1|^2.0", - "symfony/cache": "^5.4|^6.0", + "symfony/cache": "^6.3", "symfony/dependency-injection": "^5.4|^6.0", "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4", "symfony/mime": "^5.4|^6.0", @@ -32,7 +32,7 @@ "symfony/rate-limiter": "^5.2|^6.0" }, "conflict": { - "symfony/cache": "<6.2" + "symfony/cache": "<6.3" }, "autoload": { "psr-4": { "Symfony\\Component\\HttpFoundation\\": "" }, From a27c5412eab95c058d96ef71dbe38b61dac01746 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Thu, 12 Oct 2023 19:29:50 +0200 Subject: [PATCH 066/111] Run tests with ORM 3 and DBAL 4 This reverts commit 913a317706780c843a7d1d7ed78e2e81a0d4d9e1. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 6b4c4a364..2128f56fc 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "symfony/polyfill-php83": "^1.27" }, "require-dev": { - "doctrine/dbal": "^2.13.1|^3.0", + "doctrine/dbal": "^2.13.1|^3|^4", "predis/predis": "^1.1|^2.0", "symfony/cache": "^6.3", "symfony/dependency-injection": "^5.4|^6.0", From 167f6d0f5c11765860f5d9c08ab5620178d79b5a Mon Sep 17 00:00:00 2001 From: Fabrice Locher Date: Thu, 12 Oct 2023 00:58:45 +0200 Subject: [PATCH 067/111] [HttpFoundation] Cookies Having Independent Partitioned State (CHIPS) --- CHANGELOG.md | 1 + Cookie.php | 41 +++++++++++++++++++++++++++++++++++------ Tests/CookieTest.php | 41 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 603314b00..d504dac2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG * Make `HeaderBag::getDate()`, `Response::getDate()`, `getExpires()` and `getLastModified()` return a `DateTimeImmutable` * Support root-level `Generator` in `StreamedJsonResponse` * Add `UriSigner` from the HttpKernel component + * Add `partitioned` flag to `Cookie` (CHIPS Cookie) 6.3 --- diff --git a/Cookie.php b/Cookie.php index 9f43cc2ae..706f5ca25 100644 --- a/Cookie.php +++ b/Cookie.php @@ -32,6 +32,7 @@ class Cookie private bool $raw; private ?string $sameSite = null; + private bool $partitioned = false; private bool $secureDefault = false; private const RESERVED_CHARS_LIST = "=,; \t\r\n\v\f"; @@ -51,6 +52,7 @@ public static function fromString(string $cookie, bool $decode = false): static 'httponly' => false, 'raw' => !$decode, 'samesite' => null, + 'partitioned' => false, ]; $parts = HeaderUtils::split($cookie, ';='); @@ -66,17 +68,20 @@ public static function fromString(string $cookie, bool $decode = false): static $data['expires'] = time() + (int) $data['max-age']; } - return new static($name, $value, $data['expires'], $data['path'], $data['domain'], $data['secure'], $data['httponly'], $data['raw'], $data['samesite']); + return new static($name, $value, $data['expires'], $data['path'], $data['domain'], $data['secure'], $data['httponly'], $data['raw'], $data['samesite'], $data['partitioned']); } /** * @see self::__construct * * @param self::SAMESITE_*|''|null $sameSite + * @param bool $partitioned */ - public static function create(string $name, string $value = null, int|string|\DateTimeInterface $expire = 0, ?string $path = '/', string $domain = null, bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = self::SAMESITE_LAX): self + public static function create(string $name, string $value = null, int|string|\DateTimeInterface $expire = 0, ?string $path = '/', string $domain = null, bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = self::SAMESITE_LAX /* , bool $partitioned = false */): self { - return new self($name, $value, $expire, $path, $domain, $secure, $httpOnly, $raw, $sameSite); + $partitioned = 9 < \func_num_args() ? func_get_arg(9) : false; + + return new self($name, $value, $expire, $path, $domain, $secure, $httpOnly, $raw, $sameSite, $partitioned); } /** @@ -92,7 +97,7 @@ public static function create(string $name, string $value = null, int|string|\Da * * @throws \InvalidArgumentException */ - public function __construct(string $name, string $value = null, int|string|\DateTimeInterface $expire = 0, ?string $path = '/', string $domain = null, bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = self::SAMESITE_LAX) + public function __construct(string $name, string $value = null, int|string|\DateTimeInterface $expire = 0, ?string $path = '/', string $domain = null, bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = self::SAMESITE_LAX, bool $partitioned = false) { // from PHP source code if ($raw && false !== strpbrk($name, self::RESERVED_CHARS_LIST)) { @@ -112,6 +117,7 @@ public function __construct(string $name, string $value = null, int|string|\Date $this->httpOnly = $httpOnly; $this->raw = $raw; $this->sameSite = $this->withSameSite($sameSite)->sameSite; + $this->partitioned = $partitioned; } /** @@ -237,6 +243,17 @@ public function withSameSite(?string $sameSite): static return $cookie; } + /** + * Creates a cookie copy that is tied to the top-level site in cross-site context. + */ + public function withPartitioned(bool $partitioned = true): static + { + $cookie = clone $this; + $cookie->partitioned = $partitioned; + + return $cookie; + } + /** * Returns the cookie as a string. */ @@ -268,11 +285,11 @@ public function __toString(): string $str .= '; domain='.$this->getDomain(); } - if (true === $this->isSecure()) { + if ($this->isSecure()) { $str .= '; secure'; } - if (true === $this->isHttpOnly()) { + if ($this->isHttpOnly()) { $str .= '; httponly'; } @@ -280,6 +297,10 @@ public function __toString(): string $str .= '; samesite='.$this->getSameSite(); } + if ($this->isPartitioned()) { + $str .= '; partitioned'; + } + return $str; } @@ -365,6 +386,14 @@ public function isRaw(): bool return $this->raw; } + /** + * Checks whether the cookie should be tied to the top-level site in cross-site context. + */ + public function isPartitioned(): bool + { + return $this->partitioned; + } + /** * @return self::SAMESITE_*|null */ diff --git a/Tests/CookieTest.php b/Tests/CookieTest.php index 874758e9d..eca5ee3e3 100644 --- a/Tests/CookieTest.php +++ b/Tests/CookieTest.php @@ -87,6 +87,19 @@ public function testNegativeExpirationIsNotPossible() $this->assertSame(0, $cookie->getExpiresTime()); } + public function testMinimalParameters() + { + $constructedCookie = new Cookie('foo'); + + $createdCookie = Cookie::create('foo'); + + $cookie = new Cookie('foo', null, 0, '/', null, null, true, false, 'lax'); + + $this->assertEquals($constructedCookie, $cookie); + + $this->assertEquals($createdCookie, $cookie); + } + public function testGetValue() { $value = 'MyValue'; @@ -187,6 +200,17 @@ public function testIsHttpOnly() $this->assertTrue($cookie->isHttpOnly(), '->isHttpOnly() returns whether the cookie is only transmitted over HTTP'); } + public function testIsPartitioned() + { + $cookie = new Cookie('foo', 'bar', 0, '/', '.myfoodomain.com', true, true, false, 'Lax', true); + + $this->assertTrue($cookie->isPartitioned()); + + $cookie = Cookie::create('foo')->withPartitioned(true); + + $this->assertTrue($cookie->isPartitioned()); + } + public function testCookieIsNotCleared() { $cookie = Cookie::create('foo', 'bar', time() + 3600 * 24); @@ -262,6 +286,20 @@ public function testToString() ->withSameSite(null); $this->assertEquals($expected, (string) $cookie, '->__toString() returns string representation of a cleared cookie if value is NULL'); + $expected = 'foo=deleted; expires='.gmdate('D, d M Y H:i:s T', $expire = time() - 31536001).'; Max-Age=0; path=/admin/; domain=.myfoodomain.com; secure; httponly; samesite=none; partitioned'; + $cookie = new Cookie('foo', null, 1, '/admin/', '.myfoodomain.com', true, true, false, 'none', true); + $this->assertEquals($expected, (string) $cookie, '->__toString() returns string representation of a cleared cookie if value is NULL'); + + $cookie = Cookie::create('foo') + ->withExpires(1) + ->withPath('/admin/') + ->withDomain('.myfoodomain.com') + ->withSecure(true) + ->withHttpOnly(true) + ->withSameSite('none') + ->withPartitioned(true); + $this->assertEquals($expected, (string) $cookie, '->__toString() returns string representation of a cleared cookie if value is NULL'); + $expected = 'foo=bar; path=/; httponly; samesite=lax'; $cookie = Cookie::create('foo', 'bar'); $this->assertEquals($expected, (string) $cookie); @@ -321,6 +359,9 @@ public function testFromString() $cookie = Cookie::fromString('foo_cookie=foo==; expires=Tue, 22 Sep 2020 06:27:09 GMT; path=/'); $this->assertEquals(Cookie::create('foo_cookie', 'foo==', strtotime('Tue, 22 Sep 2020 06:27:09 GMT'), '/', null, false, false, true, null), $cookie); + + $cookie = Cookie::fromString('foo_cookie=foo==; expires=Tue, 22 Sep 2020 06:27:09 GMT; path=/; secure; httponly; samesite=none; partitioned'); + $this->assertEquals(new Cookie('foo_cookie', 'foo==', strtotime('Tue, 22 Sep 2020 06:27:09 GMT'), '/', null, true, true, true, 'none', true), $cookie); } public function testFromStringWithHttpOnly() From fc6fc0063ada3616e58957c60518df49afe289e9 Mon Sep 17 00:00:00 2001 From: Yanick Witschi Date: Mon, 16 Oct 2023 12:43:38 +0200 Subject: [PATCH 068/111] [HttpFoundation] Cache trusted values --- Request.php | 21 +++++++++++++++++---- Tests/RequestTest.php | 17 +++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/Request.php b/Request.php index 981fd24bd..10f3a758f 100644 --- a/Request.php +++ b/Request.php @@ -212,6 +212,8 @@ class Request private bool $isForwardedValid = true; private bool $isSafeContentPreferred; + private array $trustedValuesCache = []; + private static int $trustedHeaderSet = -1; private const FORWARDED_PARAMS = [ @@ -1997,8 +1999,20 @@ public function isFromTrustedProxy(): bool return self::$trustedProxies && IpUtils::checkIp($this->server->get('REMOTE_ADDR', ''), self::$trustedProxies); } + /** + * This method is rather heavy because it splits and merges headers, and it's called by many other methods such as + * getPort(), isSecure(), getHost(), getClientIps(), getBaseUrl() etc. Thus, we try to cache the results for + * best performance. + */ private function getTrustedValues(int $type, string $ip = null): array { + $cacheKey = $type."\0".((self::$trustedHeaderSet & $type) ? $this->headers->get(self::TRUSTED_HEADERS[$type]) : ''); + $cacheKey .= "\0".$ip."\0".$this->headers->get(self::TRUSTED_HEADERS[self::HEADER_FORWARDED]); + + if (isset($this->trustedValuesCache[$cacheKey])) { + return $this->trustedValuesCache[$cacheKey]; + } + $clientValues = []; $forwardedValues = []; @@ -2011,7 +2025,6 @@ private function getTrustedValues(int $type, string $ip = null): array if ((self::$trustedHeaderSet & self::HEADER_FORWARDED) && (isset(self::FORWARDED_PARAMS[$type])) && $this->headers->has(self::TRUSTED_HEADERS[self::HEADER_FORWARDED])) { $forwarded = $this->headers->get(self::TRUSTED_HEADERS[self::HEADER_FORWARDED]); $parts = HeaderUtils::split($forwarded, ',;='); - $forwardedValues = []; $param = self::FORWARDED_PARAMS[$type]; foreach ($parts as $subParts) { if (null === $v = HeaderUtils::combine($subParts)[$param] ?? null) { @@ -2033,15 +2046,15 @@ private function getTrustedValues(int $type, string $ip = null): array } if ($forwardedValues === $clientValues || !$clientValues) { - return $forwardedValues; + return $this->trustedValuesCache[$cacheKey] = $forwardedValues; } if (!$forwardedValues) { - return $clientValues; + return $this->trustedValuesCache[$cacheKey] = $clientValues; } if (!$this->isForwardedValid) { - return null !== $ip ? ['0.0.0.0', $ip] : []; + return $this->trustedValuesCache[$cacheKey] = null !== $ip ? ['0.0.0.0', $ip] : []; } $this->isForwardedValid = false; diff --git a/Tests/RequestTest.php b/Tests/RequestTest.php index 03b4e6e6b..4329cb224 100644 --- a/Tests/RequestTest.php +++ b/Tests/RequestTest.php @@ -2550,6 +2550,23 @@ public function testTrustedProxiesRemoteAddr($serverRemoteAddr, $trustedProxies, $this->assertSame($result, Request::getTrustedProxies()); } + public function testTrustedValuesCache() + { + $request = Request::create('http://example.com/'); + $request->server->set('REMOTE_ADDR', '3.3.3.3'); + $request->headers->set('X_FORWARDED_FOR', '1.1.1.1, 2.2.2.2'); + $request->headers->set('X_FORWARDED_PROTO', 'https'); + + $this->assertFalse($request->isSecure()); + + Request::setTrustedProxies(['3.3.3.3', '2.2.2.2'], Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO); + $this->assertTrue($request->isSecure()); + + // Header is changed, cache must not be hit now + $request->headers->set('X_FORWARDED_PROTO', 'http'); + $this->assertFalse($request->isSecure()); + } + public static function trustedProxiesRemoteAddr() { return [ From 2213955e8653e84b84566bc3f952f4c43a30b71c Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Mon, 2 Oct 2023 23:04:54 +0200 Subject: [PATCH 069/111] [HttpFoundation] Do not swallow trailing `=` in cookie value --- HeaderUtils.php | 63 ++++++++++++++++++++------------------- Tests/CookieTest.php | 3 ++ Tests/HeaderUtilsTest.php | 6 ++-- 3 files changed, 39 insertions(+), 33 deletions(-) diff --git a/HeaderUtils.php b/HeaderUtils.php index 46b1e6aed..f91c7e1d9 100644 --- a/HeaderUtils.php +++ b/HeaderUtils.php @@ -33,17 +33,21 @@ private function __construct() * * Example: * - * HeaderUtils::split("da, en-gb;q=0.8", ",;") + * HeaderUtils::split('da, en-gb;q=0.8', ',;') * // => ['da'], ['en-gb', 'q=0.8']] * * @param string $separators List of characters to split on, ordered by - * precedence, e.g. ",", ";=", or ",;=" + * precedence, e.g. ',', ';=', or ',;=' * * @return array Nested array with as many levels as there are characters in * $separators */ public static function split(string $header, string $separators): array { + if ('' === $separators) { + throw new \InvalidArgumentException('At least one separator must be specified.'); + } + $quotedSeparators = preg_quote($separators, '/'); preg_match_all(' @@ -77,8 +81,8 @@ public static function split(string $header, string $separators): array * * Example: * - * HeaderUtils::combine([["foo", "abc"], ["bar"]]) - * // => ["foo" => "abc", "bar" => true] + * HeaderUtils::combine([['foo', 'abc'], ['bar']]) + * // => ['foo' => 'abc', 'bar' => true] */ public static function combine(array $parts): array { @@ -95,13 +99,13 @@ public static function combine(array $parts): array /** * Joins an associative array into a string for use in an HTTP header. * - * The key and value of each entry are joined with "=", and all entries + * The key and value of each entry are joined with '=', and all entries * are joined with the specified separator and an additional space (for * readability). Values are quoted if necessary. * * Example: * - * HeaderUtils::toString(["foo" => "abc", "bar" => true, "baz" => "a b c"], ",") + * HeaderUtils::toString(['foo' => 'abc', 'bar' => true, 'baz' => 'a b c'], ',') * // => 'foo=abc, bar, baz="a b c"' */ public static function toString(array $assoc, string $separator): string @@ -252,40 +256,37 @@ public static function parseQuery(string $query, bool $ignoreBrackets = false, s private static function groupParts(array $matches, string $separators, bool $first = true): array { $separator = $separators[0]; - $partSeparators = substr($separators, 1); - + $separators = substr($separators, 1); $i = 0; + + if ('' === $separators && !$first) { + $parts = ['']; + + foreach ($matches as $match) { + if (!$i && isset($match['separator'])) { + $i = 1; + $parts[1] = ''; + } else { + $parts[$i] .= self::unquote($match[0]); + } + } + + return $parts; + } + + $parts = []; $partMatches = []; - $previousMatchWasSeparator = false; + foreach ($matches as $match) { - if (!$first && $previousMatchWasSeparator && isset($match['separator']) && $match['separator'] === $separator) { - $previousMatchWasSeparator = true; - $partMatches[$i][] = $match; - } elseif (isset($match['separator']) && $match['separator'] === $separator) { - $previousMatchWasSeparator = true; + if (($match['separator'] ?? null) === $separator) { ++$i; } else { - $previousMatchWasSeparator = false; $partMatches[$i][] = $match; } } - $parts = []; - if ($partSeparators) { - foreach ($partMatches as $matches) { - $parts[] = self::groupParts($matches, $partSeparators, false); - } - } else { - foreach ($partMatches as $matches) { - $parts[] = self::unquote($matches[0][0]); - } - - if (!$first && 2 < \count($parts)) { - $parts = [ - $parts[0], - implode($separator, \array_slice($parts, 1)), - ]; - } + foreach ($partMatches as $matches) { + $parts[] = '' === $separators ? self::unquote($matches[0][0]) : self::groupParts($matches, $separators, false); } return $parts; diff --git a/Tests/CookieTest.php b/Tests/CookieTest.php index ec5a4e28f..9cc0d9fb8 100644 --- a/Tests/CookieTest.php +++ b/Tests/CookieTest.php @@ -324,6 +324,9 @@ public function testFromString() $cookie = Cookie::fromString('foo=bar', true); $this->assertEquals(Cookie::create('foo', 'bar', 0, '/', null, false, false, false, null), $cookie); + $cookie = Cookie::fromString('foo=bar=', true); + $this->assertEquals(Cookie::create('foo', 'bar=', 0, '/', null, false, false, false, null), $cookie); + $cookie = Cookie::fromString('foo', true); $this->assertEquals(Cookie::create('foo', null, 0, '/', null, false, false, false, null), $cookie); diff --git a/Tests/HeaderUtilsTest.php b/Tests/HeaderUtilsTest.php index 73d3f150c..befa4aea0 100644 --- a/Tests/HeaderUtilsTest.php +++ b/Tests/HeaderUtilsTest.php @@ -34,7 +34,7 @@ public static function provideHeaderToSplit(): array [['foo', '123, bar'], 'foo=123, bar', '='], [['foo', '123, bar'], ' foo = 123, bar ', '='], [[['foo', '123'], ['bar']], 'foo=123, bar', ',='], - [[[['foo', '123']], [['bar'], ['foo', '456']]], 'foo=123, bar; foo=456', ',;='], + [[[['foo', '123']], [['bar'], ['foo', '456']]], 'foo=123, bar;; foo=456', ',;='], [[[['foo', 'a,b;c=d']]], 'foo="a,b;c=d"', ',;='], [['foo', 'bar'], 'foo,,,, bar', ','], @@ -46,13 +46,15 @@ public static function provideHeaderToSplit(): array [[['foo_cookie', 'foo=1&bar=2&baz=3'], ['expires', 'Tue, 22-Sep-2020 06:27:09 GMT'], ['path', '/']], 'foo_cookie=foo=1&bar=2&baz=3; expires=Tue, 22-Sep-2020 06:27:09 GMT; path=/', ';='], [[['foo_cookie', 'foo=='], ['expires', 'Tue, 22-Sep-2020 06:27:09 GMT'], ['path', '/']], 'foo_cookie=foo==; expires=Tue, 22-Sep-2020 06:27:09 GMT; path=/', ';='], + [[['foo_cookie', 'foo='], ['expires', 'Tue, 22-Sep-2020 06:27:09 GMT'], ['path', '/']], 'foo_cookie=foo=; expires=Tue, 22-Sep-2020 06:27:09 GMT; path=/', ';='], [[['foo_cookie', 'foo=a=b'], ['expires', 'Tue, 22-Sep-2020 06:27:09 GMT'], ['path', '/']], 'foo_cookie=foo="a=b"; expires=Tue, 22-Sep-2020 06:27:09 GMT; path=/', ';='], // These are not a valid header values. We test that they parse anyway, // and that both the valid and invalid parts are returned. [[], '', ','], [[], ',,,', ','], - [['foo', 'bar', 'baz'], 'foo, "bar", "baz', ','], + [[['', 'foo'], ['bar', '']], '=foo,bar=', ',='], + [['foo', 'foobar', 'baz'], 'foo, foo"bar", "baz', ','], [['foo', 'bar, baz'], 'foo, "bar, baz', ','], [['foo', 'bar, baz\\'], 'foo, "bar, baz\\', ','], [['foo', 'bar, baz\\'], 'foo, "bar, baz\\\\', ','], From 2970be3ae68d786db4f834a191d7dc03d3b03bde Mon Sep 17 00:00:00 2001 From: Thomas Calvet Date: Fri, 13 Oct 2023 18:23:37 +0200 Subject: [PATCH 070/111] [HttpFoundation] Add $flush parameter to Response::send() --- CHANGELOG.md | 1 + Response.php | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d504dac2c..61297e2c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG * Support root-level `Generator` in `StreamedJsonResponse` * Add `UriSigner` from the HttpKernel component * Add `partitioned` flag to `Cookie` (CHIPS Cookie) + * Add argument `bool $flush = true` to `Response::send()` 6.3 --- diff --git a/Response.php b/Response.php index 4b56449e0..29db6297f 100644 --- a/Response.php +++ b/Response.php @@ -415,13 +415,20 @@ public function sendContent(): static /** * Sends HTTP headers and content. * + * @param bool $flush Whether output buffers should be flushed + * * @return $this */ - public function send(): static + public function send(/* bool $flush = true */): static { $this->sendHeaders(); $this->sendContent(); + $flush = 1 <= \func_num_args() ? func_get_arg(0) : true; + if (!$flush) { + return $this; + } + if (\function_exists('fastcgi_finish_request')) { fastcgi_finish_request(); } elseif (\function_exists('litespeed_finish_request')) { From f98c17747cfc35453ff0e0f6281aed51671728d6 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 16 Oct 2023 17:36:33 +0200 Subject: [PATCH 071/111] [HttpKernel] Add parameters `kernel.runtime_mode` and `kernel.runtime_mode.*`, all set from env var `APP_RUNTIME_MODE` --- Response.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Response.php b/Response.php index 29db6297f..dca93df61 100644 --- a/Response.php +++ b/Response.php @@ -433,7 +433,7 @@ public function send(/* bool $flush = true */): static fastcgi_finish_request(); } elseif (\function_exists('litespeed_finish_request')) { litespeed_finish_request(); - } elseif (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) { + } elseif (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) { static::closeOutputBuffers(0, true); flush(); } From 683d3d7c4abd28c76ec64ffe380ca8272b387a2b Mon Sep 17 00:00:00 2001 From: Thomas Calvet Date: Thu, 26 Oct 2023 19:20:47 +0200 Subject: [PATCH 072/111] [6.3] Remove unused test fixture --- Tests/ParameterBagTest.php | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Tests/ParameterBagTest.php b/Tests/ParameterBagTest.php index 62b95f42f..67ba51218 100644 --- a/Tests/ParameterBagTest.php +++ b/Tests/ParameterBagTest.php @@ -379,15 +379,3 @@ public function testGetEnumThrowsExceptionWithInvalidValueType() $this->assertNull($bag->getEnum('invalid-value', FooEnum::class)); } } - -class InputStringable -{ - public function __construct(private string $value) - { - } - - public function __toString(): string - { - return $this->value; - } -} From 671769f79de0532da1478c60968b42506e185d2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 27 Oct 2023 22:18:39 +0200 Subject: [PATCH 073/111] [Lock] Fix mongodb extension requirement in tests --- .../Storage/Handler/MongoDbSessionHandlerTest.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php b/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php index 1e6a05df2..8e9c5fa04 100644 --- a/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php @@ -13,6 +13,7 @@ use MongoDB\Client; use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\SkippedTestSuiteError; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler; @@ -32,13 +33,16 @@ class MongoDbSessionHandlerTest extends TestCase private $storage; public $options; - protected function setUp(): void + public static function setUpBeforeClass(): void { - parent::setUp(); - if (!class_exists(Client::class)) { - $this->markTestSkipped('The mongodb/mongodb package is required.'); + throw new SkippedTestSuiteError('The mongodb/mongodb package is required.'); } + } + + protected function setUp(): void + { + parent::setUp(); $this->mongo = $this->getMockBuilder(Client::class) ->disableOriginalConstructor() From 73549023487af301d20982bb4551cacd7167dd3a Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Fri, 27 Oct 2023 14:14:50 +0200 Subject: [PATCH 074/111] DX: re-apply self_accessor and phpdoc_types_order by PHP CS Fixer --- Response.php | 2 +- StreamedResponse.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Response.php b/Response.php index dca93df61..ef6ece002 100644 --- a/Response.php +++ b/Response.php @@ -331,7 +331,7 @@ public function prepare(Request $request): static /** * Sends HTTP headers. * - * @param null|positive-int $statusCode The status code to use, override the statusCode property if set and not null + * @param positive-int|null $statusCode The status code to use, override the statusCode property if set and not null * * @return $this */ diff --git a/StreamedResponse.php b/StreamedResponse.php index 43a8dad54..87be96a11 100644 --- a/StreamedResponse.php +++ b/StreamedResponse.php @@ -68,7 +68,7 @@ public function getCallback(): ?\Closure /** * This method only sends the headers once. * - * @param null|positive-int $statusCode The status code to use, override the statusCode property if set and not null + * @param positive-int|null $statusCode The status code to use, override the statusCode property if set and not null * * @return $this */ From 59e265807eb5d8964e8db2267ff2df8d32aa6e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Sun, 29 Oct 2023 21:56:38 +0100 Subject: [PATCH 075/111] [HttpFoundation][Lock] Makes MongoDB adapters usable with `ext-mongodb` only --- CHANGELOG.md | 1 + .../Storage/Handler/MongoDbSessionHandler.php | 94 ++++--- .../Handler/MongoDbSessionHandlerTest.php | 239 +++++++++--------- .../Session/Storage/Handler/stubs/mongodb.php | 24 ++ phpunit.xml.dist | 1 + 5 files changed, 201 insertions(+), 158 deletions(-) create mode 100644 Tests/Session/Storage/Handler/stubs/mongodb.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 61297e2c1..3f09854ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ CHANGELOG * Add `UriSigner` from the HttpKernel component * Add `partitioned` flag to `Cookie` (CHIPS Cookie) * Add argument `bool $flush = true` to `Response::send()` +* Make `MongoDbSessionHandler` instantiable with the mongodb extension directly 6.3 --- diff --git a/Session/Storage/Handler/MongoDbSessionHandler.php b/Session/Storage/Handler/MongoDbSessionHandler.php index 5ea5b4ae7..d5586030f 100644 --- a/Session/Storage/Handler/MongoDbSessionHandler.php +++ b/Session/Storage/Handler/MongoDbSessionHandler.php @@ -14,20 +14,22 @@ use MongoDB\BSON\Binary; use MongoDB\BSON\UTCDateTime; use MongoDB\Client; -use MongoDB\Collection; +use MongoDB\Driver\BulkWrite; +use MongoDB\Driver\Manager; +use MongoDB\Driver\Query; /** - * Session handler using the mongodb/mongodb package and MongoDB driver extension. + * Session handler using the MongoDB driver extension. * * @author Markus Bachmann + * @author Jérôme Tamarelle * - * @see https://packagist.org/packages/mongodb/mongodb * @see https://php.net/mongodb */ class MongoDbSessionHandler extends AbstractSessionHandler { - private Client $mongo; - private Collection $collection; + private Manager $manager; + private string $namespace; private array $options; private int|\Closure|null $ttl; @@ -62,13 +64,18 @@ class MongoDbSessionHandler extends AbstractSessionHandler * * @throws \InvalidArgumentException When "database" or "collection" not provided */ - public function __construct(Client $mongo, array $options) + public function __construct(Client|Manager $mongo, array $options) { if (!isset($options['database']) || !isset($options['collection'])) { throw new \InvalidArgumentException('You must provide the "database" and "collection" option for MongoDBSessionHandler.'); } - $this->mongo = $mongo; + if ($mongo instanceof Client) { + $mongo = $mongo->getManager(); + } + + $this->manager = $mongo; + $this->namespace = $options['database'].'.'.$options['collection']; $this->options = array_merge([ 'id_field' => '_id', @@ -86,77 +93,94 @@ public function close(): bool protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool { - $this->getCollection()->deleteOne([ - $this->options['id_field'] => $sessionId, - ]); + $write = new BulkWrite(); + $write->delete( + [$this->options['id_field'] => $sessionId], + ['limit' => 1] + ); + + $this->manager->executeBulkWrite($this->namespace, $write); return true; } public function gc(int $maxlifetime): int|false { - return $this->getCollection()->deleteMany([ - $this->options['expiry_field'] => ['$lt' => new UTCDateTime()], - ])->getDeletedCount(); + $write = new BulkWrite(); + $write->delete( + [$this->options['expiry_field'] => ['$lt' => $this->getUTCDateTime()]], + ); + $result = $this->manager->executeBulkWrite($this->namespace, $write); + + return $result->getDeletedCount() ?? false; } protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool { $ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime'); - $expiry = new UTCDateTime((time() + (int) $ttl) * 1000); + $expiry = $this->getUTCDateTime($ttl); $fields = [ - $this->options['time_field'] => new UTCDateTime(), + $this->options['time_field'] => $this->getUTCDateTime(), $this->options['expiry_field'] => $expiry, - $this->options['data_field'] => new Binary($data, Binary::TYPE_OLD_BINARY), + $this->options['data_field'] => new Binary($data, Binary::TYPE_GENERIC), ]; - $this->getCollection()->updateOne( + $write = new BulkWrite(); + $write->update( [$this->options['id_field'] => $sessionId], ['$set' => $fields], ['upsert' => true] ); + $this->manager->executeBulkWrite($this->namespace, $write); + return true; } public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool { $ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime'); - $expiry = new UTCDateTime((time() + (int) $ttl) * 1000); + $expiry = $this->getUTCDateTime($ttl); - $this->getCollection()->updateOne( + $write = new BulkWrite(); + $write->update( [$this->options['id_field'] => $sessionId], ['$set' => [ - $this->options['time_field'] => new UTCDateTime(), + $this->options['time_field'] => $this->getUTCDateTime(), $this->options['expiry_field'] => $expiry, - ]] + ]], + ['multi' => false], ); + $this->manager->executeBulkWrite($this->namespace, $write); + return true; } protected function doRead(#[\SensitiveParameter] string $sessionId): string { - $dbData = $this->getCollection()->findOne([ + $cursor = $this->manager->executeQuery($this->namespace, new Query([ $this->options['id_field'] => $sessionId, - $this->options['expiry_field'] => ['$gte' => new UTCDateTime()], - ]); - - if (null === $dbData) { - return ''; + $this->options['expiry_field'] => ['$gte' => $this->getUTCDateTime()], + ], [ + 'projection' => [ + '_id' => false, + $this->options['data_field'] => true, + ], + 'limit' => 1, + ])); + + foreach ($cursor as $document) { + return (string) $document->{$this->options['data_field']} ?? ''; } - return $dbData[$this->options['data_field']]->getData(); - } - - private function getCollection(): Collection - { - return $this->collection ??= $this->mongo->selectCollection($this->options['database'], $this->options['collection']); + // Not found + return ''; } - protected function getMongo(): Client + private function getUTCDateTime(int $additionalSeconds = 0): UTCDateTime { - return $this->mongo; + return new UTCDateTime((time() + $additionalSeconds) * 1000); } } diff --git a/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php b/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php index d1fc5fa57..0c6de4c8d 100644 --- a/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php @@ -11,56 +11,98 @@ namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler; +use MongoDB\BSON\Binary; +use MongoDB\BSON\UTCDateTime; use MongoDB\Client; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\SkippedTestSuiteError; +use MongoDB\Driver\BulkWrite; +use MongoDB\Driver\Command; +use MongoDB\Driver\Exception\CommandException; +use MongoDB\Driver\Exception\ConnectionException; +use MongoDB\Driver\Manager; +use MongoDB\Driver\Query; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler; +require_once __DIR__.'/stubs/mongodb.php'; + /** * @author Markus Bachmann * + * @group integration * @group time-sensitive * * @requires extension mongodb */ class MongoDbSessionHandlerTest extends TestCase { + private const DABASE_NAME = 'sf-test'; + private const COLLECTION_NAME = 'session-test'; + public array $options; - private MockObject&Client $mongo; + private Manager $manager; private MongoDbSessionHandler $storage; - public static function setUpBeforeClass(): void - { - if (!class_exists(Client::class)) { - throw new SkippedTestSuiteError('The mongodb/mongodb package is required.'); - } - } - protected function setUp(): void { parent::setUp(); - $this->mongo = $this->getMockBuilder(Client::class) - ->disableOriginalConstructor() - ->getMock(); + $this->manager = new Manager('mongodb://'.getenv('MONGODB_HOST')); + + try { + $this->manager->executeCommand(self::DABASE_NAME, new Command(['ping' => 1])); + } catch (ConnectionException $e) { + $this->markTestSkipped(sprintf('MongoDB Server "%s" not running: %s', getenv('MONGODB_HOST'), $e->getMessage())); + } $this->options = [ 'id_field' => '_id', 'data_field' => 'data', 'time_field' => 'time', 'expiry_field' => 'expires_at', - 'database' => 'sf-test', - 'collection' => 'session-test', + 'database' => self::DABASE_NAME, + 'collection' => self::COLLECTION_NAME, ]; - $this->storage = new MongoDbSessionHandler($this->mongo, $this->options); + $this->storage = new MongoDbSessionHandler($this->manager, $this->options); + } + + public function testCreateFromClient() + { + $client = $this->createMock(Client::class); + $client->expects($this->once()) + ->method('getManager') + ->willReturn($this->manager); + + $this->storage = new MongoDbSessionHandler($client, $this->options); + $this->storage->write('foo', 'bar'); + + $this->assertCount(1, $this->getSessions()); } - public function testConstructorShouldThrowExceptionForMissingOptions() + protected function tearDown(): void + { + try { + $this->manager->executeCommand(self::DABASE_NAME, new Command(['drop' => self::COLLECTION_NAME])); + } catch (CommandException $e) { + // The server may return a NamespaceNotFound error if the collection does not exist + if (26 !== $e->getCode()) { + throw $e; + } + } + } + + /** @dataProvider provideInvalidOptions */ + public function testConstructorShouldThrowExceptionForMissingOptions(array $options) { $this->expectException(\InvalidArgumentException::class); - new MongoDbSessionHandler($this->mongo, []); + new MongoDbSessionHandler($this->manager, $options); + } + + public function provideInvalidOptions() + { + yield 'empty' => [[]]; + yield 'collection missing' => [['database' => 'foo']]; + yield 'database missing' => [['collection' => 'foo']]; } public function testOpenMethodAlwaysReturnTrue() @@ -75,142 +117,93 @@ public function testCloseMethodAlwaysReturnTrue() public function testRead() { - $collection = $this->createMongoCollectionMock(); - - $this->mongo->expects($this->once()) - ->method('selectCollection') - ->with($this->options['database'], $this->options['collection']) - ->willReturn($collection); - - // defining the timeout before the actual method call - // allows to test for "greater than" values in the $criteria - $testTimeout = time() + 1; - - $collection->expects($this->once()) - ->method('findOne') - ->willReturnCallback(function ($criteria) use ($testTimeout) { - $this->assertArrayHasKey($this->options['id_field'], $criteria); - $this->assertEquals('foo', $criteria[$this->options['id_field']]); - - $this->assertArrayHasKey($this->options['expiry_field'], $criteria); - $this->assertArrayHasKey('$gte', $criteria[$this->options['expiry_field']]); - - $this->assertInstanceOf(\MongoDB\BSON\UTCDateTime::class, $criteria[$this->options['expiry_field']]['$gte']); - $this->assertGreaterThanOrEqual(round((string) $criteria[$this->options['expiry_field']]['$gte'] / 1000), $testTimeout); - - return [ - $this->options['id_field'] => 'foo', - $this->options['expiry_field'] => new \MongoDB\BSON\UTCDateTime(), - $this->options['data_field'] => new \MongoDB\BSON\Binary('bar', \MongoDB\BSON\Binary::TYPE_OLD_BINARY), - ]; - }); - + $this->insertSession('foo', 'bar', 0); $this->assertEquals('bar', $this->storage->read('foo')); } - public function testWrite() + public function testReadNotFound() { - $collection = $this->createMongoCollectionMock(); - - $this->mongo->expects($this->once()) - ->method('selectCollection') - ->with($this->options['database'], $this->options['collection']) - ->willReturn($collection); - - $collection->expects($this->once()) - ->method('updateOne') - ->willReturnCallback(function ($criteria, $updateData, $options) { - $this->assertEquals([$this->options['id_field'] => 'foo'], $criteria); - $this->assertEquals(['upsert' => true], $options); + $this->insertSession('foo', 'bar', 0); + $this->assertEquals('', $this->storage->read('foobar')); + } - $data = $updateData['$set']; - $expectedExpiry = time() + (int) \ini_get('session.gc_maxlifetime'); - $this->assertInstanceOf(\MongoDB\BSON\Binary::class, $data[$this->options['data_field']]); - $this->assertEquals('bar', $data[$this->options['data_field']]->getData()); - $this->assertInstanceOf(\MongoDB\BSON\UTCDateTime::class, $data[$this->options['time_field']]); - $this->assertInstanceOf(\MongoDB\BSON\UTCDateTime::class, $data[$this->options['expiry_field']]); - $this->assertGreaterThanOrEqual($expectedExpiry, round((string) $data[$this->options['expiry_field']] / 1000)); - }); + public function testReadExpired() + { + $this->insertSession('foo', 'bar', -100_000); + $this->assertEquals('', $this->storage->read('foo')); + } + public function testWrite() + { + $expectedTime = (new \DateTimeImmutable())->getTimestamp(); + $expectedExpiry = $expectedTime + (int) \ini_get('session.gc_maxlifetime'); $this->assertTrue($this->storage->write('foo', 'bar')); + + $sessions = $this->getSessions(); + $this->assertCount(1, $sessions); + $this->assertEquals('foo', $sessions[0]->_id); + $this->assertInstanceOf(Binary::class, $sessions[0]->data); + $this->assertEquals('bar', $sessions[0]->data->getData()); + $this->assertInstanceOf(UTCDateTime::class, $sessions[0]->time); + $this->assertGreaterThanOrEqual($expectedTime, round((string) $sessions[0]->time / 1000)); + $this->assertInstanceOf(UTCDateTime::class, $sessions[0]->expires_at); + $this->assertGreaterThanOrEqual($expectedExpiry, round((string) $sessions[0]->expires_at / 1000)); } public function testReplaceSessionData() { - $collection = $this->createMongoCollectionMock(); - - $this->mongo->expects($this->once()) - ->method('selectCollection') - ->with($this->options['database'], $this->options['collection']) - ->willReturn($collection); - - $data = []; - - $collection->expects($this->exactly(2)) - ->method('updateOne') - ->willReturnCallback(function ($criteria, $updateData, $options) use (&$data) { - $data = $updateData; - }); - $this->storage->write('foo', 'bar'); + $this->storage->write('baz', 'qux'); $this->storage->write('foo', 'foobar'); - $this->assertEquals('foobar', $data['$set'][$this->options['data_field']]->getData()); + $sessions = $this->getSessions(); + $this->assertCount(2, $sessions); + $this->assertEquals('foobar', $sessions[0]->data->getData()); } public function testDestroy() { - $collection = $this->createMongoCollectionMock(); - - $this->mongo->expects($this->once()) - ->method('selectCollection') - ->with($this->options['database'], $this->options['collection']) - ->willReturn($collection); - - $collection->expects($this->once()) - ->method('deleteOne') - ->with([$this->options['id_field'] => 'foo']); + $this->storage->write('foo', 'bar'); + $this->storage->write('baz', 'qux'); $this->assertTrue($this->storage->destroy('foo')); + + $sessions = $this->getSessions(); + $this->assertCount(1, $sessions); + $this->assertEquals('baz', $sessions[0]->_id); } public function testGc() { - $collection = $this->createMongoCollectionMock(); + $this->insertSession('foo', 'bar', -100_000); + $this->insertSession('bar', 'bar', -100_000); + $this->insertSession('qux', 'bar', -300); + $this->insertSession('baz', 'bar', 0); - $this->mongo->expects($this->once()) - ->method('selectCollection') - ->with($this->options['database'], $this->options['collection']) - ->willReturn($collection); - - $collection->expects($this->once()) - ->method('deleteMany') - ->willReturnCallback(function ($criteria) { - $this->assertInstanceOf(\MongoDB\BSON\UTCDateTime::class, $criteria[$this->options['expiry_field']]['$lt']); - $this->assertGreaterThanOrEqual(time() - 1, round((string) $criteria[$this->options['expiry_field']]['$lt'] / 1000)); - - $result = $this->createMock(\MongoDB\DeleteResult::class); - $result->method('getDeletedCount')->willReturn(42); - - return $result; - }); - - $this->assertSame(42, $this->storage->gc(1)); + $this->assertSame(2, $this->storage->gc(1)); + $this->assertCount(2, $this->getSessions()); } - public function testGetConnection() + /** + * @return list + */ + private function getSessions(): array { - $method = new \ReflectionMethod($this->storage, 'getMongo'); - - $this->assertInstanceOf(Client::class, $method->invoke($this->storage)); + return $this->manager->executeQuery(self::DABASE_NAME.'.'.self::COLLECTION_NAME, new Query([]))->toArray(); } - private function createMongoCollectionMock(): \MongoDB\Collection + private function insertSession(string $sessionId, string $data, int $timeDiff): void { - $collection = $this->getMockBuilder(\MongoDB\Collection::class) - ->disableOriginalConstructor() - ->getMock(); + $time = time() + $timeDiff; + + $write = new BulkWrite(); + $write->insert([ + '_id' => $sessionId, + 'data' => new Binary($data, Binary::TYPE_GENERIC), + 'time' => new UTCDateTime($time * 1000), + 'expires_at' => new UTCDateTime(($time + (int) \ini_get('session.gc_maxlifetime')) * 1000), + ]); - return $collection; + $this->manager->executeBulkWrite(self::DABASE_NAME.'.'.self::COLLECTION_NAME, $write); } } diff --git a/Tests/Session/Storage/Handler/stubs/mongodb.php b/Tests/Session/Storage/Handler/stubs/mongodb.php new file mode 100644 index 000000000..2cc31d55c --- /dev/null +++ b/Tests/Session/Storage/Handler/stubs/mongodb.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace MongoDB; + +use MongoDB\Driver\Manager; + +/* + * Stubs for the mongodb/mongodb library version ~1.16 + */ +if (!class_exists(Client::class, false)) { + abstract class Client + { + abstract public function getManager(): Manager; + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 162056865..66c8c1836 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -10,6 +10,7 @@ > + From 3f52221830721996b468198534aba2389b8dbfc5 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 23 Jan 2023 14:36:51 +0100 Subject: [PATCH 076/111] Remove full DSNs from exception messages --- Session/Storage/Handler/SessionHandlerFactory.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Session/Storage/Handler/SessionHandlerFactory.php b/Session/Storage/Handler/SessionHandlerFactory.php index 39dc30c6f..14454d0b8 100644 --- a/Session/Storage/Handler/SessionHandlerFactory.php +++ b/Session/Storage/Handler/SessionHandlerFactory.php @@ -63,7 +63,7 @@ public static function createHandler($connection): AbstractSessionHandler case str_starts_with($connection, 'rediss:'): case str_starts_with($connection, 'memcached:'): if (!class_exists(AbstractAdapter::class)) { - throw new \InvalidArgumentException(sprintf('Unsupported DSN "%s". Try running "composer require symfony/cache".', $connection)); + throw new \InvalidArgumentException('Unsupported Redis or Memcached DSN. Try running "composer require symfony/cache".'); } $handlerClass = str_starts_with($connection, 'memcached:') ? MemcachedSessionHandler::class : RedisSessionHandler::class; $connection = AbstractAdapter::createConnection($connection, ['lazy' => true]); @@ -72,7 +72,7 @@ public static function createHandler($connection): AbstractSessionHandler case str_starts_with($connection, 'pdo_oci://'): if (!class_exists(DriverManager::class)) { - throw new \InvalidArgumentException(sprintf('Unsupported DSN "%s". Try running "composer require doctrine/dbal".', $connection)); + throw new \InvalidArgumentException('Unsupported PDO OCI DSN. Try running "composer require doctrine/dbal".'); } $connection[3] = '-'; $params = class_exists(DsnParser::class) ? (new DsnParser())->parse($connection) : ['url' => $connection]; From c5b1134ce882ec5d8c3abb372f8b24c8c755ddbd Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 6 Nov 2023 16:34:32 +0100 Subject: [PATCH 077/111] Check whether secrets are empty and mark them all as sensitive --- UriSigner.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/UriSigner.php b/UriSigner.php index 091ac03e4..b04987724 100644 --- a/UriSigner.php +++ b/UriSigner.php @@ -12,8 +12,6 @@ namespace Symfony\Component\HttpFoundation; /** - * Signs URIs. - * * @author Fabien Potencier */ class UriSigner @@ -22,11 +20,14 @@ class UriSigner private string $parameter; /** - * @param string $secret A secret * @param string $parameter Query string parameter to use */ public function __construct(#[\SensitiveParameter] string $secret, string $parameter = '_hash') { + if (!$secret) { + throw new \InvalidArgumentException('A non-empty secret is required.'); + } + $this->secret = $secret; $this->parameter = $parameter; } From 7a12bc1dade63deb74dc04a82f157a3a944b0eb8 Mon Sep 17 00:00:00 2001 From: HypeMC Date: Sun, 5 Nov 2023 18:18:30 +0100 Subject: [PATCH 078/111] [Cache][HttpFoundation][Lock] Fix empty username/password for PDO PostgreSQL --- Session/Storage/Handler/PdoSessionHandler.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Session/Storage/Handler/PdoSessionHandler.php b/Session/Storage/Handler/PdoSessionHandler.php index cad7e0a72..f9c5d9b59 100644 --- a/Session/Storage/Handler/PdoSessionHandler.php +++ b/Session/Storage/Handler/PdoSessionHandler.php @@ -112,16 +112,16 @@ class PdoSessionHandler extends AbstractSessionHandler /** * Username when lazy-connect. * - * @var string + * @var string|null */ - private $username = ''; + private $username = null; /** * Password when lazy-connect. * - * @var string + * @var string|null */ - private $password = ''; + private $password = null; /** * Connection options when lazy-connect. From 4fbe1337a0309e117cc508906cdeb47900366d6c Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Thu, 2 Nov 2023 12:02:02 +0100 Subject: [PATCH 079/111] ensure string type with mbstring func overloading enabled --- HeaderUtils.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HeaderUtils.php b/HeaderUtils.php index f91c7e1d9..3456edace 100644 --- a/HeaderUtils.php +++ b/HeaderUtils.php @@ -256,7 +256,7 @@ public static function parseQuery(string $query, bool $ignoreBrackets = false, s private static function groupParts(array $matches, string $separators, bool $first = true): array { $separator = $separators[0]; - $separators = substr($separators, 1); + $separators = substr($separators, 1) ?: ''; $i = 0; if ('' === $separators && !$first) { From cbcd80a4c36f59772d62860fdb0cb6a38da63fd2 Mon Sep 17 00:00:00 2001 From: HypeMC Date: Tue, 7 Nov 2023 16:18:53 +0100 Subject: [PATCH 080/111] [Cache][Lock] Fix PDO store not creating table + add tests --- Session/Storage/Handler/SessionHandlerFactory.php | 1 + 1 file changed, 1 insertion(+) diff --git a/Session/Storage/Handler/SessionHandlerFactory.php b/Session/Storage/Handler/SessionHandlerFactory.php index 14454d0b8..76e4373f8 100644 --- a/Session/Storage/Handler/SessionHandlerFactory.php +++ b/Session/Storage/Handler/SessionHandlerFactory.php @@ -82,6 +82,7 @@ public static function createHandler($connection): AbstractSessionHandler } $connection = DriverManager::getConnection($params, $config); + // The condition should be removed once support for DBAL <3.3 is dropped $connection = method_exists($connection, 'getNativeConnection') ? $connection->getNativeConnection() : $connection->getWrappedConnection(); // no break; From 4da1713e88cf9c44bd4bf65f54772681222fcbec Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Wed, 27 Dec 2023 11:57:07 +0100 Subject: [PATCH 081/111] Fix typo --- RequestMatcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RequestMatcher.php b/RequestMatcher.php index f2645f9ae..521263447 100644 --- a/RequestMatcher.php +++ b/RequestMatcher.php @@ -91,7 +91,7 @@ public function matchHost(?string $regexp) } /** - * Adds a check for the the URL port. + * Adds a check for the URL port. * * @param int|null $port The port number to connect to */ From 528f59f15f32521f14f9d6af9fbd12409759f992 Mon Sep 17 00:00:00 2001 From: Priyadi Iman Nurcahyo Date: Wed, 3 Jan 2024 15:15:28 +0700 Subject: [PATCH 082/111] [HttpFoundation] Request without content-type or content-length header should result in null values, not empty strings --- ServerBag.php | 2 +- Tests/ServerBagTest.php | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/ServerBag.php b/ServerBag.php index 004af5708..831caa67e 100644 --- a/ServerBag.php +++ b/ServerBag.php @@ -31,7 +31,7 @@ public function getHeaders() foreach ($this->parameters as $key => $value) { if (str_starts_with($key, 'HTTP_')) { $headers[substr($key, 5)] = $value; - } elseif (\in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH', 'CONTENT_MD5'], true)) { + } elseif (\in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH', 'CONTENT_MD5'], true) && '' !== $value) { $headers[$key] = $value; } } diff --git a/Tests/ServerBagTest.php b/Tests/ServerBagTest.php index e26714bc4..3d675c512 100644 --- a/Tests/ServerBagTest.php +++ b/Tests/ServerBagTest.php @@ -177,4 +177,20 @@ public function testItDoesNotOverwriteTheAuthorizationHeaderIfItIsAlreadySet() 'PHP_AUTH_PW' => '', ], $bag->getHeaders()); } + + /** + * An HTTP request without content-type and content-length will result in + * the variables $_SERVER['CONTENT_TYPE'] and $_SERVER['CONTENT_LENGTH'] + * containing an empty string in PHP. + */ + public function testRequestWithoutContentTypeAndContentLength() + { + $bag = new ServerBag([ + 'CONTENT_TYPE' => '', + 'CONTENT_LENGTH' => '', + 'HTTP_USER_AGENT' => 'foo', + ]); + + $this->assertSame(['USER_AGENT' => 'foo'], $bag->getHeaders()); + } } From f2ab692a22aef1cd54beb893aa0068bdfb093928 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 23 Jan 2024 14:51:25 +0100 Subject: [PATCH 083/111] Apply php-cs-fixer fix --rules nullable_type_declaration_for_default_null_value --- BinaryFileResponse.php | 6 +++--- Cookie.php | 4 ++-- Exception/SessionNotFoundException.php | 2 +- File/File.php | 4 ++-- File/UploadedFile.php | 4 ++-- HeaderBag.php | 6 +++--- InputBag.php | 2 +- JsonResponse.php | 2 +- Request.php | 6 +++--- RequestMatcher.php | 2 +- Response.php | 10 +++++----- ResponseHeaderBag.php | 6 +++--- Session/Session.php | 6 +++--- Session/SessionFactory.php | 2 +- Session/SessionInterface.php | 4 ++-- Session/Storage/Handler/NativeFileSessionHandler.php | 2 +- Session/Storage/MetadataBag.php | 4 ++-- Session/Storage/MockArraySessionStorage.php | 6 +++--- Session/Storage/MockFileSessionStorage.php | 4 ++-- Session/Storage/MockFileSessionStorageFactory.php | 2 +- Session/Storage/NativeSessionStorage.php | 8 ++++---- Session/Storage/NativeSessionStorageFactory.php | 2 +- Session/Storage/PhpBridgeSessionStorage.php | 2 +- Session/Storage/PhpBridgeSessionStorageFactory.php | 2 +- Session/Storage/SessionStorageInterface.php | 2 +- StreamedResponse.php | 2 +- Test/Constraint/ResponseCookieValueSame.php | 2 +- Test/Constraint/ResponseHasCookie.php | 2 +- Tests/HeaderUtilsTest.php | 2 +- .../Session/Storage/Handler/PdoSessionHandlerTest.php | 2 +- 30 files changed, 55 insertions(+), 55 deletions(-) diff --git a/BinaryFileResponse.php b/BinaryFileResponse.php index d3caa36aa..1878caae1 100644 --- a/BinaryFileResponse.php +++ b/BinaryFileResponse.php @@ -45,7 +45,7 @@ class BinaryFileResponse extends Response * @param bool $autoEtag Whether the ETag header should be automatically set * @param bool $autoLastModified Whether the Last-Modified header should be automatically set */ - public function __construct($file, int $status = 200, array $headers = [], bool $public = true, string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true) + public function __construct($file, int $status = 200, array $headers = [], bool $public = true, ?string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true) { parent::__construct(null, $status, $headers); @@ -69,7 +69,7 @@ public function __construct($file, int $status = 200, array $headers = [], bool * * @deprecated since Symfony 5.2, use __construct() instead. */ - public static function create($file = null, int $status = 200, array $headers = [], bool $public = true, string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true) + public static function create($file = null, int $status = 200, array $headers = [], bool $public = true, ?string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true) { trigger_deprecation('symfony/http-foundation', '5.2', 'The "%s()" method is deprecated, use "new %s()" instead.', __METHOD__, static::class); @@ -85,7 +85,7 @@ public static function create($file = null, int $status = 200, array $headers = * * @throws FileException */ - public function setFile($file, string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true) + public function setFile($file, ?string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true) { if (!$file instanceof File) { if ($file instanceof \SplFileInfo) { diff --git a/Cookie.php b/Cookie.php index 91024535b..3ff93b9c1 100644 --- a/Cookie.php +++ b/Cookie.php @@ -71,7 +71,7 @@ public static function fromString(string $cookie, bool $decode = false) return new static($name, $value, $data['expires'], $data['path'], $data['domain'], $data['secure'], $data['httponly'], $data['raw'], $data['samesite']); } - public static function create(string $name, string $value = null, $expire = 0, ?string $path = '/', string $domain = null, bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = self::SAMESITE_LAX): self + public static function create(string $name, ?string $value = null, $expire = 0, ?string $path = '/', ?string $domain = null, ?bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = self::SAMESITE_LAX): self { return new self($name, $value, $expire, $path, $domain, $secure, $httpOnly, $raw, $sameSite); } @@ -89,7 +89,7 @@ public static function create(string $name, string $value = null, $expire = 0, ? * * @throws \InvalidArgumentException */ - public function __construct(string $name, string $value = null, $expire = 0, ?string $path = '/', string $domain = null, bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = 'lax') + public function __construct(string $name, ?string $value = null, $expire = 0, ?string $path = '/', ?string $domain = null, ?bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = 'lax') { // from PHP source code if ($raw && false !== strpbrk($name, self::RESERVED_CHARS_LIST)) { diff --git a/Exception/SessionNotFoundException.php b/Exception/SessionNotFoundException.php index 94b0cb69a..80a21bf15 100644 --- a/Exception/SessionNotFoundException.php +++ b/Exception/SessionNotFoundException.php @@ -20,7 +20,7 @@ */ class SessionNotFoundException extends \LogicException implements RequestExceptionInterface { - public function __construct(string $message = 'There is currently no session available.', int $code = 0, \Throwable $previous = null) + public function __construct(string $message = 'There is currently no session available.', int $code = 0, ?\Throwable $previous = null) { parent::__construct($message, $code, $previous); } diff --git a/File/File.php b/File/File.php index d941577d2..2deb53d6d 100644 --- a/File/File.php +++ b/File/File.php @@ -88,7 +88,7 @@ public function getMimeType() * * @throws FileException if the target file could not be created */ - public function move(string $directory, string $name = null) + public function move(string $directory, ?string $name = null) { $target = $this->getTargetFile($directory, $name); @@ -121,7 +121,7 @@ public function getContent(): string /** * @return self */ - protected function getTargetFile(string $directory, string $name = null) + protected function getTargetFile(string $directory, ?string $name = null) { if (!is_dir($directory)) { if (false === @mkdir($directory, 0777, true) && !is_dir($directory)) { diff --git a/File/UploadedFile.php b/File/UploadedFile.php index 1161556c4..6ff6e51a8 100644 --- a/File/UploadedFile.php +++ b/File/UploadedFile.php @@ -60,7 +60,7 @@ class UploadedFile extends File * @throws FileException If file_uploads is disabled * @throws FileNotFoundException If the file does not exist */ - public function __construct(string $path, string $originalName, string $mimeType = null, int $error = null, bool $test = false) + public function __construct(string $path, string $originalName, ?string $mimeType = null, ?int $error = null, bool $test = false) { $this->originalName = $this->getName($originalName); $this->mimeType = $mimeType ?: 'application/octet-stream'; @@ -172,7 +172,7 @@ public function isValid() * * @throws FileException if, for any reason, the file could not have been moved */ - public function move(string $directory, string $name = null) + public function move(string $directory, ?string $name = null) { if ($this->isValid()) { if ($this->test) { diff --git a/HeaderBag.php b/HeaderBag.php index 4683a6840..43d5f6327 100644 --- a/HeaderBag.php +++ b/HeaderBag.php @@ -67,7 +67,7 @@ public function __toString() * * @return array>|array */ - public function all(string $key = null) + public function all(?string $key = null) { if (null !== $key) { return $this->headers[strtr($key, self::UPPER, self::LOWER)] ?? []; @@ -110,7 +110,7 @@ public function add(array $headers) * * @return string|null */ - public function get(string $key, string $default = null) + public function get(string $key, ?string $default = null) { $headers = $this->all($key); @@ -197,7 +197,7 @@ public function remove(string $key) * * @throws \RuntimeException When the HTTP header is not parseable */ - public function getDate(string $key, \DateTime $default = null) + public function getDate(string $key, ?\DateTime $default = null) { if (null === $value = $this->get($key)) { return $default; diff --git a/InputBag.php b/InputBag.php index a9d3cd82a..356fbbc6f 100644 --- a/InputBag.php +++ b/InputBag.php @@ -45,7 +45,7 @@ public function get(string $key, $default = null) /** * {@inheritdoc} */ - public function all(string $key = null): array + public function all(?string $key = null): array { return parent::all($key); } diff --git a/JsonResponse.php b/JsonResponse.php index 501a6387d..51bdf1976 100644 --- a/JsonResponse.php +++ b/JsonResponse.php @@ -105,7 +105,7 @@ public static function fromJsonString(string $data, int $status = 200, array $he * * @throws \InvalidArgumentException When the callback name is not valid */ - public function setCallback(string $callback = null) + public function setCallback(?string $callback = null) { if (null !== $callback) { // partially taken from https://geekality.net/2011/08/03/valid-javascript-identifier/ diff --git a/Request.php b/Request.php index f8e342154..75db0300b 100644 --- a/Request.php +++ b/Request.php @@ -451,7 +451,7 @@ public static function setFactory(?callable $callable) * * @return static */ - public function duplicate(array $query = null, array $request = null, array $attributes = null, array $cookies = null, array $files = null, array $server = null) + public function duplicate(?array $query = null, ?array $request = null, ?array $attributes = null, ?array $cookies = null, ?array $files = null, ?array $server = null) { $dup = clone $this; if (null !== $query) { @@ -1651,7 +1651,7 @@ public function getPreferredFormat(?string $default = 'html'): ?string * * @return string|null */ - public function getPreferredLanguage(array $locales = null) + public function getPreferredLanguage(?array $locales = null) { $preferredLanguages = $this->getLanguages(); @@ -2061,7 +2061,7 @@ public function isFromTrustedProxy() return self::$trustedProxies && IpUtils::checkIp($this->server->get('REMOTE_ADDR', ''), self::$trustedProxies); } - private function getTrustedValues(int $type, string $ip = null): array + private function getTrustedValues(int $type, ?string $ip = null): array { $clientValues = []; $forwardedValues = []; diff --git a/RequestMatcher.php b/RequestMatcher.php index 521263447..03ccee97e 100644 --- a/RequestMatcher.php +++ b/RequestMatcher.php @@ -58,7 +58,7 @@ class RequestMatcher implements RequestMatcherInterface * @param string|string[]|null $ips * @param string|string[]|null $schemes */ - public function __construct(string $path = null, string $host = null, $methods = null, $ips = null, array $attributes = [], $schemes = null, int $port = null) + public function __construct(?string $path = null, ?string $host = null, $methods = null, $ips = null, array $attributes = [], $schemes = null, ?int $port = null) { $this->matchPath($path); $this->matchHost($host); diff --git a/Response.php b/Response.php index 23bfb2199..6798a04c8 100644 --- a/Response.php +++ b/Response.php @@ -463,7 +463,7 @@ public function getProtocolVersion(): string * * @final */ - public function setStatusCode(int $code, string $text = null): object + public function setStatusCode(int $code, ?string $text = null): object { $this->statusCode = $code; if ($this->isInvalid()) { @@ -737,7 +737,7 @@ public function getExpires(): ?\DateTimeInterface * * @final */ - public function setExpires(\DateTimeInterface $date = null): object + public function setExpires(?\DateTimeInterface $date = null): object { if (null === $date) { $this->headers->remove('Expires'); @@ -886,7 +886,7 @@ public function getLastModified(): ?\DateTimeInterface * * @final */ - public function setLastModified(\DateTimeInterface $date = null): object + public function setLastModified(?\DateTimeInterface $date = null): object { if (null === $date) { $this->headers->remove('Last-Modified'); @@ -924,7 +924,7 @@ public function getEtag(): ?string * * @final */ - public function setEtag(string $etag = null, bool $weak = false): object + public function setEtag(?string $etag = null, bool $weak = false): object { if (null === $etag) { $this->headers->remove('Etag'); @@ -1217,7 +1217,7 @@ public function isNotFound(): bool * * @final */ - public function isRedirect(string $location = null): bool + public function isRedirect(?string $location = null): bool { return \in_array($this->statusCode, [201, 301, 302, 303, 307, 308]) && (null === $location ?: $location == $this->headers->get('Location')); } diff --git a/ResponseHeaderBag.php b/ResponseHeaderBag.php index 1df13fa21..d4c4f393f 100644 --- a/ResponseHeaderBag.php +++ b/ResponseHeaderBag.php @@ -88,7 +88,7 @@ public function replace(array $headers = []) /** * {@inheritdoc} */ - public function all(string $key = null) + public function all(?string $key = null) { $headers = parent::all(); @@ -186,7 +186,7 @@ public function setCookie(Cookie $cookie) /** * Removes a cookie from the array, but does not unset it in the browser. */ - public function removeCookie(string $name, ?string $path = '/', string $domain = null) + public function removeCookie(string $name, ?string $path = '/', ?string $domain = null) { if (null === $path) { $path = '/'; @@ -239,7 +239,7 @@ public function getCookies(string $format = self::COOKIES_FLAT) /** * Clears a cookie in the browser. */ - public function clearCookie(string $name, ?string $path = '/', string $domain = null, bool $secure = false, bool $httpOnly = true, string $sameSite = null) + public function clearCookie(string $name, ?string $path = '/', ?string $domain = null, bool $secure = false, bool $httpOnly = true, ?string $sameSite = null) { $this->setCookie(new Cookie($name, null, 1, $path, $domain, $secure, $httpOnly, false, $sameSite)); } diff --git a/Session/Session.php b/Session/Session.php index 022e3986f..917920a46 100644 --- a/Session/Session.php +++ b/Session/Session.php @@ -39,7 +39,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable private $usageIndex = 0; private $usageReporter; - public function __construct(SessionStorageInterface $storage = null, AttributeBagInterface $attributes = null, FlashBagInterface $flashes = null, callable $usageReporter = null) + public function __construct(?SessionStorageInterface $storage = null, ?AttributeBagInterface $attributes = null, ?FlashBagInterface $flashes = null, ?callable $usageReporter = null) { $this->storage = $storage ?? new NativeSessionStorage(); $this->usageReporter = $usageReporter; @@ -175,7 +175,7 @@ public function isEmpty(): bool /** * {@inheritdoc} */ - public function invalidate(int $lifetime = null) + public function invalidate(?int $lifetime = null) { $this->storage->clear(); @@ -185,7 +185,7 @@ public function invalidate(int $lifetime = null) /** * {@inheritdoc} */ - public function migrate(bool $destroy = false, int $lifetime = null) + public function migrate(bool $destroy = false, ?int $lifetime = null) { return $this->storage->regenerate($destroy, $lifetime); } diff --git a/Session/SessionFactory.php b/Session/SessionFactory.php index 04c4b06a0..bd79282ee 100644 --- a/Session/SessionFactory.php +++ b/Session/SessionFactory.php @@ -26,7 +26,7 @@ class SessionFactory implements SessionFactoryInterface private $storageFactory; private $usageReporter; - public function __construct(RequestStack $requestStack, SessionStorageFactoryInterface $storageFactory, callable $usageReporter = null) + public function __construct(RequestStack $requestStack, SessionStorageFactoryInterface $storageFactory, ?callable $usageReporter = null) { $this->requestStack = $requestStack; $this->storageFactory = $storageFactory; diff --git a/Session/SessionInterface.php b/Session/SessionInterface.php index e67338337..b73dfd0c3 100644 --- a/Session/SessionInterface.php +++ b/Session/SessionInterface.php @@ -66,7 +66,7 @@ public function setName(string $name); * * @return bool */ - public function invalidate(int $lifetime = null); + public function invalidate(?int $lifetime = null); /** * Migrates the current session to a new session id while maintaining all @@ -80,7 +80,7 @@ public function invalidate(int $lifetime = null); * * @return bool */ - public function migrate(bool $destroy = false, int $lifetime = null); + public function migrate(bool $destroy = false, ?int $lifetime = null); /** * Force the session to be saved and closed. diff --git a/Session/Storage/Handler/NativeFileSessionHandler.php b/Session/Storage/Handler/NativeFileSessionHandler.php index 52a103879..570d4f427 100644 --- a/Session/Storage/Handler/NativeFileSessionHandler.php +++ b/Session/Storage/Handler/NativeFileSessionHandler.php @@ -28,7 +28,7 @@ class NativeFileSessionHandler extends \SessionHandler * @throws \InvalidArgumentException On invalid $savePath * @throws \RuntimeException When failing to create the save directory */ - public function __construct(string $savePath = null) + public function __construct(?string $savePath = null) { if (null === $savePath) { $savePath = \ini_get('session.save_path'); diff --git a/Session/Storage/MetadataBag.php b/Session/Storage/MetadataBag.php index 52d332094..3e10f6dbc 100644 --- a/Session/Storage/MetadataBag.php +++ b/Session/Storage/MetadataBag.php @@ -100,7 +100,7 @@ public function getLifetime() * to expire with browser session. Time is in seconds, and is * not a Unix timestamp. */ - public function stampNew(int $lifetime = null) + public function stampNew(?int $lifetime = null) { $this->stampCreated($lifetime); } @@ -158,7 +158,7 @@ public function setName(string $name) $this->name = $name; } - private function stampCreated(int $lifetime = null): void + private function stampCreated(?int $lifetime = null): void { $timeStamp = time(); $this->meta[self::CREATED] = $this->meta[self::UPDATED] = $this->lastUsed = $timeStamp; diff --git a/Session/Storage/MockArraySessionStorage.php b/Session/Storage/MockArraySessionStorage.php index c5c2bb073..77bb38f15 100644 --- a/Session/Storage/MockArraySessionStorage.php +++ b/Session/Storage/MockArraySessionStorage.php @@ -62,7 +62,7 @@ class MockArraySessionStorage implements SessionStorageInterface */ protected $bags = []; - public function __construct(string $name = 'MOCKSESSID', MetadataBag $metaBag = null) + public function __construct(string $name = 'MOCKSESSID', ?MetadataBag $metaBag = null) { $this->name = $name; $this->setMetadataBag($metaBag); @@ -94,7 +94,7 @@ public function start() /** * {@inheritdoc} */ - public function regenerate(bool $destroy = false, int $lifetime = null) + public function regenerate(bool $destroy = false, ?int $lifetime = null) { if (!$this->started) { $this->start(); @@ -204,7 +204,7 @@ public function isStarted() return $this->started; } - public function setMetadataBag(MetadataBag $bag = null) + public function setMetadataBag(?MetadataBag $bag = null) { if (null === $bag) { $bag = new MetadataBag(); diff --git a/Session/Storage/MockFileSessionStorage.php b/Session/Storage/MockFileSessionStorage.php index 8e32a45e3..8aeb9724c 100644 --- a/Session/Storage/MockFileSessionStorage.php +++ b/Session/Storage/MockFileSessionStorage.php @@ -30,7 +30,7 @@ class MockFileSessionStorage extends MockArraySessionStorage /** * @param string|null $savePath Path of directory to save session files */ - public function __construct(string $savePath = null, string $name = 'MOCKSESSID', MetadataBag $metaBag = null) + public function __construct(?string $savePath = null, string $name = 'MOCKSESSID', ?MetadataBag $metaBag = null) { if (null === $savePath) { $savePath = sys_get_temp_dir(); @@ -68,7 +68,7 @@ public function start() /** * {@inheritdoc} */ - public function regenerate(bool $destroy = false, int $lifetime = null) + public function regenerate(bool $destroy = false, ?int $lifetime = null) { if (!$this->started) { $this->start(); diff --git a/Session/Storage/MockFileSessionStorageFactory.php b/Session/Storage/MockFileSessionStorageFactory.php index d0da1e169..900fa7cfa 100644 --- a/Session/Storage/MockFileSessionStorageFactory.php +++ b/Session/Storage/MockFileSessionStorageFactory.php @@ -28,7 +28,7 @@ class MockFileSessionStorageFactory implements SessionStorageFactoryInterface /** * @see MockFileSessionStorage constructor. */ - public function __construct(string $savePath = null, string $name = 'MOCKSESSID', MetadataBag $metaBag = null) + public function __construct(?string $savePath = null, string $name = 'MOCKSESSID', ?MetadataBag $metaBag = null) { $this->savePath = $savePath; $this->name = $name; diff --git a/Session/Storage/NativeSessionStorage.php b/Session/Storage/NativeSessionStorage.php index 242478c42..e7b42ed0b 100644 --- a/Session/Storage/NativeSessionStorage.php +++ b/Session/Storage/NativeSessionStorage.php @@ -97,7 +97,7 @@ class NativeSessionStorage implements SessionStorageInterface * * @param AbstractProxy|\SessionHandlerInterface|null $handler */ - public function __construct(array $options = [], $handler = null, MetadataBag $metaBag = null) + public function __construct(array $options = [], $handler = null, ?MetadataBag $metaBag = null) { if (!\extension_loaded('session')) { throw new \LogicException('PHP extension "session" is required.'); @@ -233,7 +233,7 @@ public function setName(string $name) /** * {@inheritdoc} */ - public function regenerate(bool $destroy = false, int $lifetime = null) + public function regenerate(bool $destroy = false, ?int $lifetime = null) { // Cannot regenerate the session ID for non-active sessions. if (\PHP_SESSION_ACTIVE !== session_status()) { @@ -355,7 +355,7 @@ public function getBag(string $name) return $this->bags[$name]; } - public function setMetadataBag(MetadataBag $metaBag = null) + public function setMetadataBag(?MetadataBag $metaBag = null) { if (null === $metaBag) { $metaBag = new MetadataBag(); @@ -487,7 +487,7 @@ public function setSaveHandler($saveHandler = null) * PHP takes the return value from the read() handler, unserializes it * and populates $_SESSION with the result automatically. */ - protected function loadSession(array &$session = null) + protected function loadSession(?array &$session = null) { if (null === $session) { $session = &$_SESSION; diff --git a/Session/Storage/NativeSessionStorageFactory.php b/Session/Storage/NativeSessionStorageFactory.php index a7d7411ff..48e65267e 100644 --- a/Session/Storage/NativeSessionStorageFactory.php +++ b/Session/Storage/NativeSessionStorageFactory.php @@ -29,7 +29,7 @@ class NativeSessionStorageFactory implements SessionStorageFactoryInterface /** * @see NativeSessionStorage constructor. */ - public function __construct(array $options = [], $handler = null, MetadataBag $metaBag = null, bool $secure = false) + public function __construct(array $options = [], $handler = null, ?MetadataBag $metaBag = null, bool $secure = false) { $this->options = $options; $this->handler = $handler; diff --git a/Session/Storage/PhpBridgeSessionStorage.php b/Session/Storage/PhpBridgeSessionStorage.php index 72dbef134..855d5e111 100644 --- a/Session/Storage/PhpBridgeSessionStorage.php +++ b/Session/Storage/PhpBridgeSessionStorage.php @@ -23,7 +23,7 @@ class PhpBridgeSessionStorage extends NativeSessionStorage /** * @param AbstractProxy|\SessionHandlerInterface|null $handler */ - public function __construct($handler = null, MetadataBag $metaBag = null) + public function __construct($handler = null, ?MetadataBag $metaBag = null) { if (!\extension_loaded('session')) { throw new \LogicException('PHP extension "session" is required.'); diff --git a/Session/Storage/PhpBridgeSessionStorageFactory.php b/Session/Storage/PhpBridgeSessionStorageFactory.php index 173ef71de..aa9326322 100644 --- a/Session/Storage/PhpBridgeSessionStorageFactory.php +++ b/Session/Storage/PhpBridgeSessionStorageFactory.php @@ -28,7 +28,7 @@ class PhpBridgeSessionStorageFactory implements SessionStorageFactoryInterface /** * @see PhpBridgeSessionStorage constructor. */ - public function __construct($handler = null, MetadataBag $metaBag = null, bool $secure = false) + public function __construct($handler = null, ?MetadataBag $metaBag = null, bool $secure = false) { $this->handler = $handler; $this->metaBag = $metaBag; diff --git a/Session/Storage/SessionStorageInterface.php b/Session/Storage/SessionStorageInterface.php index 705374552..70b7c6a15 100644 --- a/Session/Storage/SessionStorageInterface.php +++ b/Session/Storage/SessionStorageInterface.php @@ -90,7 +90,7 @@ public function setName(string $name); * * @throws \RuntimeException If an error occurs while regenerating this storage */ - public function regenerate(bool $destroy = false, int $lifetime = null); + public function regenerate(bool $destroy = false, ?int $lifetime = null); /** * Force the session to be saved and closed. diff --git a/StreamedResponse.php b/StreamedResponse.php index 0599bd1e4..b42330dcd 100644 --- a/StreamedResponse.php +++ b/StreamedResponse.php @@ -30,7 +30,7 @@ class StreamedResponse extends Response protected $streamed; private $headersSent; - public function __construct(callable $callback = null, int $status = 200, array $headers = []) + public function __construct(?callable $callback = null, int $status = 200, array $headers = []) { parent::__construct(null, $status, $headers); diff --git a/Test/Constraint/ResponseCookieValueSame.php b/Test/Constraint/ResponseCookieValueSame.php index eb9c26a3b..939925b98 100644 --- a/Test/Constraint/ResponseCookieValueSame.php +++ b/Test/Constraint/ResponseCookieValueSame.php @@ -22,7 +22,7 @@ final class ResponseCookieValueSame extends Constraint private $path; private $domain; - public function __construct(string $name, string $value, string $path = '/', string $domain = null) + public function __construct(string $name, string $value, string $path = '/', ?string $domain = null) { $this->name = $name; $this->value = $value; diff --git a/Test/Constraint/ResponseHasCookie.php b/Test/Constraint/ResponseHasCookie.php index eae9e271b..9d6e58c8d 100644 --- a/Test/Constraint/ResponseHasCookie.php +++ b/Test/Constraint/ResponseHasCookie.php @@ -21,7 +21,7 @@ final class ResponseHasCookie extends Constraint private $path; private $domain; - public function __construct(string $name, string $path = '/', string $domain = null) + public function __construct(string $name, string $path = '/', ?string $domain = null) { $this->name = $name; $this->path = $path; diff --git a/Tests/HeaderUtilsTest.php b/Tests/HeaderUtilsTest.php index befa4aea0..3279b9a53 100644 --- a/Tests/HeaderUtilsTest.php +++ b/Tests/HeaderUtilsTest.php @@ -149,7 +149,7 @@ public static function provideMakeDispositionFail() /** * @dataProvider provideParseQuery */ - public function testParseQuery(string $query, string $expected = null) + public function testParseQuery(string $query, ?string $expected = null) { $this->assertSame($expected ?? $query, http_build_query(HeaderUtils::parseQuery($query), '', '&')); } diff --git a/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php b/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php index 4403cda3d..5b5f660c4 100644 --- a/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php @@ -368,7 +368,7 @@ class MockPdo extends \PDO private $driverName; private $errorMode; - public function __construct(string $driverName = null, int $errorMode = null) + public function __construct(?string $driverName = null, ?int $errorMode = null) { $this->driverName = $driverName; $this->errorMode = null !== $errorMode ?: \PDO::ERRMODE_EXCEPTION; From d68e6995538e9a2e7ad6ec876cecede6fc3274d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Isaert?= Date: Wed, 31 Jan 2024 14:12:26 +0100 Subject: [PATCH 084/111] [HttpFoundation] Fix clearing CHIPS cookies --- ResponseHeaderBag.php | 8 ++++++-- Tests/ResponseHeaderBagTest.php | 8 ++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/ResponseHeaderBag.php b/ResponseHeaderBag.php index 80d267553..376357d01 100644 --- a/ResponseHeaderBag.php +++ b/ResponseHeaderBag.php @@ -234,11 +234,15 @@ public function getCookies(string $format = self::COOKIES_FLAT): array /** * Clears a cookie in the browser. * + * @param bool $partitioned + * * @return void */ - public function clearCookie(string $name, ?string $path = '/', ?string $domain = null, bool $secure = false, bool $httpOnly = true, ?string $sameSite = null) + public function clearCookie(string $name, ?string $path = '/', ?string $domain = null, bool $secure = false, bool $httpOnly = true, ?string $sameSite = null /* , bool $partitioned = false */) { - $this->setCookie(new Cookie($name, null, 1, $path, $domain, $secure, $httpOnly, false, $sameSite)); + $partitioned = 6 < \func_num_args() ? \func_get_arg(6) : false; + + $this->setCookie(new Cookie($name, null, 1, $path, $domain, $secure, $httpOnly, false, $sameSite, $partitioned)); } /** diff --git a/Tests/ResponseHeaderBagTest.php b/Tests/ResponseHeaderBagTest.php index 8165e4374..9e61dd684 100644 --- a/Tests/ResponseHeaderBagTest.php +++ b/Tests/ResponseHeaderBagTest.php @@ -136,6 +136,14 @@ public function testClearCookieSamesite() $this->assertSetCookieHeader('foo=deleted; expires='.gmdate('D, d M Y H:i:s T', time() - 31536001).'; Max-Age=0; path=/; secure; samesite=none', $bag); } + public function testClearCookiePartitioned() + { + $bag = new ResponseHeaderBag([]); + + $bag->clearCookie('foo', '/', null, true, false, 'none', true); + $this->assertSetCookieHeader('foo=deleted; expires='.gmdate('D, d M Y H:i:s T', time() - 31536001).'; Max-Age=0; path=/; secure; samesite=none; partitioned', $bag); + } + public function testReplace() { $bag = new ResponseHeaderBag([]); From ebc713bc6e6f4b53f46539fc158be85dfcd77304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Fri, 2 Feb 2024 16:32:09 +0100 Subject: [PATCH 085/111] [HttpFoundation] Prevent duplicated headers when using Early Hints --- Response.php | 24 +++++++------- .../response-functional/early_hints.php | 31 +++++++++++++++++++ Tests/ResponseFunctionalTest.php | 28 ++++++++++++++++- 3 files changed, 69 insertions(+), 14 deletions(-) create mode 100644 Tests/Fixtures/response-functional/early_hints.php diff --git a/Response.php b/Response.php index d67c8f726..a43e7a9ac 100644 --- a/Response.php +++ b/Response.php @@ -355,23 +355,21 @@ public function sendHeaders(/* int $statusCode = null */): static $replace = false; // As recommended by RFC 8297, PHP automatically copies headers from previous 103 responses, we need to deal with that if headers changed - if (103 === $statusCode) { - $previousValues = $this->sentHeaders[$name] ?? null; - if ($previousValues === $values) { - // Header already sent in a previous response, it will be automatically copied in this response by PHP - continue; - } + $previousValues = $this->sentHeaders[$name] ?? null; + if ($previousValues === $values) { + // Header already sent in a previous response, it will be automatically copied in this response by PHP + continue; + } - $replace = 0 === strcasecmp($name, 'Content-Type'); + $replace = 0 === strcasecmp($name, 'Content-Type'); - if (null !== $previousValues && array_diff($previousValues, $values)) { - header_remove($name); - $previousValues = null; - } - - $newValues = null === $previousValues ? $values : array_diff($values, $previousValues); + if (null !== $previousValues && array_diff($previousValues, $values)) { + header_remove($name); + $previousValues = null; } + $newValues = null === $previousValues ? $values : array_diff($values, $previousValues); + foreach ($newValues as $value) { header($name.': '.$value, $replace, $this->statusCode); } diff --git a/Tests/Fixtures/response-functional/early_hints.php b/Tests/Fixtures/response-functional/early_hints.php new file mode 100644 index 000000000..90294d9ae --- /dev/null +++ b/Tests/Fixtures/response-functional/early_hints.php @@ -0,0 +1,31 @@ +headers->set('Link', '; rel="preload"; as="style"'); +$r->sendHeaders(103); + +$r->headers->set('Link', '; rel="preload"; as="script"', false); +$r->sendHeaders(103); + +$r->setContent('Hello, Early Hints'); +$r->send(); diff --git a/Tests/ResponseFunctionalTest.php b/Tests/ResponseFunctionalTest.php index ccda147df..1b3566a2c 100644 --- a/Tests/ResponseFunctionalTest.php +++ b/Tests/ResponseFunctionalTest.php @@ -12,6 +12,8 @@ namespace Symfony\Component\HttpFoundation\Tests; use PHPUnit\Framework\TestCase; +use Symfony\Component\Process\ExecutableFinder; +use Symfony\Component\Process\Process; class ResponseFunctionalTest extends TestCase { @@ -51,7 +53,31 @@ public function testCookie($fixture) public static function provideCookie() { foreach (glob(__DIR__.'/Fixtures/response-functional/*.php') as $file) { - yield [pathinfo($file, \PATHINFO_FILENAME)]; + if (str_contains($file, 'cookie')) { + yield [pathinfo($file, \PATHINFO_FILENAME)]; + } } } + + /** + * @group integration + */ + public function testInformationalResponse() + { + if (!(new ExecutableFinder())->find('curl')) { + $this->markTestSkipped('curl is not installed'); + } + + if (!($fp = @fsockopen('localhost', 80, $errorCode, $errorMessage, 2))) { + $this->markTestSkipped('FrankenPHP is not running'); + } + fclose($fp); + + $p = new Process(['curl', '-v', 'http://localhost/early_hints.php']); + $p->run(); + $output = $p->getErrorOutput(); + + $this->assertSame(3, preg_match_all('#Link: ; rel="preload"; as="style"#', $output)); + $this->assertSame(2, preg_match_all('#Link: ; rel="preload"; as="script"#', $output)); + } } From d8c13d35f68c69e15595fe37fa2c225d11c10f7e Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 19 Mar 2024 11:13:26 +0100 Subject: [PATCH 086/111] Make more nullable types explicit --- ParameterBag.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ParameterBag.php b/ParameterBag.php index e1f89d69e..b542292bc 100644 --- a/ParameterBag.php +++ b/ParameterBag.php @@ -39,7 +39,7 @@ public function __construct(array $parameters = []) * * @return array */ - public function all(/* string $key = null */) + public function all(/* ?string $key = null */) { $key = \func_num_args() > 0 ? func_get_arg(0) : null; From 3c8deb7540bb59d6a773812f01004c2883d5c5c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Sat, 6 Apr 2024 01:54:31 +0200 Subject: [PATCH 087/111] [HttpFoundation] Set content-type header in RedirectResponse --- RedirectResponse.php | 1 + Tests/RedirectResponseTest.php | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/RedirectResponse.php b/RedirectResponse.php index 2103280c6..7b89f0faf 100644 --- a/RedirectResponse.php +++ b/RedirectResponse.php @@ -103,6 +103,7 @@ public function setTargetUrl(string $url) ', htmlspecialchars($url, \ENT_QUOTES, 'UTF-8'))); $this->headers->set('Location', $url); + $this->headers->set('Content-Type', 'text/html; charset=utf-8'); return $this; } diff --git a/Tests/RedirectResponseTest.php b/Tests/RedirectResponseTest.php index 3d2f05ffc..483bad20c 100644 --- a/Tests/RedirectResponseTest.php +++ b/Tests/RedirectResponseTest.php @@ -44,6 +44,13 @@ public function testGenerateLocationHeader() $this->assertEquals('foo.bar', $response->headers->get('Location')); } + public function testGenerateContentTypeHeader() + { + $response = new RedirectResponse('foo.bar'); + + $this->assertSame('text/html; charset=utf-8', $response->headers->get('Content-Type')); + } + public function testGetTargetUrl() { $response = new RedirectResponse('foo.bar'); From 3356c93efc30b0c85a37606bdfef16b813faec0e Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 18 Apr 2024 09:55:03 +0200 Subject: [PATCH 088/111] Auto-close PRs on subtree-splits --- .gitattributes | 3 +- .github/PULL_REQUEST_TEMPLATE.md | 8 +++++ .github/workflows/check-subtree-split.yml | 37 +++++++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/check-subtree-split.yml diff --git a/.gitattributes b/.gitattributes index 84c7add05..14c3c3594 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,3 @@ /Tests export-ignore /phpunit.xml.dist export-ignore -/.gitattributes export-ignore -/.gitignore export-ignore +/.git* export-ignore diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..4689c4dad --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ +Please do not submit any Pull Requests here. They will be closed. +--- + +Please submit your PR here instead: +https://github.com/symfony/symfony + +This repository is what we call a "subtree split": a read-only subset of that main repository. +We're looking forward to your PR there! diff --git a/.github/workflows/check-subtree-split.yml b/.github/workflows/check-subtree-split.yml new file mode 100644 index 000000000..16be48bae --- /dev/null +++ b/.github/workflows/check-subtree-split.yml @@ -0,0 +1,37 @@ +name: Check subtree split + +on: + pull_request_target: + +jobs: + close-pull-request: + runs-on: ubuntu-latest + + steps: + - name: Close pull request + uses: actions/github-script@v6 + with: + script: | + if (context.repo.owner === "symfony") { + github.rest.issues.createComment({ + owner: "symfony", + repo: context.repo.repo, + issue_number: context.issue.number, + body: ` + Thanks for your Pull Request! We love contributions. + + However, you should instead open your PR on the main repository: + https://github.com/symfony/symfony + + This repository is what we call a "subtree split": a read-only subset of that main repository. + We're looking forward to your PR there! + ` + }); + + github.rest.pulls.update({ + owner: "symfony", + repo: context.repo.repo, + pull_number: context.issue.number, + state: "closed" + }); + } From fd44aca749f80491192a5d7d834ba5fb9b37a120 Mon Sep 17 00:00:00 2001 From: mfettig Date: Fri, 24 Mar 2023 15:07:33 -0400 Subject: [PATCH 089/111] [Cache] Fix support for predis/predis:^2.0 --- .../Storage/Handler/PredisClusterSessionHandlerTest.php | 5 ++++- composer.json | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Tests/Session/Storage/Handler/PredisClusterSessionHandlerTest.php b/Tests/Session/Storage/Handler/PredisClusterSessionHandlerTest.php index 4990b1a1f..1712dcc49 100644 --- a/Tests/Session/Storage/Handler/PredisClusterSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/PredisClusterSessionHandlerTest.php @@ -23,6 +23,9 @@ class PredisClusterSessionHandlerTest extends AbstractRedisSessionHandlerTestCas */ protected function createRedisClient(string $host): object { - return new Client([array_combine(['host', 'port'], explode(':', getenv('REDIS_HOST')) + [1 => 6379])]); + return new Client( + [array_combine(['host', 'port'], explode(':', getenv('REDIS_HOST')) + [1 => 6379])], + ['cluster' => 'redis'] + ); } } diff --git a/composer.json b/composer.json index cb8d59ffe..a2e43a99c 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "symfony/polyfill-php80": "^1.16" }, "require-dev": { - "predis/predis": "~1.0", + "predis/predis": "^1.0|^2.0", "symfony/cache": "^4.4|^5.0|^6.0", "symfony/dependency-injection": "^5.4|^6.0", "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4", From a2622e3e62a9d959e3fdab287f13df4c0866cf5a Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Mon, 29 Apr 2024 16:31:15 +0200 Subject: [PATCH 090/111] Remove calls to `TestCase::iniSet()` and calls to deprecated methods of `MockBuilder` --- .../Storage/NativeSessionStorageTest.php | 63 ++++++++++++------- .../Storage/PhpBridgeSessionStorageTest.php | 10 ++- 2 files changed, 49 insertions(+), 24 deletions(-) diff --git a/Tests/Session/Storage/NativeSessionStorageTest.php b/Tests/Session/Storage/NativeSessionStorageTest.php index adf074e36..c67b1391d 100644 --- a/Tests/Session/Storage/NativeSessionStorageTest.php +++ b/Tests/Session/Storage/NativeSessionStorageTest.php @@ -34,10 +34,14 @@ class NativeSessionStorageTest extends TestCase { private $savePath; + private $initialSessionSaveHandler; + private $initialSessionSavePath; + protected function setUp(): void { - $this->iniSet('session.save_handler', 'files'); - $this->iniSet('session.save_path', $this->savePath = sys_get_temp_dir().'/sftest'); + $this->initialSessionSaveHandler = ini_set('session.save_handler', 'files'); + $this->initialSessionSavePath = ini_set('session.save_path', $this->savePath = sys_get_temp_dir().'/sftest'); + if (!is_dir($this->savePath)) { mkdir($this->savePath); } @@ -52,6 +56,8 @@ protected function tearDown(): void } $this->savePath = null; + ini_set('session.save_handler', $this->initialSessionSaveHandler); + ini_set('session.save_path', $this->initialSessionSavePath); } protected function getStorage(array $options = []): NativeSessionStorage @@ -154,18 +160,26 @@ public function testRegenerationFailureDoesNotFlagStorageAsStarted() public function testDefaultSessionCacheLimiter() { - $this->iniSet('session.cache_limiter', 'nocache'); + $initialLimiter = ini_set('session.cache_limiter', 'nocache'); - new NativeSessionStorage(); - $this->assertEquals('', \ini_get('session.cache_limiter')); + try { + new NativeSessionStorage(); + $this->assertEquals('', \ini_get('session.cache_limiter')); + } finally { + ini_set('session.cache_limiter', $initialLimiter); + } } public function testExplicitSessionCacheLimiter() { - $this->iniSet('session.cache_limiter', 'nocache'); + $initialLimiter = ini_set('session.cache_limiter', 'nocache'); - new NativeSessionStorage(['cache_limiter' => 'public']); - $this->assertEquals('public', \ini_get('session.cache_limiter')); + try { + new NativeSessionStorage(['cache_limiter' => 'public']); + $this->assertEquals('public', \ini_get('session.cache_limiter')); + } finally { + ini_set('session.cache_limiter', $initialLimiter); + } } public function testCookieOptions() @@ -208,20 +222,25 @@ public function testSessionOptions() public function testSetSaveHandler() { - $this->iniSet('session.save_handler', 'files'); - $storage = $this->getStorage(); - $storage->setSaveHandler(); - $this->assertInstanceOf(SessionHandlerProxy::class, $storage->getSaveHandler()); - $storage->setSaveHandler(null); - $this->assertInstanceOf(SessionHandlerProxy::class, $storage->getSaveHandler()); - $storage->setSaveHandler(new SessionHandlerProxy(new NativeFileSessionHandler())); - $this->assertInstanceOf(SessionHandlerProxy::class, $storage->getSaveHandler()); - $storage->setSaveHandler(new NativeFileSessionHandler()); - $this->assertInstanceOf(SessionHandlerProxy::class, $storage->getSaveHandler()); - $storage->setSaveHandler(new SessionHandlerProxy(new NullSessionHandler())); - $this->assertInstanceOf(SessionHandlerProxy::class, $storage->getSaveHandler()); - $storage->setSaveHandler(new NullSessionHandler()); - $this->assertInstanceOf(SessionHandlerProxy::class, $storage->getSaveHandler()); + $initialSaveHandler = ini_set('session.save_handler', 'files'); + + try { + $storage = $this->getStorage(); + $storage->setSaveHandler(); + $this->assertInstanceOf(SessionHandlerProxy::class, $storage->getSaveHandler()); + $storage->setSaveHandler(null); + $this->assertInstanceOf(SessionHandlerProxy::class, $storage->getSaveHandler()); + $storage->setSaveHandler(new SessionHandlerProxy(new NativeFileSessionHandler())); + $this->assertInstanceOf(SessionHandlerProxy::class, $storage->getSaveHandler()); + $storage->setSaveHandler(new NativeFileSessionHandler()); + $this->assertInstanceOf(SessionHandlerProxy::class, $storage->getSaveHandler()); + $storage->setSaveHandler(new SessionHandlerProxy(new NullSessionHandler())); + $this->assertInstanceOf(SessionHandlerProxy::class, $storage->getSaveHandler()); + $storage->setSaveHandler(new NullSessionHandler()); + $this->assertInstanceOf(SessionHandlerProxy::class, $storage->getSaveHandler()); + } finally { + ini_set('session.save_handler', $initialSaveHandler); + } } public function testStarted() diff --git a/Tests/Session/Storage/PhpBridgeSessionStorageTest.php b/Tests/Session/Storage/PhpBridgeSessionStorageTest.php index e2fb93ebc..80d656518 100644 --- a/Tests/Session/Storage/PhpBridgeSessionStorageTest.php +++ b/Tests/Session/Storage/PhpBridgeSessionStorageTest.php @@ -30,10 +30,14 @@ class PhpBridgeSessionStorageTest extends TestCase { private $savePath; + private $initialSessionSaveHandler; + private $initialSessionSavePath; + protected function setUp(): void { - $this->iniSet('session.save_handler', 'files'); - $this->iniSet('session.save_path', $this->savePath = sys_get_temp_dir().'/sftest'); + $this->initialSessionSaveHandler = ini_set('session.save_handler', 'files'); + $this->initialSessionSavePath = ini_set('session.save_path', $this->savePath = sys_get_temp_dir().'/sftest'); + if (!is_dir($this->savePath)) { mkdir($this->savePath); } @@ -48,6 +52,8 @@ protected function tearDown(): void } $this->savePath = null; + ini_set('session.save_handler', $this->initialSessionSaveHandler); + ini_set('session.save_path', $this->initialSessionSavePath); } protected function getStorage(): PhpBridgeSessionStorage From c5a5143347ca85aac4087b1ee5126a31c9ac39c4 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 13 May 2024 16:58:31 +0200 Subject: [PATCH 091/111] add test for JSON response with null as content --- Tests/JsonResponseTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Tests/JsonResponseTest.php b/Tests/JsonResponseTest.php index 47facb776..6a1402fed 100644 --- a/Tests/JsonResponseTest.php +++ b/Tests/JsonResponseTest.php @@ -294,6 +294,14 @@ public function testConstructorWithObjectWithoutToStringMethodThrowsAnException( new JsonResponse(new \stdClass(), 200, [], true); } + + public function testSetDataWithNull() + { + $response = new JsonResponse(); + $response->setData(null); + + $this->assertSame('null', $response->getContent()); + } } class JsonSerializableObject implements \JsonSerializable From b4a91a17d1b7d283773f6a3b4bba957461d71fac Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Fri, 26 Apr 2024 10:58:17 +0200 Subject: [PATCH 092/111] Remove calls to `getMockForAbstractClass()` --- Tests/Session/Storage/Proxy/AbstractProxyTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Session/Storage/Proxy/AbstractProxyTest.php b/Tests/Session/Storage/Proxy/AbstractProxyTest.php index fde7a4a0a..742779c50 100644 --- a/Tests/Session/Storage/Proxy/AbstractProxyTest.php +++ b/Tests/Session/Storage/Proxy/AbstractProxyTest.php @@ -29,7 +29,7 @@ class AbstractProxyTest extends TestCase protected function setUp(): void { - $this->proxy = $this->getMockForAbstractClass(AbstractProxy::class); + $this->proxy = new class() extends AbstractProxy {}; } protected function tearDown(): void From fdd485ea991a7df9823874e1158149c7fa22ce52 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 13 May 2024 08:48:02 +0200 Subject: [PATCH 093/111] filter out empty HTTP header parts --- HeaderUtils.php | 6 +++++- Tests/AcceptHeaderTest.php | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/HeaderUtils.php b/HeaderUtils.php index 3456edace..110896e17 100644 --- a/HeaderUtils.php +++ b/HeaderUtils.php @@ -286,7 +286,11 @@ private static function groupParts(array $matches, string $separators, bool $fir } foreach ($partMatches as $matches) { - $parts[] = '' === $separators ? self::unquote($matches[0][0]) : self::groupParts($matches, $separators, false); + if ('' === $separators && '' !== $unquoted = self::unquote($matches[0][0])) { + $parts[] = $unquoted; + } elseif ($groupedParts = self::groupParts($matches, $separators, false)) { + $parts[] = $groupedParts; + } } return $parts; diff --git a/Tests/AcceptHeaderTest.php b/Tests/AcceptHeaderTest.php index bf4582430..e972d714e 100644 --- a/Tests/AcceptHeaderTest.php +++ b/Tests/AcceptHeaderTest.php @@ -41,6 +41,8 @@ public static function provideFromStringData() { return [ ['', []], + [';;;', []], + ['0', [new AcceptHeaderItem('0')]], ['gzip', [new AcceptHeaderItem('gzip')]], ['gzip,deflate,sdch', [new AcceptHeaderItem('gzip'), new AcceptHeaderItem('deflate'), new AcceptHeaderItem('sdch')]], ["gzip, deflate\t,sdch", [new AcceptHeaderItem('gzip'), new AcceptHeaderItem('deflate'), new AcceptHeaderItem('sdch')]], From 9834910442b47df011743d7516c9f1e1b8a596f9 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 17 May 2024 16:37:29 +0200 Subject: [PATCH 094/111] fix merge --- Tests/Session/Storage/NativeSessionStorageTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/Session/Storage/NativeSessionStorageTest.php b/Tests/Session/Storage/NativeSessionStorageTest.php index 857ec4eec..d2e198eeb 100644 --- a/Tests/Session/Storage/NativeSessionStorageTest.php +++ b/Tests/Session/Storage/NativeSessionStorageTest.php @@ -55,7 +55,6 @@ protected function tearDown(): void @rmdir($this->savePath); } - $this->savePath = null; ini_set('session.save_handler', $this->initialSessionSaveHandler); ini_set('session.save_path', $this->initialSessionSavePath); } From 35549a775093fefde948146bfb667e3339e5bb39 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 17 May 2024 16:43:13 +0200 Subject: [PATCH 095/111] fix merge --- Tests/Session/Storage/PhpBridgeSessionStorageTest.php | 1 - Tests/Session/Storage/Proxy/AbstractProxyTest.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/Session/Storage/PhpBridgeSessionStorageTest.php b/Tests/Session/Storage/PhpBridgeSessionStorageTest.php index 9520bbf39..5fbc38335 100644 --- a/Tests/Session/Storage/PhpBridgeSessionStorageTest.php +++ b/Tests/Session/Storage/PhpBridgeSessionStorageTest.php @@ -51,7 +51,6 @@ protected function tearDown(): void @rmdir($this->savePath); } - $this->savePath = null; ini_set('session.save_handler', $this->initialSessionSaveHandler); ini_set('session.save_path', $this->initialSessionSavePath); } diff --git a/Tests/Session/Storage/Proxy/AbstractProxyTest.php b/Tests/Session/Storage/Proxy/AbstractProxyTest.php index 1dc97049c..bb459bb9f 100644 --- a/Tests/Session/Storage/Proxy/AbstractProxyTest.php +++ b/Tests/Session/Storage/Proxy/AbstractProxyTest.php @@ -23,7 +23,7 @@ */ class AbstractProxyTest extends TestCase { - protected MockObject&AbstractProxy $proxy; + protected AbstractProxy $proxy; protected function setUp(): void { From cf4893ca4eca3fac4ae06da1590afdbbb4217847 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 31 May 2024 16:33:22 +0200 Subject: [PATCH 096/111] Revert "minor #54653 Auto-close PRs on subtree-splits (nicolas-grekas)" This reverts commit 2c9352dd91ebaf37b8a3e3c26fd8e1306df2fb73, reversing changes made to 18c3e87f1512be2cc50e90235b144b13bc347258. --- .gitattributes | 3 +- .github/PULL_REQUEST_TEMPLATE.md | 8 ----- .github/workflows/check-subtree-split.yml | 37 ----------------------- 3 files changed, 2 insertions(+), 46 deletions(-) delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md delete mode 100644 .github/workflows/check-subtree-split.yml diff --git a/.gitattributes b/.gitattributes index 14c3c3594..84c7add05 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ /Tests export-ignore /phpunit.xml.dist export-ignore -/.git* export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 4689c4dad..000000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,8 +0,0 @@ -Please do not submit any Pull Requests here. They will be closed. ---- - -Please submit your PR here instead: -https://github.com/symfony/symfony - -This repository is what we call a "subtree split": a read-only subset of that main repository. -We're looking forward to your PR there! diff --git a/.github/workflows/check-subtree-split.yml b/.github/workflows/check-subtree-split.yml deleted file mode 100644 index 16be48bae..000000000 --- a/.github/workflows/check-subtree-split.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Check subtree split - -on: - pull_request_target: - -jobs: - close-pull-request: - runs-on: ubuntu-latest - - steps: - - name: Close pull request - uses: actions/github-script@v6 - with: - script: | - if (context.repo.owner === "symfony") { - github.rest.issues.createComment({ - owner: "symfony", - repo: context.repo.repo, - issue_number: context.issue.number, - body: ` - Thanks for your Pull Request! We love contributions. - - However, you should instead open your PR on the main repository: - https://github.com/symfony/symfony - - This repository is what we call a "subtree split": a read-only subset of that main repository. - We're looking forward to your PR there! - ` - }); - - github.rest.pulls.update({ - owner: "symfony", - repo: context.repo.repo, - pull_number: context.issue.number, - state: "closed" - }); - } From a33dd713e17a83b6669beb705ce215a192c0b46a Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 28 Jun 2024 17:37:05 +0200 Subject: [PATCH 097/111] Fix MockArraySessionStorage to generate more conform ids --- Session/Storage/MockArraySessionStorage.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Session/Storage/MockArraySessionStorage.php b/Session/Storage/MockArraySessionStorage.php index 77bb38f15..c6a28b1a4 100644 --- a/Session/Storage/MockArraySessionStorage.php +++ b/Session/Storage/MockArraySessionStorage.php @@ -226,14 +226,11 @@ public function getMetadataBag() /** * Generates a session ID. * - * This doesn't need to be particularly cryptographically secure since this is just - * a mock. - * * @return string */ protected function generateId() { - return hash('sha256', uniqid('ss_mock_', true)); + return bin2hex(random_bytes(16)); } protected function loadSession() From 9249ad7601178719116eefd74b469832442a0596 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Mon, 8 Jul 2024 21:30:21 +0200 Subject: [PATCH 098/111] Fix typo --- Tests/BinaryFileResponseTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/BinaryFileResponseTest.php b/Tests/BinaryFileResponseTest.php index 222b5f298..4599bd84b 100644 --- a/Tests/BinaryFileResponseTest.php +++ b/Tests/BinaryFileResponseTest.php @@ -368,7 +368,7 @@ public function testAcceptRangeOnUnsafeMethods() $this->assertEquals('none', $response->headers->get('Accept-Ranges')); } - public function testAcceptRangeNotOverriden() + public function testAcceptRangeNotOverridden() { $request = Request::create('/', 'POST'); $response = new BinaryFileResponse(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream']); From 0ff17bf5087c4b9fec2bf25404e2d4292c947e63 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Tue, 16 Jul 2024 09:39:12 +0200 Subject: [PATCH 099/111] [HttpFoundation] Add tests for `MethodRequestMatcher` and `SchemeRequestMatcher` --- Tests/RequestMatcher/MethodRequestMatcherTest.php | 7 +++++++ Tests/RequestMatcher/SchemeRequestMatcherTest.php | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/Tests/RequestMatcher/MethodRequestMatcherTest.php b/Tests/RequestMatcher/MethodRequestMatcherTest.php index d4af82cd9..19db917fe 100644 --- a/Tests/RequestMatcher/MethodRequestMatcherTest.php +++ b/Tests/RequestMatcher/MethodRequestMatcherTest.php @@ -27,6 +27,13 @@ public function test(string $requestMethod, array|string $matcherMethod, bool $i $this->assertSame($isMatch, $matcher->matches($request)); } + public function testAlwaysMatchesOnEmptyMethod() + { + $matcher = new MethodRequestMatcher([]); + $request = Request::create('https://example.com', 'POST'); + $this->assertTrue($matcher->matches($request)); + } + public static function getData() { return [ diff --git a/Tests/RequestMatcher/SchemeRequestMatcherTest.php b/Tests/RequestMatcher/SchemeRequestMatcherTest.php index f8d83645f..6614bfcc2 100644 --- a/Tests/RequestMatcher/SchemeRequestMatcherTest.php +++ b/Tests/RequestMatcher/SchemeRequestMatcherTest.php @@ -42,6 +42,13 @@ public function test(string $requestScheme, array|string $matcherScheme, bool $i } } + public function testAlwaysMatchesOnParamsHeaders() + { + $matcher = new SchemeRequestMatcher([]); + $request = Request::create('sftp://example.com'); + $this->assertTrue($matcher->matches($request)); + } + public static function getData() { return [ From 9c375b2abef0b657aa0b7612b763df5c12a465ab Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Tue, 16 Jul 2024 09:31:57 +0200 Subject: [PATCH 100/111] [HttpFoundation] Add tests for uncovered sections --- Tests/InputBagTest.php | 18 ++++++++++++ .../AbstractRequestRateLimiterTest.php | 29 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/Tests/InputBagTest.php b/Tests/InputBagTest.php index b21e988a4..fc3f0964c 100644 --- a/Tests/InputBagTest.php +++ b/Tests/InputBagTest.php @@ -98,4 +98,22 @@ public function testFilterArrayWithoutArrayFlagIsDeprecated() $this->expectDeprecation('Since symfony/http-foundation 5.1: Filtering an array value with "Symfony\Component\HttpFoundation\InputBag::filter()" without passing the FILTER_REQUIRE_ARRAY or FILTER_FORCE_ARRAY flag is deprecated'); $bag->filter('foo', \FILTER_VALIDATE_INT); } + + public function testAdd() + { + $bag = new InputBag(['foo' => 'bar']); + $bag->add(['baz' => 'qux']); + + $this->assertSame('bar', $bag->get('foo'), '->add() does not remove existing parameters'); + $this->assertSame('qux', $bag->get('baz'), '->add() adds new parameters'); + } + + public function testReplace() + { + $bag = new InputBag(['foo' => 'bar']); + $bag->replace(['baz' => 'qux']); + + $this->assertNull($bag->get('foo'), '->replace() removes existing parameters'); + $this->assertSame('qux', $bag->get('baz'), '->replace() adds new parameters'); + } } diff --git a/Tests/RateLimiter/AbstractRequestRateLimiterTest.php b/Tests/RateLimiter/AbstractRequestRateLimiterTest.php index 4e102777a..26f2fac90 100644 --- a/Tests/RateLimiter/AbstractRequestRateLimiterTest.php +++ b/Tests/RateLimiter/AbstractRequestRateLimiterTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\RateLimiter\LimiterInterface; +use Symfony\Component\RateLimiter\Policy\NoLimiter; use Symfony\Component\RateLimiter\RateLimit; class AbstractRequestRateLimiterTest extends TestCase @@ -33,6 +34,34 @@ public function testConsume(array $rateLimits, ?RateLimit $expected) $this->assertSame($expected, $rateLimiter->consume(new Request())); } + public function testConsumeWithoutLimiterAddsSpecialNoLimiter() + { + $rateLimiter = new MockAbstractRequestRateLimiter([]); + + try { + $this->assertSame(\PHP_INT_MAX, $rateLimiter->consume(new Request())->getLimit()); + } catch (\TypeError $error) { + if (str_contains($error->getMessage(), 'RateLimit::__construct(): Argument #1 ($availableTokens) must be of type int, float given')) { + $this->markTestSkipped('This test cannot be run on a version of the RateLimiter component that uses \INF instead of \PHP_INT_MAX in NoLimiter.'); + } + + throw $error; + } + } + + public function testResetLimiters() + { + $rateLimiter = new MockAbstractRequestRateLimiter([ + $limiter1 = $this->createMock(LimiterInterface::class), + $limiter2 = $this->createMock(LimiterInterface::class), + ]); + + $limiter1->expects($this->once())->method('reset'); + $limiter2->expects($this->once())->method('reset'); + + $rateLimiter->reset(new Request()); + } + public static function provideRateLimits() { $now = new \DateTimeImmutable(); From 4c7be83c5f9a6d2e5434794831711cfa9d421dcb Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 5 Sep 2024 11:31:06 +0200 Subject: [PATCH 101/111] [HttpFoundation] Update links for X-Accel-Redirect and fail properly when X-Accel-Mapping is missing --- BinaryFileResponse.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/BinaryFileResponse.php b/BinaryFileResponse.php index 1878caae1..ccfd6389a 100644 --- a/BinaryFileResponse.php +++ b/BinaryFileResponse.php @@ -244,8 +244,12 @@ public function prepare(Request $request) } if ('x-accel-redirect' === strtolower($type)) { // Do X-Accel-Mapping substitutions. - // @link https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/#x-accel-redirect - $parts = HeaderUtils::split($request->headers->get('X-Accel-Mapping', ''), ',='); + // @link https://github.com/rack/rack/blob/main/lib/rack/sendfile.rb + // @link https://mattbrictson.com/blog/accelerated-rails-downloads + if (!$request->headers->has('X-Accel-Mapping')) { + throw new \LogicException('The "X-Accel-Mapping" header must be set when "X-Sendfile-Type" is set to "X-Accel-Redirect".'); + } + $parts = HeaderUtils::split($request->headers->get('X-Accel-Mapping'), ',='); foreach ($parts as $part) { [$pathPrefix, $location] = $part; if (substr($path, 0, \strlen($pathPrefix)) === $pathPrefix) { From b61630eea65383a4d5cdfd0ca3b3e21defefcbd5 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 10 Sep 2024 10:17:27 +0200 Subject: [PATCH 102/111] Work around parse_url() bug --- Request.php | 6 +++++- Tests/RequestTest.php | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Request.php b/Request.php index 75db0300b..561cb887f 100644 --- a/Request.php +++ b/Request.php @@ -355,7 +355,11 @@ public static function create(string $uri, string $method = 'GET', array $parame $server['PATH_INFO'] = ''; $server['REQUEST_METHOD'] = strtoupper($method); - $components = parse_url($uri); + if (false === ($components = parse_url($uri)) && '/' === ($uri[0] ?? '')) { + $components = parse_url($uri.'#'); + unset($components['fragment']); + } + if (isset($components['host'])) { $server['SERVER_NAME'] = $components['host']; $server['HTTP_HOST'] = $components['host']; diff --git a/Tests/RequestTest.php b/Tests/RequestTest.php index 395df09c5..082e8695c 100644 --- a/Tests/RequestTest.php +++ b/Tests/RequestTest.php @@ -244,6 +244,9 @@ public function testCreate() // Fragment should not be included in the URI $request = Request::create('http://test.com/foo#bar'); $this->assertEquals('http://test.com/foo', $request->getUri()); + + $request = Request::create('/foo:123'); + $this->assertEquals('http://localhost/foo:123', $request->getUri()); } public function testCreateWithRequestUri() From 3bd4a2658511c465ee6f5e2d5c9fd549b451690f Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sun, 15 Sep 2024 08:40:50 +0200 Subject: [PATCH 103/111] fix merge --- Tests/RequestTest.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Tests/RequestTest.php b/Tests/RequestTest.php index 115399149..0e4a51ab5 100644 --- a/Tests/RequestTest.php +++ b/Tests/RequestTest.php @@ -263,9 +263,6 @@ public function testCreate() // Fragment should not be included in the URI $request = Request::create('http://test.com/foo#bar'); $this->assertEquals('http://test.com/foo', $request->getUri()); - - $request = Request::create('/foo:123'); - $this->assertEquals('http://localhost/foo:123', $request->getUri()); } public function testCreateWithRequestUri() @@ -2651,7 +2648,8 @@ public function testReservedFlags() public function testInvalidUriCreationDeprecated() { $this->expectDeprecation('Since symfony/http-foundation 6.3: Calling "Symfony\Component\HttpFoundation\Request::create()" with an invalid URI is deprecated.'); - Request::create('/invalid-path:123'); + $request = Request::create('/invalid-path:123'); + $this->assertEquals('http://localhost/invalid-path:123', $request->getUri()); } } From ae0d217e5932aa0b70ddb4cf7822cc76d48aee53 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 13 Sep 2024 22:50:27 +0200 Subject: [PATCH 104/111] move setting deprecation session options into a legacy group test --- .../Storage/NativeSessionStorageTest.php | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/Tests/Session/Storage/NativeSessionStorageTest.php b/Tests/Session/Storage/NativeSessionStorageTest.php index c67b1391d..d5ee85f62 100644 --- a/Tests/Session/Storage/NativeSessionStorageTest.php +++ b/Tests/Session/Storage/NativeSessionStorageTest.php @@ -207,19 +207,39 @@ public function testCookieOptions() $this->assertEquals($options, $gco); } - public function testSessionOptions() + public function testCacheExpireOption() { $options = [ - 'trans_sid_tags' => 'a=href', 'cache_expire' => '200', ]; $this->getStorage($options); - $this->assertSame('a=href', \ini_get('session.trans_sid_tags')); $this->assertSame('200', \ini_get('session.cache_expire')); } + /** + * The test must only be removed when the "session.trans_sid_tags" option is removed from PHP or when the "trans_sid_tags" option is no longer supported by the native session storage. + */ + public function testTransSidTagsOption() + { + $previousErrorHandler = set_error_handler(function ($errno, $errstr) use (&$previousErrorHandler) { + if ('ini_set(): Usage of session.trans_sid_tags INI setting is deprecated' !== $errstr) { + return $previousErrorHandler ? $previousErrorHandler(...\func_get_args()) : false; + } + }); + + try { + $this->getStorage([ + 'trans_sid_tags' => 'a=href', + ]); + } finally { + restore_error_handler(); + } + + $this->assertSame('a=href', \ini_get('session.trans_sid_tags')); + } + public function testSetSaveHandler() { $initialSaveHandler = ini_set('session.save_handler', 'files'); From 133ac043875f59c26c55e79cf074562127cce4d2 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Thu, 19 Sep 2024 23:14:15 +0200 Subject: [PATCH 105/111] Make more data providers static --- Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php | 2 +- Tests/Test/Constraint/ResponseHeaderLocationSameTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php b/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php index 0c6de4c8d..f43115a13 100644 --- a/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php @@ -98,7 +98,7 @@ public function testConstructorShouldThrowExceptionForMissingOptions(array $opti new MongoDbSessionHandler($this->manager, $options); } - public function provideInvalidOptions() + public static function provideInvalidOptions(): iterable { yield 'empty' => [[]]; yield 'collection missing' => [['database' => 'foo']]; diff --git a/Tests/Test/Constraint/ResponseHeaderLocationSameTest.php b/Tests/Test/Constraint/ResponseHeaderLocationSameTest.php index 5754befbc..d05a9f879 100644 --- a/Tests/Test/Constraint/ResponseHeaderLocationSameTest.php +++ b/Tests/Test/Constraint/ResponseHeaderLocationSameTest.php @@ -36,7 +36,7 @@ public function testConstraintSuccess(string $requestUrl, ?string $location, str self::assertTrue($constraint->evaluate($response, '', true)); } - public function provideSuccessCases(): iterable + public static function provideSuccessCases(): iterable { yield ['http://example.com', 'http://example.com', 'http://example.com']; yield ['http://example.com', 'http://example.com', '//example.com']; @@ -112,7 +112,7 @@ public function testConstraintFailure(string $requestUrl, ?string $location, str $constraint->evaluate($response); } - public function provideFailureCases(): iterable + public static function provideFailureCases(): iterable { yield ['http://example.com', null, 'http://example.com']; yield ['http://example.com', null, '//example.com']; From a5509aa59ea1de39482dfcaa0ade0c2895fa4969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 23 Sep 2024 11:24:18 +0200 Subject: [PATCH 106/111] Add PR template and auto-close PR on subtree split repositories --- .gitattributes | 3 +-- .github/PULL_REQUEST_TEMPLATE.md | 8 ++++++++ .github/workflows/close-pull-request.yml | 20 ++++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/close-pull-request.yml diff --git a/.gitattributes b/.gitattributes index 84c7add05..14c3c3594 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,3 @@ /Tests export-ignore /phpunit.xml.dist export-ignore -/.gitattributes export-ignore -/.gitignore export-ignore +/.git* export-ignore diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..4689c4dad --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ +Please do not submit any Pull Requests here. They will be closed. +--- + +Please submit your PR here instead: +https://github.com/symfony/symfony + +This repository is what we call a "subtree split": a read-only subset of that main repository. +We're looking forward to your PR there! diff --git a/.github/workflows/close-pull-request.yml b/.github/workflows/close-pull-request.yml new file mode 100644 index 000000000..e55b47817 --- /dev/null +++ b/.github/workflows/close-pull-request.yml @@ -0,0 +1,20 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: | + Thanks for your Pull Request! We love contributions. + + However, you should instead open your PR on the main repository: + https://github.com/symfony/symfony + + This repository is what we call a "subtree split": a read-only subset of that main repository. + We're looking forward to your PR there! From e641eddada95b55477370076db7ffcde120a0f69 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 4 Oct 2024 09:29:48 +0200 Subject: [PATCH 107/111] ensure session storages are opened in tests before destroying them --- .../Storage/Handler/AbstractRedisSessionHandlerTestCase.php | 1 + .../Session/Storage/Handler/MemcachedSessionHandlerTest.php | 1 + .../Session/Storage/Handler/MigratingSessionHandlerTest.php | 2 ++ Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php | 2 ++ Tests/Session/Storage/Handler/PdoSessionHandlerTest.php | 1 + Tests/Session/Storage/Handler/StrictSessionHandlerTest.php | 5 +++++ 6 files changed, 12 insertions(+) diff --git a/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php b/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php index cd8b31c60..e73281173 100644 --- a/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php +++ b/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php @@ -106,6 +106,7 @@ public function testUseSessionGcMaxLifetimeAsTimeToLive() public function testDestroySession() { + $this->storage->open('', ''); $this->redisClient->set(self::PREFIX.'id', 'foo'); $this->assertTrue((bool) $this->redisClient->exists(self::PREFIX.'id')); diff --git a/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php b/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php index a3aea2e8e..25edf922d 100644 --- a/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php @@ -119,6 +119,7 @@ public function testWriteSessionWithLargeTTL() public function testDestroySession() { + $this->storage->open('', ''); $this->memcached ->expects($this->once()) ->method('delete') diff --git a/Tests/Session/Storage/Handler/MigratingSessionHandlerTest.php b/Tests/Session/Storage/Handler/MigratingSessionHandlerTest.php index f56f753af..6fccde04f 100644 --- a/Tests/Session/Storage/Handler/MigratingSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/MigratingSessionHandlerTest.php @@ -51,6 +51,8 @@ public function testClose() public function testDestroy() { + $this->dualHandler->open('/path/to/save/location', 'xyz'); + $sessionId = 'xyz'; $this->currentHandler->expects($this->once()) diff --git a/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php b/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php index 8e9c5fa04..93c7995dd 100644 --- a/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php @@ -174,6 +174,8 @@ public function testDestroy() ->method('deleteOne') ->with([$this->options['id_field'] => 'foo']); + $this->storage->open('test', 'test'); + $this->assertTrue($this->storage->destroy('foo')); } diff --git a/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php b/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php index 5b5f660c4..455469c5f 100644 --- a/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php @@ -225,6 +225,7 @@ public function testWrongUsageStillWorks() { // wrong method sequence that should no happen, but still works $storage = new PdoSessionHandler($this->getMemorySqlitePdo()); + $storage->open('', 'sid'); $storage->write('id', 'data'); $storage->write('other_id', 'other_data'); $storage->destroy('inexistent'); diff --git a/Tests/Session/Storage/Handler/StrictSessionHandlerTest.php b/Tests/Session/Storage/Handler/StrictSessionHandlerTest.php index 68db5f4cf..27c952cd2 100644 --- a/Tests/Session/Storage/Handler/StrictSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/StrictSessionHandlerTest.php @@ -130,6 +130,7 @@ public function testWriteEmptyNewSession() $handler->expects($this->never())->method('write'); $handler->expects($this->once())->method('destroy')->willReturn(true); $proxy = new StrictSessionHandler($handler); + $proxy->open('path', 'name'); $this->assertFalse($proxy->validateId('id')); $this->assertSame('', $proxy->read('id')); @@ -144,6 +145,7 @@ public function testWriteEmptyExistingSession() $handler->expects($this->never())->method('write'); $handler->expects($this->once())->method('destroy')->willReturn(true); $proxy = new StrictSessionHandler($handler); + $proxy->open('path', 'name'); $this->assertSame('data', $proxy->read('id')); $this->assertTrue($proxy->write('id', '')); @@ -155,6 +157,7 @@ public function testDestroy() $handler->expects($this->once())->method('destroy') ->with('id')->willReturn(true); $proxy = new StrictSessionHandler($handler); + $proxy->open('path', 'name'); $this->assertTrue($proxy->destroy('id')); } @@ -166,6 +169,7 @@ public function testDestroyNewSession() ->with('id')->willReturn(''); $handler->expects($this->once())->method('destroy')->willReturn(true); $proxy = new StrictSessionHandler($handler); + $proxy->open('path', 'name'); $this->assertSame('', $proxy->read('id')); $this->assertTrue($proxy->destroy('id')); @@ -181,6 +185,7 @@ public function testDestroyNonEmptyNewSession() $handler->expects($this->once())->method('destroy') ->with('id')->willReturn(true); $proxy = new StrictSessionHandler($handler); + $proxy->open('path', 'name'); $this->assertSame('', $proxy->read('id')); $this->assertTrue($proxy->write('id', 'data')); From 35f7b4ca005d2300bdb57fbdc0d6df4e11ed70d0 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Thu, 10 Oct 2024 09:55:54 +0200 Subject: [PATCH 108/111] session names must not be empty The changes done in #58453 were not enough. Since the session name is used as the cookie name it must not be the empty string. --- .../Storage/Handler/AbstractRedisSessionHandlerTestCase.php | 2 +- Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php b/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php index e73281173..0cf11c7de 100644 --- a/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php +++ b/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php @@ -106,7 +106,7 @@ public function testUseSessionGcMaxLifetimeAsTimeToLive() public function testDestroySession() { - $this->storage->open('', ''); + $this->storage->open('', 'test'); $this->redisClient->set(self::PREFIX.'id', 'foo'); $this->assertTrue((bool) $this->redisClient->exists(self::PREFIX.'id')); diff --git a/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php b/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php index 25edf922d..cd98a1fd4 100644 --- a/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php @@ -119,7 +119,7 @@ public function testWriteSessionWithLargeTTL() public function testDestroySession() { - $this->storage->open('', ''); + $this->storage->open('', 'sid'); $this->memcached ->expects($this->once()) ->method('delete') From 3f38426b9447521b044fbee56a1b31b18e049042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 21 Oct 2024 21:59:03 +0200 Subject: [PATCH 109/111] Ensure compatibility with mongodb v2 --- Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php b/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php index 93c7995dd..b1c9db759 100644 --- a/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php @@ -133,6 +133,8 @@ public function testWrite() $this->assertInstanceOf(\MongoDB\BSON\UTCDateTime::class, $data[$this->options['time_field']]); $this->assertInstanceOf(\MongoDB\BSON\UTCDateTime::class, $data[$this->options['expiry_field']]); $this->assertGreaterThanOrEqual($expectedExpiry, round((string) $data[$this->options['expiry_field']] / 1000)); + + return $this->createMock(\MongoDB\UpdateResult::class); }); $this->assertTrue($this->storage->write('foo', 'bar')); @@ -153,6 +155,8 @@ public function testReplaceSessionData() ->method('updateOne') ->willReturnCallback(function ($criteria, $updateData, $options) use (&$data) { $data = $updateData; + + return $this->createMock(\MongoDB\UpdateResult::class); }); $this->storage->write('foo', 'bar'); From 38bd9bc4f5de292ddbf860d02596133dbc09d422 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 25 Oct 2024 15:35:27 +0200 Subject: [PATCH 110/111] [HttpFoundation] Remove invalid HTTP method from exception message --- Request.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Request.php b/Request.php index 561cb887f..31d11c5da 100644 --- a/Request.php +++ b/Request.php @@ -1294,7 +1294,7 @@ public function getMethod() } if (!preg_match('/^[A-Z]++$/D', $method)) { - throw new SuspiciousOperationException(sprintf('Invalid method override "%s".', $method)); + throw new SuspiciousOperationException('Invalid HTTP method override.'); } return $this->method = $method; From 32310ff3aa8126ede47168fc9d9ae4a33b09c3a2 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 22 Oct 2024 10:31:42 +0200 Subject: [PATCH 111/111] [HttpFoundation] Reject URIs that contain invalid characters --- Request.php | 17 +++++++++++++++++ Tests/RequestTest.php | 30 ++++++++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/Request.php b/Request.php index 561cb887f..e404b4cd0 100644 --- a/Request.php +++ b/Request.php @@ -11,6 +11,7 @@ namespace Symfony\Component\HttpFoundation; +use Symfony\Component\HttpFoundation\Exception\BadRequestException; use Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException; use Symfony\Component\HttpFoundation\Exception\JsonException; use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException; @@ -333,6 +334,8 @@ public static function createFromGlobals() * @param string|resource|null $content The raw body data * * @return static + * + * @throws BadRequestException When the URI is invalid */ public static function create(string $uri, string $method = 'GET', array $parameters = [], array $cookies = [], array $files = [], array $server = [], $content = null) { @@ -360,6 +363,20 @@ public static function create(string $uri, string $method = 'GET', array $parame unset($components['fragment']); } + if (false === $components) { + throw new BadRequestException('Invalid URI.'); + } + + if (false !== ($i = strpos($uri, '\\')) && $i < strcspn($uri, '?#')) { + throw new BadRequestException('Invalid URI: A URI cannot contain a backslash.'); + } + if (\strlen($uri) !== strcspn($uri, "\r\n\t")) { + throw new BadRequestException('Invalid URI: A URI cannot contain CR/LF/TAB characters.'); + } + if ('' !== $uri && (\ord($uri[0]) <= 32 || \ord($uri[-1]) <= 32)) { + throw new BadRequestException('Invalid URI: A URI must not start nor end with ASCII control characters or spaces.'); + } + if (isset($components['host'])) { $server['SERVER_NAME'] = $components['host']; $server['HTTP_HOST'] = $components['host']; diff --git a/Tests/RequestTest.php b/Tests/RequestTest.php index 082e8695c..c2986907b 100644 --- a/Tests/RequestTest.php +++ b/Tests/RequestTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; +use Symfony\Component\HttpFoundation\Exception\BadRequestException; use Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException; use Symfony\Component\HttpFoundation\Exception\JsonException; use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException; @@ -289,9 +290,34 @@ public function testCreateWithRequestUri() $this->assertTrue($request->isSecure()); // Fragment should not be included in the URI - $request = Request::create('http://test.com/foo#bar'); - $request->server->set('REQUEST_URI', 'http://test.com/foo#bar'); + $request = Request::create('http://test.com/foo#bar\\baz'); + $request->server->set('REQUEST_URI', 'http://test.com/foo#bar\\baz'); $this->assertEquals('http://test.com/foo', $request->getUri()); + + $request = Request::create('http://test.com/foo?bar=f\\o'); + $this->assertEquals('http://test.com/foo?bar=f%5Co', $request->getUri()); + $this->assertEquals('/foo', $request->getPathInfo()); + $this->assertEquals('bar=f%5Co', $request->getQueryString()); + } + + /** + * @testWith ["http://foo.com\\bar"] + * ["\\\\foo.com/bar"] + * ["a\rb"] + * ["a\nb"] + * ["a\tb"] + * ["\u0000foo"] + * ["foo\u0000"] + * [" foo"] + * ["foo "] + * [":"] + */ + public function testCreateWithBadRequestUri(string $uri) + { + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('Invalid URI'); + + Request::create($uri); } /**