From 810791737c44298a9ab930d5a1dddc275e0df38f Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Wed, 8 May 2024 16:40:28 -0500 Subject: [PATCH 01/44] inline variable no need to create the temporary `$ipAddress` variable, we can access the array offset directly from the function. --- Request.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Request.php b/Request.php index df332479d..0dc5a9230 100644 --- a/Request.php +++ b/Request.php @@ -750,9 +750,7 @@ public function getClientIps(): array */ public function getClientIp(): ?string { - $ipAddresses = $this->getClientIps(); - - return $ipAddresses[0]; + return $this->getClientIps()[0]; } /** From 7e606f5de1862889274437c193e9e8b45eb0b936 Mon Sep 17 00:00:00 2001 From: Ian Irlen <45947370+kevinirlen@users.noreply.github.com> Date: Sat, 1 Jun 2024 00:17:32 +0400 Subject: [PATCH 02/44] [HttpFoundation] A more precise comment for HeaderUtils::split method. --- HeaderUtils.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HeaderUtils.php b/HeaderUtils.php index 110896e17..421aefdb8 100644 --- a/HeaderUtils.php +++ b/HeaderUtils.php @@ -34,7 +34,7 @@ private function __construct() * Example: * * HeaderUtils::split('da, en-gb;q=0.8', ',;') - * // => ['da'], ['en-gb', 'q=0.8']] + * # returns [['da'], ['en-gb', 'q=0.8']] * * @param string $separators List of characters to split on, ordered by * precedence, e.g. ',', ';=', or ',;=' From 5f81c6eebe36ed7319c7d0b98259d65abaf45705 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 24 May 2024 12:26:22 +0200 Subject: [PATCH 03/44] use constructor property promotion --- AcceptHeaderItem.php | 8 +++--- Cookie.php | 28 ++++++++----------- File/UploadedFile.php | 11 +++++--- ParameterBag.php | 8 ++---- Session/Attribute/AttributeBag.php | 7 ++--- Session/Flash/AutoExpireFlashBag.php | 7 ++--- Session/Flash/FlashBag.php | 7 ++--- Session/SessionBagProxy.php | 9 ++++-- Session/SessionFactory.php | 11 ++++---- .../Handler/MarshallingSessionHandler.php | 11 +++----- .../Handler/MemcachedSessionHandler.php | 10 +++---- .../Storage/Handler/StrictSessionHandler.php | 8 ++---- Session/Storage/MetadataBag.php | 10 +++---- Session/Storage/MockArraySessionStorage.php | 8 +++--- .../Storage/MockFileSessionStorageFactory.php | 14 ++++------ .../Storage/NativeSessionStorageFactory.php | 17 ++++------- .../PhpBridgeSessionStorageFactory.php | 14 ++++------ Session/Storage/Proxy/SessionHandlerProxy.php | 8 ++---- Test/Constraint/RequestAttributeValueSame.php | 11 +++----- Test/Constraint/ResponseCookieValueSame.php | 17 ++++------- Test/Constraint/ResponseFormatSame.php | 9 ++---- Test/Constraint/ResponseHasCookie.php | 14 ++++------ Test/Constraint/ResponseHasHeader.php | 8 ++---- Test/Constraint/ResponseHeaderSame.php | 11 +++----- Test/Constraint/ResponseStatusCodeSame.php | 9 +++--- UriSigner.php | 15 ++++------ 26 files changed, 117 insertions(+), 173 deletions(-) diff --git a/AcceptHeaderItem.php b/AcceptHeaderItem.php index 35ecd4ea2..b8b2b8ad3 100644 --- a/AcceptHeaderItem.php +++ b/AcceptHeaderItem.php @@ -18,14 +18,14 @@ */ class AcceptHeaderItem { - private string $value; private float $quality = 1.0; private int $index = 0; private array $attributes = []; - public function __construct(string $value, array $attributes = []) - { - $this->value = $value; + public function __construct( + private string $value, + array $attributes = [], + ) { foreach ($attributes as $name => $value) { $this->setAttribute($name, $value); } diff --git a/Cookie.php b/Cookie.php index 46be14bae..034a1bf17 100644 --- a/Cookie.php +++ b/Cookie.php @@ -22,17 +22,10 @@ class Cookie public const SAMESITE_LAX = 'lax'; public const SAMESITE_STRICT = 'strict'; - protected string $name; - protected ?string $value; - protected ?string $domain; protected int $expire; protected string $path; - protected ?bool $secure; - protected bool $httpOnly; - 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"; @@ -94,8 +87,18 @@ public static function create(string $name, ?string $value = null, int|string|\D * * @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, bool $partitioned = false) - { + public function __construct( + protected string $name, + protected ?string $value = null, + int|string|\DateTimeInterface $expire = 0, + ?string $path = '/', + protected ?string $domain = null, + protected ?bool $secure = null, + protected bool $httpOnly = true, + private bool $raw = false, + ?string $sameSite = self::SAMESITE_LAX, + private bool $partitioned = false, + ) { // from PHP source code if ($raw && false !== strpbrk($name, self::RESERVED_CHARS_LIST)) { throw new \InvalidArgumentException(sprintf('The cookie name "%s" contains invalid characters.', $name)); @@ -105,16 +108,9 @@ public function __construct(string $name, ?string $value = null, int|string|\Dat throw new \InvalidArgumentException('The cookie name cannot be empty.'); } - $this->name = $name; - $this->value = $value; - $this->domain = $domain; $this->expire = self::expiresTimestamp($expire); $this->path = $path ?: '/'; - $this->secure = $secure; - $this->httpOnly = $httpOnly; - $this->raw = $raw; $this->sameSite = $this->withSameSite($sameSite)->sameSite; - $this->partitioned = $partitioned; } /** diff --git a/File/UploadedFile.php b/File/UploadedFile.php index 74e929f9b..3b050fb77 100644 --- a/File/UploadedFile.php +++ b/File/UploadedFile.php @@ -31,7 +31,6 @@ */ class UploadedFile extends File { - private bool $test; private string $originalName; private string $mimeType; private int $error; @@ -61,13 +60,17 @@ 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, + private bool $test = false, + ) { $this->originalName = $this->getName($originalName); $this->originalPath = strtr($originalName, '\\', '/'); $this->mimeType = $mimeType ?: 'application/octet-stream'; $this->error = $error ?: \UPLOAD_ERR_OK; - $this->test = $test; parent::__construct($path, \UPLOAD_ERR_OK === $this->error); } diff --git a/ParameterBag.php b/ParameterBag.php index d60d3bda2..760e4f947 100644 --- a/ParameterBag.php +++ b/ParameterBag.php @@ -23,11 +23,9 @@ */ class ParameterBag implements \IteratorAggregate, \Countable { - protected array $parameters; - - public function __construct(array $parameters = []) - { - $this->parameters = $parameters; + public function __construct( + protected array $parameters = [], + ) { } /** diff --git a/Session/Attribute/AttributeBag.php b/Session/Attribute/AttributeBag.php index 042f3bd90..e34a497c5 100644 --- a/Session/Attribute/AttributeBag.php +++ b/Session/Attribute/AttributeBag.php @@ -21,14 +21,13 @@ class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Counta protected array $attributes = []; private string $name = 'attributes'; - private string $storageKey; /** * @param string $storageKey The key used to store attributes in the session */ - public function __construct(string $storageKey = '_sf2_attributes') - { - $this->storageKey = $storageKey; + public function __construct( + private string $storageKey = '_sf2_attributes', + ) { } public function getName(): string diff --git a/Session/Flash/AutoExpireFlashBag.php b/Session/Flash/AutoExpireFlashBag.php index 2eba84330..bfb856d51 100644 --- a/Session/Flash/AutoExpireFlashBag.php +++ b/Session/Flash/AutoExpireFlashBag.php @@ -20,14 +20,13 @@ class AutoExpireFlashBag implements FlashBagInterface { private string $name = 'flashes'; private array $flashes = ['display' => [], 'new' => []]; - private string $storageKey; /** * @param string $storageKey The key used to store flashes in the session */ - public function __construct(string $storageKey = '_symfony_flashes') - { - $this->storageKey = $storageKey; + public function __construct( + private string $storageKey = '_symfony_flashes', + ) { } public function getName(): string diff --git a/Session/Flash/FlashBag.php b/Session/Flash/FlashBag.php index 044639b36..72753a66a 100644 --- a/Session/Flash/FlashBag.php +++ b/Session/Flash/FlashBag.php @@ -20,14 +20,13 @@ class FlashBag implements FlashBagInterface { private string $name = 'flashes'; private array $flashes = []; - private string $storageKey; /** * @param string $storageKey The key used to store flashes in the session */ - public function __construct(string $storageKey = '_symfony_flashes') - { - $this->storageKey = $storageKey; + public function __construct( + private string $storageKey = '_symfony_flashes', + ) { } public function getName(): string diff --git a/Session/SessionBagProxy.php b/Session/SessionBagProxy.php index e759d94db..a389bd8b1 100644 --- a/Session/SessionBagProxy.php +++ b/Session/SessionBagProxy.php @@ -18,13 +18,16 @@ */ final class SessionBagProxy implements SessionBagInterface { - private SessionBagInterface $bag; private array $data; private ?int $usageIndex; private ?\Closure $usageReporter; - public function __construct(SessionBagInterface $bag, array &$data, ?int &$usageIndex, ?callable $usageReporter) - { + public function __construct( + private SessionBagInterface $bag, + array &$data, + ?int &$usageIndex, + ?callable $usageReporter, + ) { $this->bag = $bag; $this->data = &$data; $this->usageIndex = &$usageIndex; diff --git a/Session/SessionFactory.php b/Session/SessionFactory.php index c06ed4b7d..b875a23c5 100644 --- a/Session/SessionFactory.php +++ b/Session/SessionFactory.php @@ -22,14 +22,13 @@ class_exists(Session::class); */ class SessionFactory implements SessionFactoryInterface { - private RequestStack $requestStack; - private SessionStorageFactoryInterface $storageFactory; private ?\Closure $usageReporter; - public function __construct(RequestStack $requestStack, SessionStorageFactoryInterface $storageFactory, ?callable $usageReporter = null) - { - $this->requestStack = $requestStack; - $this->storageFactory = $storageFactory; + public function __construct( + private RequestStack $requestStack, + private SessionStorageFactoryInterface $storageFactory, + ?callable $usageReporter = null, + ) { $this->usageReporter = null === $usageReporter ? null : $usageReporter(...); } diff --git a/Session/Storage/Handler/MarshallingSessionHandler.php b/Session/Storage/Handler/MarshallingSessionHandler.php index 1567f5433..8e82f184d 100644 --- a/Session/Storage/Handler/MarshallingSessionHandler.php +++ b/Session/Storage/Handler/MarshallingSessionHandler.php @@ -18,13 +18,10 @@ */ class MarshallingSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface { - private AbstractSessionHandler $handler; - private MarshallerInterface $marshaller; - - public function __construct(AbstractSessionHandler $handler, MarshallerInterface $marshaller) - { - $this->handler = $handler; - $this->marshaller = $marshaller; + public function __construct( + private AbstractSessionHandler $handler, + private MarshallerInterface $marshaller, + ) { } public function open(string $savePath, string $name): bool diff --git a/Session/Storage/Handler/MemcachedSessionHandler.php b/Session/Storage/Handler/MemcachedSessionHandler.php index 91a023ddb..ecee15f37 100644 --- a/Session/Storage/Handler/MemcachedSessionHandler.php +++ b/Session/Storage/Handler/MemcachedSessionHandler.php @@ -21,8 +21,6 @@ */ class MemcachedSessionHandler extends AbstractSessionHandler { - private \Memcached $memcached; - /** * Time to live in seconds. */ @@ -42,10 +40,10 @@ class MemcachedSessionHandler extends AbstractSessionHandler * * @throws \InvalidArgumentException When unsupported options are passed */ - public function __construct(\Memcached $memcached, array $options = []) - { - $this->memcached = $memcached; - + public function __construct( + private \Memcached $memcached, + array $options = [], + ) { if ($diff = array_diff(array_keys($options), ['prefix', 'expiretime', 'ttl'])) { throw new \InvalidArgumentException(sprintf('The following options are not supported "%s".', implode(', ', $diff))); } diff --git a/Session/Storage/Handler/StrictSessionHandler.php b/Session/Storage/Handler/StrictSessionHandler.php index 1f8668744..74a9962c7 100644 --- a/Session/Storage/Handler/StrictSessionHandler.php +++ b/Session/Storage/Handler/StrictSessionHandler.php @@ -18,16 +18,14 @@ */ class StrictSessionHandler extends AbstractSessionHandler { - private \SessionHandlerInterface $handler; private bool $doDestroy; - public function __construct(\SessionHandlerInterface $handler) - { + public function __construct( + private \SessionHandlerInterface $handler, + ) { if ($handler instanceof \SessionUpdateTimestampHandlerInterface) { throw new \LogicException(sprintf('"%s" is already an instance of "SessionUpdateTimestampHandlerInterface", you cannot wrap it with "%s".', get_debug_type($handler), self::class)); } - - $this->handler = $handler; } /** diff --git a/Session/Storage/MetadataBag.php b/Session/Storage/MetadataBag.php index 3e80f7dd8..c9e0bdd33 100644 --- a/Session/Storage/MetadataBag.php +++ b/Session/Storage/MetadataBag.php @@ -29,18 +29,16 @@ class MetadataBag implements SessionBagInterface protected array $meta = [self::CREATED => 0, self::UPDATED => 0, self::LIFETIME => 0]; private string $name = '__metadata'; - private string $storageKey; private int $lastUsed; - private int $updateThreshold; /** * @param string $storageKey The key used to store bag in the session * @param int $updateThreshold The time to wait between two UPDATED updates */ - public function __construct(string $storageKey = '_sf2_meta', int $updateThreshold = 0) - { - $this->storageKey = $storageKey; - $this->updateThreshold = $updateThreshold; + public function __construct( + private string $storageKey = '_sf2_meta', + private int $updateThreshold = 0, + ) { } public function initialize(array &$array): void diff --git a/Session/Storage/MockArraySessionStorage.php b/Session/Storage/MockArraySessionStorage.php index 97774ce0e..8e8e3109b 100644 --- a/Session/Storage/MockArraySessionStorage.php +++ b/Session/Storage/MockArraySessionStorage.php @@ -28,7 +28,6 @@ class MockArraySessionStorage implements SessionStorageInterface { protected string $id = ''; - protected string $name; protected bool $started = false; protected bool $closed = false; protected array $data = []; @@ -39,9 +38,10 @@ class MockArraySessionStorage implements SessionStorageInterface */ protected array $bags = []; - public function __construct(string $name = 'MOCKSESSID', ?MetadataBag $metaBag = null) - { - $this->name = $name; + public function __construct( + protected string $name = 'MOCKSESSID', + ?MetadataBag $metaBag = null, + ) { $this->setMetadataBag($metaBag); } diff --git a/Session/Storage/MockFileSessionStorageFactory.php b/Session/Storage/MockFileSessionStorageFactory.php index 6727cf14f..77ee7b658 100644 --- a/Session/Storage/MockFileSessionStorageFactory.php +++ b/Session/Storage/MockFileSessionStorageFactory.php @@ -21,18 +21,14 @@ class_exists(MockFileSessionStorage::class); */ class MockFileSessionStorageFactory implements SessionStorageFactoryInterface { - private ?string $savePath; - private string $name; - private ?MetadataBag $metaBag; - /** * @see MockFileSessionStorage constructor. */ - public function __construct(?string $savePath = null, string $name = 'MOCKSESSID', ?MetadataBag $metaBag = null) - { - $this->savePath = $savePath; - $this->name = $name; - $this->metaBag = $metaBag; + public function __construct( + private ?string $savePath = null, + private string $name = 'MOCKSESSID', + private ?MetadataBag $metaBag = null, + ) { } public function createStorage(?Request $request): SessionStorageInterface diff --git a/Session/Storage/NativeSessionStorageFactory.php b/Session/Storage/NativeSessionStorageFactory.php index 6463a4c1b..cb8c53539 100644 --- a/Session/Storage/NativeSessionStorageFactory.php +++ b/Session/Storage/NativeSessionStorageFactory.php @@ -22,20 +22,15 @@ class_exists(NativeSessionStorage::class); */ class NativeSessionStorageFactory implements SessionStorageFactoryInterface { - private array $options; - private AbstractProxy|\SessionHandlerInterface|null $handler; - private ?MetadataBag $metaBag; - private bool $secure; - /** * @see NativeSessionStorage constructor. */ - public function __construct(array $options = [], AbstractProxy|\SessionHandlerInterface|null $handler = null, ?MetadataBag $metaBag = null, bool $secure = false) - { - $this->options = $options; - $this->handler = $handler; - $this->metaBag = $metaBag; - $this->secure = $secure; + public function __construct( + private array $options = [], + private AbstractProxy|\SessionHandlerInterface|null $handler = null, + private ?MetadataBag $metaBag = null, + private bool $secure = false, + ) { } public function createStorage(?Request $request): SessionStorageInterface diff --git a/Session/Storage/PhpBridgeSessionStorageFactory.php b/Session/Storage/PhpBridgeSessionStorageFactory.php index aa4f800d3..357e5c713 100644 --- a/Session/Storage/PhpBridgeSessionStorageFactory.php +++ b/Session/Storage/PhpBridgeSessionStorageFactory.php @@ -22,15 +22,11 @@ class_exists(PhpBridgeSessionStorage::class); */ class PhpBridgeSessionStorageFactory implements SessionStorageFactoryInterface { - private AbstractProxy|\SessionHandlerInterface|null $handler; - private ?MetadataBag $metaBag; - private bool $secure; - - public function __construct(AbstractProxy|\SessionHandlerInterface|null $handler = null, ?MetadataBag $metaBag = null, bool $secure = false) - { - $this->handler = $handler; - $this->metaBag = $metaBag; - $this->secure = $secure; + public function __construct( + private AbstractProxy|\SessionHandlerInterface|null $handler = null, + private ?MetadataBag $metaBag = null, + private bool $secure = false, + ) { } public function createStorage(?Request $request): SessionStorageInterface diff --git a/Session/Storage/Proxy/SessionHandlerProxy.php b/Session/Storage/Proxy/SessionHandlerProxy.php index b8df97f45..0316362f0 100644 --- a/Session/Storage/Proxy/SessionHandlerProxy.php +++ b/Session/Storage/Proxy/SessionHandlerProxy.php @@ -18,11 +18,9 @@ */ class SessionHandlerProxy extends AbstractProxy implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface { - protected \SessionHandlerInterface $handler; - - public function __construct(\SessionHandlerInterface $handler) - { - $this->handler = $handler; + public function __construct( + protected \SessionHandlerInterface $handler, + ) { $this->wrapper = $handler instanceof \SessionHandler; $this->saveHandlerName = $this->wrapper || ($handler instanceof StrictSessionHandler && $handler->isWrapper()) ? \ini_get('session.save_handler') : 'user'; } diff --git a/Test/Constraint/RequestAttributeValueSame.php b/Test/Constraint/RequestAttributeValueSame.php index 6e1142681..fe910f063 100644 --- a/Test/Constraint/RequestAttributeValueSame.php +++ b/Test/Constraint/RequestAttributeValueSame.php @@ -16,13 +16,10 @@ final class RequestAttributeValueSame extends Constraint { - private string $name; - private string $value; - - public function __construct(string $name, string $value) - { - $this->name = $name; - $this->value = $value; + public function __construct( + private string $name, + private string $value, + ) { } public function toString(): string diff --git a/Test/Constraint/ResponseCookieValueSame.php b/Test/Constraint/ResponseCookieValueSame.php index 768007b95..936496a28 100644 --- a/Test/Constraint/ResponseCookieValueSame.php +++ b/Test/Constraint/ResponseCookieValueSame.php @@ -17,17 +17,12 @@ final class ResponseCookieValueSame extends Constraint { - private string $name; - private string $value; - private string $path; - private ?string $domain; - - public function __construct(string $name, string $value, string $path = '/', ?string $domain = null) - { - $this->name = $name; - $this->value = $value; - $this->path = $path; - $this->domain = $domain; + public function __construct( + private string $name, + private string $value, + private string $path = '/', + private ?string $domain = null, + ) { } public function toString(): string diff --git a/Test/Constraint/ResponseFormatSame.php b/Test/Constraint/ResponseFormatSame.php index c75321f24..cc3655aef 100644 --- a/Test/Constraint/ResponseFormatSame.php +++ b/Test/Constraint/ResponseFormatSame.php @@ -22,16 +22,11 @@ */ final class ResponseFormatSame extends Constraint { - private Request $request; - private ?string $format; - public function __construct( - Request $request, - ?string $format, + private Request $request, + private ?string $format, private readonly bool $verbose = true, ) { - $this->request = $request; - $this->format = $format; } public function toString(): string diff --git a/Test/Constraint/ResponseHasCookie.php b/Test/Constraint/ResponseHasCookie.php index 8eccea9d1..aff7d4944 100644 --- a/Test/Constraint/ResponseHasCookie.php +++ b/Test/Constraint/ResponseHasCookie.php @@ -17,15 +17,11 @@ final class ResponseHasCookie extends Constraint { - private string $name; - private string $path; - private ?string $domain; - - public function __construct(string $name, string $path = '/', ?string $domain = null) - { - $this->name = $name; - $this->path = $path; - $this->domain = $domain; + public function __construct( + private string $name, + private string $path = '/', + private ?string $domain = null, + ) { } public function toString(): string diff --git a/Test/Constraint/ResponseHasHeader.php b/Test/Constraint/ResponseHasHeader.php index 08522c89c..4dbd0c997 100644 --- a/Test/Constraint/ResponseHasHeader.php +++ b/Test/Constraint/ResponseHasHeader.php @@ -16,11 +16,9 @@ final class ResponseHasHeader extends Constraint { - private string $headerName; - - public function __construct(string $headerName) - { - $this->headerName = $headerName; + public function __construct( + private string $headerName, + ) { } public function toString(): string diff --git a/Test/Constraint/ResponseHeaderSame.php b/Test/Constraint/ResponseHeaderSame.php index 8141df972..af1cb5fbb 100644 --- a/Test/Constraint/ResponseHeaderSame.php +++ b/Test/Constraint/ResponseHeaderSame.php @@ -16,13 +16,10 @@ final class ResponseHeaderSame extends Constraint { - private string $headerName; - private string $expectedValue; - - public function __construct(string $headerName, string $expectedValue) - { - $this->headerName = $headerName; - $this->expectedValue = $expectedValue; + public function __construct( + private string $headerName, + private string $expectedValue, + ) { } public function toString(): string diff --git a/Test/Constraint/ResponseStatusCodeSame.php b/Test/Constraint/ResponseStatusCodeSame.php index 5ca6373ce..1223608b9 100644 --- a/Test/Constraint/ResponseStatusCodeSame.php +++ b/Test/Constraint/ResponseStatusCodeSame.php @@ -16,11 +16,10 @@ final class ResponseStatusCodeSame extends Constraint { - private int $statusCode; - - public function __construct(int $statusCode, private readonly bool $verbose = true) - { - $this->statusCode = $statusCode; + public function __construct( + private int $statusCode, + private readonly bool $verbose = true, + ) { } public function toString(): string diff --git a/UriSigner.php b/UriSigner.php index 4415026b1..f9fba605b 100644 --- a/UriSigner.php +++ b/UriSigner.php @@ -18,23 +18,18 @@ */ class UriSigner { - private string $secret; - private string $hashParameter; - private string $expirationParameter; - /** * @param string $hashParameter Query string parameter to use * @param string $expirationParameter Query string parameter to use for expiration */ - public function __construct(#[\SensitiveParameter] string $secret, string $hashParameter = '_hash', string $expirationParameter = '_expiration') - { + public function __construct( + #[\SensitiveParameter] private string $secret, + private string $hashParameter = '_hash', + private string $expirationParameter = '_expiration', + ) { if (!$secret) { throw new \InvalidArgumentException('A non-empty secret is required.'); } - - $this->secret = $secret; - $this->hashParameter = $hashParameter; - $this->expirationParameter = $expirationParameter; } /** From 044d55526b302250b8c3bd1985124dcdca0f8518 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Thu, 20 Jun 2024 17:52:34 +0200 Subject: [PATCH 04/44] Prefix all sprintf() calls --- BinaryFileResponse.php | 4 ++-- Cookie.php | 4 ++-- File/Exception/AccessDeniedException.php | 2 +- File/Exception/FileNotFoundException.php | 2 +- File/Exception/UnexpectedTypeException.php | 2 +- File/File.php | 8 ++++---- File/UploadedFile.php | 4 ++-- HeaderBag.php | 4 ++-- HeaderUtils.php | 2 +- InputBag.php | 12 +++++------ IpUtils.php | 2 +- JsonResponse.php | 4 ++-- ParameterBag.php | 12 +++++------ RedirectResponse.php | 4 ++-- Request.php | 20 +++++++++---------- Response.php | 8 ++++---- ResponseHeaderBag.php | 2 +- Session/SessionUtils.php | 4 ++-- .../Handler/AbstractSessionHandler.php | 4 ++-- .../Storage/Handler/IdentityMarshaller.php | 2 +- .../Handler/MemcachedSessionHandler.php | 2 +- .../Handler/NativeFileSessionHandler.php | 4 ++-- Session/Storage/Handler/PdoSessionHandler.php | 12 +++++------ .../Storage/Handler/RedisSessionHandler.php | 2 +- .../Storage/Handler/SessionHandlerFactory.php | 4 ++-- .../Storage/Handler/StrictSessionHandler.php | 2 +- Session/Storage/MockArraySessionStorage.php | 2 +- Session/Storage/MockFileSessionStorage.php | 2 +- Session/Storage/NativeSessionStorage.php | 6 +++--- Test/Constraint/RequestAttributeValueSame.php | 2 +- Test/Constraint/ResponseCookieValueSame.php | 8 ++++---- Test/Constraint/ResponseHasCookie.php | 6 +++--- Test/Constraint/ResponseHasHeader.php | 2 +- .../Constraint/ResponseHeaderLocationSame.php | 4 ++-- Test/Constraint/ResponseHeaderSame.php | 2 +- Tests/RequestTest.php | 2 +- Tests/ResponseFunctionalTest.php | 4 ++-- Tests/ResponseTest.php | 6 +++--- .../Handler/AbstractSessionHandlerTest.php | 4 ++-- .../Handler/MongoDbSessionHandlerTest.php | 2 +- UriSigner.php | 6 +++--- 41 files changed, 95 insertions(+), 95 deletions(-) diff --git a/BinaryFileResponse.php b/BinaryFileResponse.php index e49b1c984..535aa6068 100644 --- a/BinaryFileResponse.php +++ b/BinaryFileResponse.php @@ -256,13 +256,13 @@ public function prepare(Request $request): static $end = min($end, $fileSize - 1); if ($start < 0 || $start > $end) { $this->setStatusCode(416); - $this->headers->set('Content-Range', sprintf('bytes */%s', $fileSize)); + $this->headers->set('Content-Range', \sprintf('bytes */%s', $fileSize)); } elseif ($end - $start < $fileSize - 1) { $this->maxlen = $end < $fileSize ? $end - $start + 1 : -1; $this->offset = $start; $this->setStatusCode(206); - $this->headers->set('Content-Range', sprintf('bytes %s-%s/%s', $start, $end, $fileSize)); + $this->headers->set('Content-Range', \sprintf('bytes %s-%s/%s', $start, $end, $fileSize)); $this->headers->set('Content-Length', $end - $start + 1); } } diff --git a/Cookie.php b/Cookie.php index 034a1bf17..199287057 100644 --- a/Cookie.php +++ b/Cookie.php @@ -101,7 +101,7 @@ public function __construct( ) { // from PHP source code if ($raw && false !== strpbrk($name, self::RESERVED_CHARS_LIST)) { - throw new \InvalidArgumentException(sprintf('The cookie name "%s" contains invalid characters.', $name)); + throw new \InvalidArgumentException(\sprintf('The cookie name "%s" contains invalid characters.', $name)); } if (!$name) { @@ -204,7 +204,7 @@ public function withHttpOnly(bool $httpOnly = true): static public function withRaw(bool $raw = true): static { if ($raw && false !== strpbrk($this->name, self::RESERVED_CHARS_LIST)) { - throw new \InvalidArgumentException(sprintf('The cookie name "%s" contains invalid characters.', $this->name)); + throw new \InvalidArgumentException(\sprintf('The cookie name "%s" contains invalid characters.', $this->name)); } $cookie = clone $this; diff --git a/File/Exception/AccessDeniedException.php b/File/Exception/AccessDeniedException.php index 136d2a9f5..79ab0fce3 100644 --- a/File/Exception/AccessDeniedException.php +++ b/File/Exception/AccessDeniedException.php @@ -20,6 +20,6 @@ class AccessDeniedException extends FileException { public function __construct(string $path) { - parent::__construct(sprintf('The file %s could not be accessed', $path)); + parent::__construct(\sprintf('The file %s could not be accessed', $path)); } } diff --git a/File/Exception/FileNotFoundException.php b/File/Exception/FileNotFoundException.php index 31bdf68fe..3a5eb039b 100644 --- a/File/Exception/FileNotFoundException.php +++ b/File/Exception/FileNotFoundException.php @@ -20,6 +20,6 @@ class FileNotFoundException extends FileException { public function __construct(string $path) { - parent::__construct(sprintf('The file "%s" does not exist', $path)); + parent::__construct(\sprintf('The file "%s" does not exist', $path)); } } diff --git a/File/Exception/UnexpectedTypeException.php b/File/Exception/UnexpectedTypeException.php index 905bd5962..09b1c7e18 100644 --- a/File/Exception/UnexpectedTypeException.php +++ b/File/Exception/UnexpectedTypeException.php @@ -15,6 +15,6 @@ class UnexpectedTypeException extends FileException { public function __construct(mixed $value, string $expectedType) { - parent::__construct(sprintf('Expected argument of type %s, %s given', $expectedType, get_debug_type($value))); + parent::__construct(\sprintf('Expected argument of type %s, %s given', $expectedType, get_debug_type($value))); } } diff --git a/File/File.php b/File/File.php index 34ca5a537..c369ecbfb 100644 --- a/File/File.php +++ b/File/File.php @@ -93,7 +93,7 @@ public function move(string $directory, ?string $name = null): self restore_error_handler(); } if (!$renamed) { - throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s).', $this->getPathname(), $target, strip_tags($error))); + throw new FileException(\sprintf('Could not move the file "%s" to "%s" (%s).', $this->getPathname(), $target, strip_tags($error))); } @chmod($target, 0666 & ~umask()); @@ -106,7 +106,7 @@ public function getContent(): string $content = file_get_contents($this->getPathname()); if (false === $content) { - throw new FileException(sprintf('Could not get the content of the file "%s".', $this->getPathname())); + throw new FileException(\sprintf('Could not get the content of the file "%s".', $this->getPathname())); } return $content; @@ -116,10 +116,10 @@ protected function getTargetFile(string $directory, ?string $name = null): self { if (!is_dir($directory)) { if (false === @mkdir($directory, 0777, true) && !is_dir($directory)) { - throw new FileException(sprintf('Unable to create the "%s" directory.', $directory)); + throw new FileException(\sprintf('Unable to create the "%s" directory.', $directory)); } } elseif (!is_writable($directory)) { - throw new FileException(sprintf('Unable to write in the "%s" directory.', $directory)); + throw new FileException(\sprintf('Unable to write in the "%s" directory.', $directory)); } $target = rtrim($directory, '/\\').\DIRECTORY_SEPARATOR.(null === $name ? $this->getBasename() : $this->getName($name)); diff --git a/File/UploadedFile.php b/File/UploadedFile.php index 3b050fb77..a27a56f96 100644 --- a/File/UploadedFile.php +++ b/File/UploadedFile.php @@ -194,7 +194,7 @@ public function move(string $directory, ?string $name = null): File restore_error_handler(); } if (!$moved) { - throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s).', $this->getPathname(), $target, strip_tags($error))); + throw new FileException(\sprintf('Could not move the file "%s" to "%s" (%s).', $this->getPathname(), $target, strip_tags($error))); } @chmod($target, 0666 & ~umask()); @@ -284,6 +284,6 @@ public function getErrorMessage(): string $maxFilesize = \UPLOAD_ERR_INI_SIZE === $errorCode ? self::getMaxFilesize() / 1024 : 0; $message = $errors[$errorCode] ?? 'The file "%s" was not uploaded due to an unknown error.'; - return sprintf($message, $this->getClientOriginalName(), $maxFilesize); + return \sprintf($message, $this->getClientOriginalName(), $maxFilesize); } } diff --git a/HeaderBag.php b/HeaderBag.php index 4bab3764b..12a2e1bdb 100644 --- a/HeaderBag.php +++ b/HeaderBag.php @@ -51,7 +51,7 @@ public function __toString(): string foreach ($headers as $name => $values) { $name = ucwords($name, '-'); foreach ($values as $value) { - $content .= sprintf("%-{$max}s %s\r\n", $name.':', $value); + $content .= \sprintf("%-{$max}s %s\r\n", $name.':', $value); } } @@ -194,7 +194,7 @@ public function getDate(string $key, ?\DateTimeInterface $default = null): ?\Dat } if (false === $date = \DateTimeImmutable::createFromFormat(\DATE_RFC2822, $value)) { - throw new \RuntimeException(sprintf('The "%s" HTTP header is not parseable (%s).', $key, $value)); + throw new \RuntimeException(\sprintf('The "%s" HTTP header is not parseable (%s).', $key, $value)); } return $date; diff --git a/HeaderUtils.php b/HeaderUtils.php index 421aefdb8..a7079be9a 100644 --- a/HeaderUtils.php +++ b/HeaderUtils.php @@ -165,7 +165,7 @@ public static function unquote(string $s): string public static function makeDisposition(string $disposition, string $filename, string $filenameFallback = ''): string { if (!\in_array($disposition, [self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE])) { - throw new \InvalidArgumentException(sprintf('The disposition must be either "%s" or "%s".', self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE)); + throw new \InvalidArgumentException(\sprintf('The disposition must be either "%s" or "%s".', self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE)); } if ('' === $filenameFallback) { diff --git a/InputBag.php b/InputBag.php index dc5e12756..97bd8b090 100644 --- a/InputBag.php +++ b/InputBag.php @@ -29,13 +29,13 @@ final class InputBag extends ParameterBag public function get(string $key, mixed $default = null): string|int|float|bool|null { if (null !== $default && !\is_scalar($default) && !$default instanceof \Stringable) { - throw new \InvalidArgumentException(sprintf('Expected a scalar value as a 2nd argument to "%s()", "%s" given.', __METHOD__, get_debug_type($default))); + throw new \InvalidArgumentException(\sprintf('Expected a scalar value as a 2nd argument to "%s()", "%s" given.', __METHOD__, get_debug_type($default))); } $value = parent::get($key, $this); if (null !== $value && $this !== $value && !\is_scalar($value) && !$value instanceof \Stringable) { - throw new BadRequestException(sprintf('Input value "%s" contains a non-scalar value.', $key)); + throw new BadRequestException(\sprintf('Input value "%s" contains a non-scalar value.', $key)); } return $this === $value ? $default : $value; @@ -68,7 +68,7 @@ public function add(array $inputs = []): void 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))); + throw new \InvalidArgumentException(\sprintf('Expected a scalar, or an array as a 2nd argument to "%s()", "%s" given.', __METHOD__, get_debug_type($value))); } $this->parameters[$key] = $value; @@ -114,11 +114,11 @@ public function filter(string $key, mixed $default = null, int $filter = \FILTER } if (\is_array($value) && !(($options['flags'] ?? 0) & (\FILTER_REQUIRE_ARRAY | \FILTER_FORCE_ARRAY))) { - throw new BadRequestException(sprintf('Input value "%s" contains an array, but "FILTER_REQUIRE_ARRAY" or "FILTER_FORCE_ARRAY" flags were not set.', $key)); + throw new BadRequestException(\sprintf('Input value "%s" contains an array, but "FILTER_REQUIRE_ARRAY" or "FILTER_FORCE_ARRAY" flags were not set.', $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))); + 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))); } $options['flags'] ??= 0; @@ -131,6 +131,6 @@ public function filter(string $key, mixed $default = null, int $filter = \FILTER return $value; } - throw new BadRequestException(sprintf('Input value "%s" is invalid and flag "FILTER_NULL_ON_FAILURE" was not set.', $key)); + throw new BadRequestException(\sprintf('Input value "%s" is invalid and flag "FILTER_NULL_ON_FAILURE" was not set.', $key)); } } diff --git a/IpUtils.php b/IpUtils.php index ceab620c2..db2c8efdc 100644 --- a/IpUtils.php +++ b/IpUtils.php @@ -102,7 +102,7 @@ public static function checkIp4(string $requestIp, string $ip): bool return self::setCacheResult($cacheKey, false); } - return self::setCacheResult($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)); } /** diff --git a/JsonResponse.php b/JsonResponse.php index 4571d22c7..cd6f177df 100644 --- a/JsonResponse.php +++ b/JsonResponse.php @@ -41,7 +41,7 @@ public function __construct(mixed $data = null, int $status = 200, array $header parent::__construct('', $status, $headers); if ($json && !\is_string($data) && !is_numeric($data) && !\is_callable([$data, '__toString'])) { - throw new \TypeError(sprintf('"%s": If $json is set to true, argument $data must be a string or object implementing __toString(), "%s" given.', __METHOD__, get_debug_type($data))); + throw new \TypeError(\sprintf('"%s": If $json is set to true, argument $data must be a string or object implementing __toString(), "%s" given.', __METHOD__, get_debug_type($data))); } $data ??= new \ArrayObject(); @@ -173,7 +173,7 @@ protected function update(): static // Not using application/javascript for compatibility reasons with older browsers. $this->headers->set('Content-Type', 'text/javascript'); - return $this->setContent(sprintf('/**/%s(%s);', $this->callback, $this->data)); + return $this->setContent(\sprintf('/**/%s(%s);', $this->callback, $this->data)); } // Only set the header when there is none or when it equals 'text/javascript' (from a previous update with callback) diff --git a/ParameterBag.php b/ParameterBag.php index 760e4f947..35a0f1819 100644 --- a/ParameterBag.php +++ b/ParameterBag.php @@ -40,7 +40,7 @@ public function all(?string $key = null): array } if (!\is_array($value = $this->parameters[$key] ?? [])) { - throw new BadRequestException(sprintf('Unexpected value for parameter "%s": expecting "array", got "%s".', $key, get_debug_type($value))); + throw new BadRequestException(\sprintf('Unexpected value for parameter "%s": expecting "array", got "%s".', $key, get_debug_type($value))); } return $value; @@ -127,7 +127,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; @@ -172,7 +172,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); } } @@ -199,11 +199,11 @@ 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)) { - 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))); + 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))); } $options['flags'] ??= 0; @@ -216,7 +216,7 @@ public function filter(string $key, mixed $default = null, int $filter = \FILTER return $value; } - throw new \UnexpectedValueException(sprintf('Parameter value "%s" is invalid and flag "FILTER_NULL_ON_FAILURE" was not set.', $key)); + throw new \UnexpectedValueException(\sprintf('Parameter value "%s" is invalid and flag "FILTER_NULL_ON_FAILURE" was not set.', $key)); } /** diff --git a/RedirectResponse.php b/RedirectResponse.php index 3c2e4b620..b1b1cf3c3 100644 --- a/RedirectResponse.php +++ b/RedirectResponse.php @@ -39,7 +39,7 @@ public function __construct(string $url, int $status = 302, array $headers = []) $this->setTargetUrl($url); if (!$this->isRedirect()) { - throw new \InvalidArgumentException(sprintf('The HTTP status code is not a redirect ("%s" given).', $status)); + throw new \InvalidArgumentException(\sprintf('The HTTP status code is not a redirect ("%s" given).', $status)); } if (301 == $status && !\array_key_exists('cache-control', array_change_key_case($headers, \CASE_LOWER))) { @@ -71,7 +71,7 @@ public function setTargetUrl(string $url): static $this->targetUrl = $url; $this->setContent( - sprintf(' + \sprintf(' diff --git a/Request.php b/Request.php index 0dc5a9230..75618e1c6 100644 --- a/Request.php +++ b/Request.php @@ -300,7 +300,7 @@ public static function create(string $uri, string $method = 'GET', array $parame $components = parse_url($uri); if (false === $components) { - throw new \InvalidArgumentException(sprintf('Malformed URI "%s".', $uri)); + throw new \InvalidArgumentException(\sprintf('Malformed URI "%s".', $uri)); } if (isset($components['host'])) { $server['SERVER_NAME'] = $components['host']; @@ -472,7 +472,7 @@ public function __toString(): string } return - sprintf('%s %s %s', $this->getMethod(), $this->getRequestUri(), $this->server->get('SERVER_PROTOCOL'))."\r\n". + \sprintf('%s %s %s', $this->getMethod(), $this->getRequestUri(), $this->server->get('SERVER_PROTOCOL'))."\r\n". $this->headers. $cookieHeader."\r\n". $content; @@ -567,7 +567,7 @@ public static function getTrustedHeaderSet(): int */ public static function setTrustedHosts(array $hostPatterns): void { - self::$trustedHostPatterns = array_map(fn ($hostPattern) => 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 = []; } @@ -1082,7 +1082,7 @@ public function getHost(): string } $this->isHostValid = false; - throw new SuspiciousOperationException(sprintf('Invalid Host "%s".', $host)); + throw new SuspiciousOperationException(\sprintf('Invalid Host "%s".', $host)); } if (\count(self::$trustedHostPatterns) > 0) { @@ -1105,7 +1105,7 @@ public function getHost(): string } $this->isHostValid = false; - throw new SuspiciousOperationException(sprintf('Untrusted Host "%s".', $host)); + throw new SuspiciousOperationException(\sprintf('Untrusted Host "%s".', $host)); } return $host; @@ -1162,7 +1162,7 @@ public function getMethod(): string } if (!preg_match('/^[A-Z]++$/D', $method)) { - throw new SuspiciousOperationException(sprintf('Invalid method override "%s".', $method)); + throw new SuspiciousOperationException(\sprintf('Invalid method override "%s".', $method)); } return $this->method = $method; @@ -1445,7 +1445,7 @@ public function getPayload(): InputBag } if (!\is_array($content)) { - throw new JsonException(sprintf('JSON content was expected to decode to an array, "%s" returned.', get_debug_type($content))); + throw new JsonException(\sprintf('JSON content was expected to decode to an array, "%s" returned.', get_debug_type($content))); } return new InputBag($content); @@ -1471,7 +1471,7 @@ public function toArray(): array } if (!\is_array($content)) { - throw new JsonException(sprintf('JSON content was expected to decode to an array, "%s" returned.', get_debug_type($content))); + throw new JsonException(\sprintf('JSON content was expected to decode to an array, "%s" returned.', get_debug_type($content))); } return $content; @@ -1937,7 +1937,7 @@ private function getUrlencodedPrefix(string $string, string $prefix): ?string $len = \strlen($prefix); - if (preg_match(sprintf('#^(%%[[:xdigit:]]{2}|.){%d}#', $len), $string, $match)) { + if (preg_match(\sprintf('#^(%%[[:xdigit:]]{2}|.){%d}#', $len), $string, $match)) { return $match[0]; } @@ -2029,7 +2029,7 @@ private function getTrustedValues(int $type, ?string $ip = null): array } $this->isForwardedValid = false; - throw new ConflictingHeadersException(sprintf('The request has both a trusted "%s" header and a trusted "%s" header, conflicting with each other. You should either configure your proxy to remove one of them, or configure your project to distrust the offending one.', self::TRUSTED_HEADERS[self::HEADER_FORWARDED], self::TRUSTED_HEADERS[$type])); + throw new ConflictingHeadersException(\sprintf('The request has both a trusted "%s" header and a trusted "%s" header, conflicting with each other. You should either configure your proxy to remove one of them, or configure your project to distrust the offending one.', self::TRUSTED_HEADERS[self::HEADER_FORWARDED], self::TRUSTED_HEADERS[$type])); } private function normalizeAndFilterClientIps(array $clientIps, string $ip): array diff --git a/Response.php b/Response.php index 22c09a01f..bf68d2741 100644 --- a/Response.php +++ b/Response.php @@ -217,7 +217,7 @@ public function __construct(?string $content = '', int $status = 200, array $hea public function __toString(): string { return - sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)."\r\n". + \sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)."\r\n". $this->headers."\r\n". $this->getContent(); } @@ -365,7 +365,7 @@ public function sendHeaders(?int $statusCode = null): static $statusCode ??= $this->statusCode; // status - header(sprintf('HTTP/%s %s %s', $this->version, $statusCode, $this->statusText), true, $statusCode); + header(\sprintf('HTTP/%s %s %s', $this->version, $statusCode, $this->statusText), true, $statusCode); return $this; } @@ -470,7 +470,7 @@ public function setStatusCode(int $code, ?string $text = null): static { $this->statusCode = $code; if ($this->isInvalid()) { - throw new \InvalidArgumentException(sprintf('The HTTP status code "%s" is not valid.', $code)); + throw new \InvalidArgumentException(\sprintf('The HTTP status code "%s" is not valid.', $code)); } if (null === $text) { @@ -973,7 +973,7 @@ public function setEtag(?string $etag, bool $weak = false): static public function setCache(array $options): static { if ($diff = array_diff(array_keys($options), array_keys(self::HTTP_RESPONSE_CACHE_CONTROL_DIRECTIVES))) { - throw new \InvalidArgumentException(sprintf('Response does not support the following options: "%s".', implode('", "', $diff))); + throw new \InvalidArgumentException(\sprintf('Response does not support the following options: "%s".', implode('", "', $diff))); } if (isset($options['etag'])) { diff --git a/ResponseHeaderBag.php b/ResponseHeaderBag.php index c8f08438d..023651efb 100644 --- a/ResponseHeaderBag.php +++ b/ResponseHeaderBag.php @@ -195,7 +195,7 @@ public function removeCookie(string $name, ?string $path = '/', ?string $domain public function getCookies(string $format = self::COOKIES_FLAT): array { if (!\in_array($format, [self::COOKIES_FLAT, self::COOKIES_ARRAY])) { - throw new \InvalidArgumentException(sprintf('Format "%s" invalid (%s).', $format, implode(', ', [self::COOKIES_FLAT, self::COOKIES_ARRAY]))); + throw new \InvalidArgumentException(\sprintf('Format "%s" invalid (%s).', $format, implode(', ', [self::COOKIES_FLAT, self::COOKIES_ARRAY]))); } if (self::COOKIES_ARRAY === $format) { diff --git a/Session/SessionUtils.php b/Session/SessionUtils.php index 504c5848e..57aa565ff 100644 --- a/Session/SessionUtils.php +++ b/Session/SessionUtils.php @@ -28,8 +28,8 @@ final class SessionUtils public static function popSessionCookie(string $sessionName, #[\SensitiveParameter] string $sessionId): ?string { $sessionCookie = null; - $sessionCookiePrefix = sprintf(' %s=', urlencode($sessionName)); - $sessionCookieWithId = sprintf('%s%s;', $sessionCookiePrefix, urlencode($sessionId)); + $sessionCookiePrefix = \sprintf(' %s=', urlencode($sessionName)); + $sessionCookieWithId = \sprintf('%s%s;', $sessionCookiePrefix, urlencode($sessionId)); $otherCookies = []; foreach (headers_list() as $h) { if (0 !== stripos($h, 'Set-Cookie:')) { diff --git a/Session/Storage/Handler/AbstractSessionHandler.php b/Session/Storage/Handler/AbstractSessionHandler.php index 288c24232..fd8562377 100644 --- a/Session/Storage/Handler/AbstractSessionHandler.php +++ b/Session/Storage/Handler/AbstractSessionHandler.php @@ -32,7 +32,7 @@ public function open(string $savePath, string $sessionName): bool { $this->sessionName = $sessionName; if (!headers_sent() && !\ini_get('session.cache_limiter') && '0' !== \ini_get('session.cache_limiter')) { - header(sprintf('Cache-Control: max-age=%d, private, must-revalidate', 60 * (int) \ini_get('session.cache_expire'))); + header(\sprintf('Cache-Control: max-age=%d, private, must-revalidate', 60 * (int) \ini_get('session.cache_expire'))); } return true; @@ -88,7 +88,7 @@ public function destroy(#[\SensitiveParameter] string $sessionId): bool { if (!headers_sent() && filter_var(\ini_get('session.use_cookies'), \FILTER_VALIDATE_BOOL)) { if (!isset($this->sessionName)) { - throw new \LogicException(sprintf('Session name cannot be empty, did you forget to call "parent::open()" in "%s"?.', static::class)); + throw new \LogicException(\sprintf('Session name cannot be empty, did you forget to call "parent::open()" in "%s"?.', static::class)); } $cookie = SessionUtils::popSessionCookie($this->sessionName, $sessionId); diff --git a/Session/Storage/Handler/IdentityMarshaller.php b/Session/Storage/Handler/IdentityMarshaller.php index 411a8d1f0..70ac76248 100644 --- a/Session/Storage/Handler/IdentityMarshaller.php +++ b/Session/Storage/Handler/IdentityMarshaller.php @@ -22,7 +22,7 @@ public function marshall(array $values, ?array &$failed): array { foreach ($values as $key => $value) { if (!\is_string($value)) { - throw new \LogicException(sprintf('%s accepts only string as data.', __METHOD__)); + throw new \LogicException(\sprintf('%s accepts only string as data.', __METHOD__)); } } diff --git a/Session/Storage/Handler/MemcachedSessionHandler.php b/Session/Storage/Handler/MemcachedSessionHandler.php index ecee15f37..4b95d8878 100644 --- a/Session/Storage/Handler/MemcachedSessionHandler.php +++ b/Session/Storage/Handler/MemcachedSessionHandler.php @@ -45,7 +45,7 @@ public function __construct( array $options = [], ) { if ($diff = array_diff(array_keys($options), ['prefix', 'expiretime', 'ttl'])) { - throw new \InvalidArgumentException(sprintf('The following options are not supported "%s".', implode(', ', $diff))); + throw new \InvalidArgumentException(\sprintf('The following options are not supported "%s".', implode(', ', $diff))); } $this->ttl = $options['expiretime'] ?? $options['ttl'] ?? null; diff --git a/Session/Storage/Handler/NativeFileSessionHandler.php b/Session/Storage/Handler/NativeFileSessionHandler.php index f8c6151a4..284cd869d 100644 --- a/Session/Storage/Handler/NativeFileSessionHandler.php +++ b/Session/Storage/Handler/NativeFileSessionHandler.php @@ -34,7 +34,7 @@ public function __construct(?string $savePath = null) if ($count = substr_count($savePath, ';')) { if ($count > 2) { - throw new \InvalidArgumentException(sprintf('Invalid argument $savePath \'%s\'.', $savePath)); + throw new \InvalidArgumentException(\sprintf('Invalid argument $savePath \'%s\'.', $savePath)); } // characters after last ';' are the path @@ -42,7 +42,7 @@ public function __construct(?string $savePath = null) } if ($baseDir && !is_dir($baseDir) && !@mkdir($baseDir, 0777, true) && !is_dir($baseDir)) { - throw new \RuntimeException(sprintf('Session Storage was not able to create directory "%s".', $baseDir)); + throw new \RuntimeException(\sprintf('Session Storage was not able to create directory "%s".', $baseDir)); } if ($savePath !== \ini_get('session.save_path')) { diff --git a/Session/Storage/Handler/PdoSessionHandler.php b/Session/Storage/Handler/PdoSessionHandler.php index aa8ab5690..f08471e9b 100644 --- a/Session/Storage/Handler/PdoSessionHandler.php +++ b/Session/Storage/Handler/PdoSessionHandler.php @@ -155,7 +155,7 @@ public function __construct(#[\SensitiveParameter] \PDO|string|null $pdoOrDsn = { if ($pdoOrDsn instanceof \PDO) { if (\PDO::ERRMODE_EXCEPTION !== $pdoOrDsn->getAttribute(\PDO::ATTR_ERRMODE)) { - throw new \InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION)).', __CLASS__)); + throw new \InvalidArgumentException(\sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION)).', __CLASS__)); } $this->pdo = $pdoOrDsn; @@ -222,7 +222,7 @@ public function configureSchema(Schema $schema, ?\Closure $isSameDatabase = null $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)); + 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'); @@ -255,7 +255,7 @@ public function createTable(): void 'pgsql' => "CREATE TABLE $this->table ($this->idCol VARCHAR(128) NOT NULL PRIMARY KEY, $this->dataCol BYTEA NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)", 'oci' => "CREATE TABLE $this->table ($this->idCol VARCHAR2(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)", 'sqlsrv' => "CREATE TABLE $this->table ($this->idCol VARCHAR(128) NOT NULL PRIMARY KEY, $this->dataCol VARBINARY(MAX) NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)", - default => throw new \DomainException(sprintf('Creating the session table is currently not implemented for PDO driver "%s".', $this->driver)), + default => throw new \DomainException(\sprintf('Creating the session table is currently not implemented for PDO driver "%s".', $this->driver)), }; try { @@ -536,7 +536,7 @@ private function buildDsnFromUrl(#[\SensitiveParameter] string $dsnOrUrl): strin return $dsn; default: - throw new \InvalidArgumentException(sprintf('The scheme "%s" is not supported by the PdoSessionHandler URL configuration. Pass a PDO DSN directly.', $params['scheme'])); + throw new \InvalidArgumentException(\sprintf('The scheme "%s" is not supported by the PdoSessionHandler URL configuration. Pass a PDO DSN directly.', $params['scheme'])); } } @@ -732,7 +732,7 @@ private function doAdvisoryLock(#[\SensitiveParameter] string $sessionId): \PDOS case 'sqlite': throw new \DomainException('SQLite does not support advisory locks.'); default: - throw new \DomainException(sprintf('Advisory locks are currently not implemented for PDO driver "%s".', $this->driver)); + throw new \DomainException(\sprintf('Advisory locks are currently not implemented for PDO driver "%s".', $this->driver)); } } @@ -774,7 +774,7 @@ private function getSelectSql(): string // we already locked when starting transaction break; default: - throw new \DomainException(sprintf('Transactional locks are currently not implemented for PDO driver "%s".', $this->driver)); + throw new \DomainException(\sprintf('Transactional locks are currently not implemented for PDO driver "%s".', $this->driver)); } } diff --git a/Session/Storage/Handler/RedisSessionHandler.php b/Session/Storage/Handler/RedisSessionHandler.php index b696eee4b..78cd4e7c2 100644 --- a/Session/Storage/Handler/RedisSessionHandler.php +++ b/Session/Storage/Handler/RedisSessionHandler.php @@ -44,7 +44,7 @@ public function __construct( array $options = [], ) { if ($diff = array_diff(array_keys($options), ['prefix', 'ttl'])) { - throw new \InvalidArgumentException(sprintf('The following options are not supported "%s".', implode(', ', $diff))); + throw new \InvalidArgumentException(\sprintf('The following options are not supported "%s".', implode(', ', $diff))); } $this->prefix = $options['prefix'] ?? 'sf_s'; diff --git a/Session/Storage/Handler/SessionHandlerFactory.php b/Session/Storage/Handler/SessionHandlerFactory.php index 3f1d03267..13af07c21 100644 --- a/Session/Storage/Handler/SessionHandlerFactory.php +++ b/Session/Storage/Handler/SessionHandlerFactory.php @@ -49,7 +49,7 @@ public static function createHandler(object|string $connection, array $options = return new PdoSessionHandler($connection); case !\is_string($connection): - throw new \InvalidArgumentException(sprintf('Unsupported Connection: "%s".', get_debug_type($connection))); + throw new \InvalidArgumentException(\sprintf('Unsupported Connection: "%s".', get_debug_type($connection))); case str_starts_with($connection, 'file://'): $savePath = substr($connection, 7); @@ -90,6 +90,6 @@ public static function createHandler(object|string $connection, array $options = return new PdoSessionHandler($connection, $options); } - throw new \InvalidArgumentException(sprintf('Unsupported Connection: "%s".', $connection)); + throw new \InvalidArgumentException(\sprintf('Unsupported Connection: "%s".', $connection)); } } diff --git a/Session/Storage/Handler/StrictSessionHandler.php b/Session/Storage/Handler/StrictSessionHandler.php index 74a9962c7..0d84eac34 100644 --- a/Session/Storage/Handler/StrictSessionHandler.php +++ b/Session/Storage/Handler/StrictSessionHandler.php @@ -24,7 +24,7 @@ public function __construct( private \SessionHandlerInterface $handler, ) { if ($handler instanceof \SessionUpdateTimestampHandlerInterface) { - throw new \LogicException(sprintf('"%s" is already an instance of "SessionUpdateTimestampHandlerInterface", you cannot wrap it with "%s".', get_debug_type($handler), self::class)); + throw new \LogicException(\sprintf('"%s" is already an instance of "SessionUpdateTimestampHandlerInterface", you cannot wrap it with "%s".', get_debug_type($handler), self::class)); } } diff --git a/Session/Storage/MockArraySessionStorage.php b/Session/Storage/MockArraySessionStorage.php index 8e8e3109b..7869d1be7 100644 --- a/Session/Storage/MockArraySessionStorage.php +++ b/Session/Storage/MockArraySessionStorage.php @@ -133,7 +133,7 @@ public function registerBag(SessionBagInterface $bag): void public function getBag(string $name): SessionBagInterface { if (!isset($this->bags[$name])) { - throw new \InvalidArgumentException(sprintf('The SessionBagInterface "%s" is not registered.', $name)); + throw new \InvalidArgumentException(\sprintf('The SessionBagInterface "%s" is not registered.', $name)); } if (!$this->started) { diff --git a/Session/Storage/MockFileSessionStorage.php b/Session/Storage/MockFileSessionStorage.php index 48dd74dfb..c230c701a 100644 --- a/Session/Storage/MockFileSessionStorage.php +++ b/Session/Storage/MockFileSessionStorage.php @@ -35,7 +35,7 @@ public function __construct(?string $savePath = null, string $name = 'MOCKSESSID $savePath ??= sys_get_temp_dir(); if (!is_dir($savePath) && !@mkdir($savePath, 0777, true) && !is_dir($savePath)) { - throw new \RuntimeException(sprintf('Session Storage was not able to create directory "%s".', $savePath)); + throw new \RuntimeException(\sprintf('Session Storage was not able to create directory "%s".', $savePath)); } $this->savePath = $savePath; diff --git a/Session/Storage/NativeSessionStorage.php b/Session/Storage/NativeSessionStorage.php index d32292ae0..239064214 100644 --- a/Session/Storage/NativeSessionStorage.php +++ b/Session/Storage/NativeSessionStorage.php @@ -113,7 +113,7 @@ public function start(): bool } if (filter_var(\ini_get('session.use_cookies'), \FILTER_VALIDATE_BOOL) && headers_sent($file, $line)) { - throw new \RuntimeException(sprintf('Failed to start the session because headers have already been sent by "%s" at line %d.', $file, $line)); + throw new \RuntimeException(\sprintf('Failed to start the session because headers have already been sent by "%s" at line %d.', $file, $line)); } $sessionId = $_COOKIE[session_name()] ?? null; @@ -224,7 +224,7 @@ public function save(): void $previousHandler = set_error_handler(function ($type, $msg, $file, $line) use (&$previousHandler) { if (\E_WARNING === $type && str_starts_with($msg, 'session_write_close():')) { $handler = $this->saveHandler instanceof SessionHandlerProxy ? $this->saveHandler->getHandler() : $this->saveHandler; - $msg = sprintf('session_write_close(): Failed to write session data with "%s" handler', $handler::class); + $msg = \sprintf('session_write_close(): Failed to write session data with "%s" handler', $handler::class); } return $previousHandler ? $previousHandler($type, $msg, $file, $line) : false; @@ -271,7 +271,7 @@ public function registerBag(SessionBagInterface $bag): void public function getBag(string $name): SessionBagInterface { if (!isset($this->bags[$name])) { - throw new \InvalidArgumentException(sprintf('The SessionBagInterface "%s" is not registered.', $name)); + throw new \InvalidArgumentException(\sprintf('The SessionBagInterface "%s" is not registered.', $name)); } if (!$this->started && $this->saveHandler->isActive()) { diff --git a/Test/Constraint/RequestAttributeValueSame.php b/Test/Constraint/RequestAttributeValueSame.php index fe910f063..c570848fb 100644 --- a/Test/Constraint/RequestAttributeValueSame.php +++ b/Test/Constraint/RequestAttributeValueSame.php @@ -24,7 +24,7 @@ public function __construct( public function toString(): string { - return sprintf('has attribute "%s" with value "%s"', $this->name, $this->value); + return \sprintf('has attribute "%s" with value "%s"', $this->name, $this->value); } /** diff --git a/Test/Constraint/ResponseCookieValueSame.php b/Test/Constraint/ResponseCookieValueSame.php index 936496a28..d93a46187 100644 --- a/Test/Constraint/ResponseCookieValueSame.php +++ b/Test/Constraint/ResponseCookieValueSame.php @@ -27,14 +27,14 @@ public function __construct( public function toString(): string { - $str = sprintf('has cookie "%s"', $this->name); + $str = \sprintf('has cookie "%s"', $this->name); if ('/' !== $this->path) { - $str .= sprintf(' with path "%s"', $this->path); + $str .= \sprintf(' with path "%s"', $this->path); } if ($this->domain) { - $str .= sprintf(' for domain "%s"', $this->domain); + $str .= \sprintf(' for domain "%s"', $this->domain); } - $str .= sprintf(' with value "%s"', $this->value); + $str .= \sprintf(' with value "%s"', $this->value); return $str; } diff --git a/Test/Constraint/ResponseHasCookie.php b/Test/Constraint/ResponseHasCookie.php index aff7d4944..0bc58036f 100644 --- a/Test/Constraint/ResponseHasCookie.php +++ b/Test/Constraint/ResponseHasCookie.php @@ -26,12 +26,12 @@ public function __construct( public function toString(): string { - $str = sprintf('has cookie "%s"', $this->name); + $str = \sprintf('has cookie "%s"', $this->name); if ('/' !== $this->path) { - $str .= sprintf(' with path "%s"', $this->path); + $str .= \sprintf(' with path "%s"', $this->path); } if ($this->domain) { - $str .= sprintf(' for domain "%s"', $this->domain); + $str .= \sprintf(' for domain "%s"', $this->domain); } return $str; diff --git a/Test/Constraint/ResponseHasHeader.php b/Test/Constraint/ResponseHasHeader.php index 4dbd0c997..52fd3c180 100644 --- a/Test/Constraint/ResponseHasHeader.php +++ b/Test/Constraint/ResponseHasHeader.php @@ -23,7 +23,7 @@ public function __construct( public function toString(): string { - return sprintf('has header "%s"', $this->headerName); + return \sprintf('has header "%s"', $this->headerName); } /** diff --git a/Test/Constraint/ResponseHeaderLocationSame.php b/Test/Constraint/ResponseHeaderLocationSame.php index 9286ec715..833ffd9f2 100644 --- a/Test/Constraint/ResponseHeaderLocationSame.php +++ b/Test/Constraint/ResponseHeaderLocationSame.php @@ -23,7 +23,7 @@ public function __construct(private Request $request, private string $expectedVa public function toString(): string { - return sprintf('has header "Location" matching "%s"', $this->expectedValue); + return \sprintf('has header "Location" matching "%s"', $this->expectedValue); } protected function matches($other): bool @@ -53,7 +53,7 @@ private function toFullUrl(string $url): string } if (str_starts_with($url, '//')) { - return sprintf('%s:%s', $this->request->getScheme(), $url); + return \sprintf('%s:%s', $this->request->getScheme(), $url); } if (str_starts_with($url, '/')) { diff --git a/Test/Constraint/ResponseHeaderSame.php b/Test/Constraint/ResponseHeaderSame.php index af1cb5fbb..f2ae27f5b 100644 --- a/Test/Constraint/ResponseHeaderSame.php +++ b/Test/Constraint/ResponseHeaderSame.php @@ -24,7 +24,7 @@ public function __construct( public function toString(): string { - return sprintf('has header "%s" with value "%s"', $this->headerName, $this->expectedValue); + return \sprintf('has header "%s" with value "%s"', $this->headerName, $this->expectedValue); } /** diff --git a/Tests/RequestTest.php b/Tests/RequestTest.php index 89b38ae92..f1902ad8c 100644 --- a/Tests/RequestTest.php +++ b/Tests/RequestTest.php @@ -2652,7 +2652,7 @@ public static function preferSafeContentData() public function testReservedFlags() { foreach ((new \ReflectionClass(Request::class))->getConstants() as $constant => $value) { - $this->assertNotSame(0b10000000, $value, sprintf('The constant "%s" should not use the reserved value "0b10000000".', $constant)); + $this->assertNotSame(0b10000000, $value, \sprintf('The constant "%s" should not use the reserved value "0b10000000".', $constant)); } } diff --git a/Tests/ResponseFunctionalTest.php b/Tests/ResponseFunctionalTest.php index 1b3566a2c..e5c6c2428 100644 --- a/Tests/ResponseFunctionalTest.php +++ b/Tests/ResponseFunctionalTest.php @@ -45,9 +45,9 @@ public static function tearDownAfterClass(): void */ public function testCookie($fixture) { - $result = file_get_contents(sprintf('http://localhost:8054/%s.php', $fixture)); + $result = file_get_contents(\sprintf('http://localhost:8054/%s.php', $fixture)); $result = preg_replace_callback('/expires=[^;]++/', fn ($m) => str_replace('-', ' ', $m[0]), $result); - $this->assertStringMatchesFormatFile(__DIR__.sprintf('/Fixtures/response-functional/%s.expected', $fixture), $result); + $this->assertStringMatchesFormatFile(__DIR__.\sprintf('/Fixtures/response-functional/%s.expected', $fixture), $result); } public static function provideCookie() diff --git a/Tests/ResponseTest.php b/Tests/ResponseTest.php index 007842476..491a50fd5 100644 --- a/Tests/ResponseTest.php +++ b/Tests/ResponseTest.php @@ -181,7 +181,7 @@ public function testIsNotModifiedEtag() $etagTwo = 'randomly_generated_etag_2'; $request = new Request(); - $request->headers->set('If-None-Match', sprintf('%s, %s, %s', $etagOne, $etagTwo, 'etagThree')); + $request->headers->set('If-None-Match', \sprintf('%s, %s, %s', $etagOne, $etagTwo, 'etagThree')); $response = new Response(); @@ -235,7 +235,7 @@ public function testIsNotModifiedLastModifiedAndEtag() $etag = 'randomly_generated_etag'; $request = new Request(); - $request->headers->set('If-None-Match', sprintf('%s, %s', $etag, 'etagThree')); + $request->headers->set('If-None-Match', \sprintf('%s, %s', $etag, 'etagThree')); $request->headers->set('If-Modified-Since', $modified); $response = new Response(); @@ -259,7 +259,7 @@ public function testIsNotModifiedIfModifiedSinceAndEtagWithoutLastModified() $etag = 'randomly_generated_etag'; $request = new Request(); - $request->headers->set('If-None-Match', sprintf('%s, %s', $etag, 'etagThree')); + $request->headers->set('If-None-Match', \sprintf('%s, %s', $etag, 'etagThree')); $request->headers->set('If-Modified-Since', $modified); $response = new Response(); diff --git a/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php b/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php index 27fb57da4..361d3b15d 100644 --- a/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php @@ -45,10 +45,10 @@ 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 = file_get_contents(\sprintf('http://localhost:8053/%s.php', $fixture), false, $context); $result = preg_replace_callback('/expires=[^;]++/', fn ($m) => str_replace('-', ' ', $m[0]), $result); - $this->assertStringEqualsFile(__DIR__.sprintf('/Fixtures/%s.expected', $fixture), $result); + $this->assertStringEqualsFile(__DIR__.\sprintf('/Fixtures/%s.expected', $fixture), $result); } public static function provideSession() diff --git a/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php b/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php index 0c6de4c8d..0027b91fd 100644 --- a/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php @@ -51,7 +51,7 @@ protected function setUp(): void 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->markTestSkipped(\sprintf('MongoDB Server "%s" not running: %s', getenv('MONGODB_HOST'), $e->getMessage())); } $this->options = [ diff --git a/UriSigner.php b/UriSigner.php index f9fba605b..dd7443489 100644 --- a/UriSigner.php +++ b/UriSigner.php @@ -55,7 +55,7 @@ public function sign(string $uri/*, \DateTimeInterface|\DateInterval|int|null $e } if (null !== $expiration && !$expiration instanceof \DateTimeInterface && !$expiration instanceof \DateInterval && !\is_int($expiration)) { - throw new \TypeError(sprintf('The second argument of %s() must be an instance of %s or %s, an integer or null (%s given).', __METHOD__, \DateTimeInterface::class, \DateInterval::class, get_debug_type($expiration))); + throw new \TypeError(\sprintf('The second argument of %s() must be an instance of %s or %s, an integer or null (%s given).', __METHOD__, \DateTimeInterface::class, \DateInterval::class, get_debug_type($expiration))); } $url = parse_url($uri); @@ -66,11 +66,11 @@ public function sign(string $uri/*, \DateTimeInterface|\DateInterval|int|null $e } if (isset($params[$this->hashParameter])) { - throw new LogicException(sprintf('URI query parameter conflict: parameter name "%s" is reserved.', $this->hashParameter)); + throw new LogicException(\sprintf('URI query parameter conflict: parameter name "%s" is reserved.', $this->hashParameter)); } if (isset($params[$this->expirationParameter])) { - throw new LogicException(sprintf('URI query parameter conflict: parameter name "%s" is reserved.', $this->expirationParameter)); + throw new LogicException(\sprintf('URI query parameter conflict: parameter name "%s" is reserved.', $this->expirationParameter)); } if (null !== $expiration) { From 1d115494660af99ab328feda260f72f48231afdc Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Sun, 16 Jun 2024 17:17:26 +0200 Subject: [PATCH 05/44] chore: CS fixes --- Tests/ParameterBagTest.php | 2 +- Tests/Session/Flash/AutoExpireFlashBagTest.php | 12 ++++++------ Tests/Session/Flash/FlashBagTest.php | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Tests/ParameterBagTest.php b/Tests/ParameterBagTest.php index c73d70507..9a34b5f99 100644 --- a/Tests/ParameterBagTest.php +++ b/Tests/ParameterBagTest.php @@ -251,7 +251,7 @@ public function testFilter() 'dec' => '256', 'hex' => '0x100', 'array' => ['bang'], - ]); + ]); $this->assertEmpty($bag->filter('nokey'), '->filter() should return empty by default if no key is found'); diff --git a/Tests/Session/Flash/AutoExpireFlashBagTest.php b/Tests/Session/Flash/AutoExpireFlashBagTest.php index 6a6510a57..0b418c006 100644 --- a/Tests/Session/Flash/AutoExpireFlashBagTest.php +++ b/Tests/Session/Flash/AutoExpireFlashBagTest.php @@ -46,9 +46,9 @@ public function testInitialize() $bag->initialize($array); $this->assertEquals(['A previous flash message'], $bag->peek('notice')); $array = ['new' => [ - 'notice' => ['Something else'], - 'error' => ['a'], - ]]; + 'notice' => ['Something else'], + 'error' => ['a'], + ]]; $bag->initialize($array); $this->assertEquals(['Something else'], $bag->peek('notice')); $this->assertEquals(['a'], $bag->peek('error')); @@ -106,13 +106,13 @@ public function testPeekAll() $this->assertEquals([ 'notice' => 'Foo', 'error' => 'Bar', - ], $this->bag->peekAll() + ], $this->bag->peekAll() ); $this->assertEquals([ 'notice' => 'Foo', 'error' => 'Bar', - ], $this->bag->peekAll() + ], $this->bag->peekAll() ); } @@ -137,7 +137,7 @@ public function testAll() $this->bag->set('error', 'Bar'); $this->assertEquals([ 'notice' => ['A previous flash message'], - ], $this->bag->all() + ], $this->bag->all() ); $this->assertEquals([], $this->bag->all()); diff --git a/Tests/Session/Flash/FlashBagTest.php b/Tests/Session/Flash/FlashBagTest.php index 59e3f1f0e..8163ba769 100644 --- a/Tests/Session/Flash/FlashBagTest.php +++ b/Tests/Session/Flash/FlashBagTest.php @@ -141,14 +141,14 @@ public function testPeekAll() $this->assertEquals([ 'notice' => ['Foo'], 'error' => ['Bar'], - ], $this->bag->peekAll() + ], $this->bag->peekAll() ); $this->assertTrue($this->bag->has('notice')); $this->assertTrue($this->bag->has('error')); $this->assertEquals([ 'notice' => ['Foo'], 'error' => ['Bar'], - ], $this->bag->peekAll() + ], $this->bag->peekAll() ); } } From 3c12220e5ab96e91dd7f03aaae6bc27458962246 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 6 Jul 2024 09:57:16 +0200 Subject: [PATCH 06/44] Update .gitattributes --- .gitattributes | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 From 1a9b7d56cde7b234f5fa1230684be1bf655292f3 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Thu, 18 Jul 2024 11:54:36 +0200 Subject: [PATCH 07/44] [HttpFoundation] Remove always false condition in `BinaryFileResponse` --- BinaryFileResponse.php | 4 ++-- Tests/BinaryFileResponseTest.php | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/BinaryFileResponse.php b/BinaryFileResponse.php index 535aa6068..cb03ea6d0 100644 --- a/BinaryFileResponse.php +++ b/BinaryFileResponse.php @@ -110,8 +110,8 @@ public function getFile(): File */ public function setChunkSize(int $chunkSize): static { - if ($chunkSize < 1 || $chunkSize > \PHP_INT_MAX) { - throw new \LogicException('The chunk size of a BinaryFileResponse cannot be less than 1 or greater than PHP_INT_MAX.'); + if ($chunkSize < 1) { + throw new \InvalidArgumentException('The chunk size of a BinaryFileResponse cannot be less than 1.'); } $this->chunkSize = $chunkSize; diff --git a/Tests/BinaryFileResponseTest.php b/Tests/BinaryFileResponseTest.php index b58276cf7..bad3329d2 100644 --- a/Tests/BinaryFileResponseTest.php +++ b/Tests/BinaryFileResponseTest.php @@ -455,4 +455,14 @@ public function testCreateFromTemporaryFile() $string = ob_get_clean(); $this->assertSame('foo,bar', $string); } + + public function testSetChunkSizeTooSmall() + { + $response = new BinaryFileResponse(__DIR__.'/File/Fixtures/test.gif'); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The chunk size of a BinaryFileResponse cannot be less than 1.'); + + $response->setChunkSize(0); + } } From 7e85301bf46e912e3450316d3db49a5755af0be5 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Fri, 19 Jul 2024 10:48:20 +0200 Subject: [PATCH 08/44] [HttpFoundation][HttpKernel] Remove dead code and useless casts --- BinaryFileResponse.php | 2 +- File/File.php | 3 +-- HeaderBag.php | 2 +- Request.php | 2 +- Test/Constraint/ResponseCookieValueSame.php | 3 +-- 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/BinaryFileResponse.php b/BinaryFileResponse.php index cb03ea6d0..bf4e446e3 100644 --- a/BinaryFileResponse.php +++ b/BinaryFileResponse.php @@ -70,7 +70,7 @@ public function setFile(\SplFileInfo|string $file, ?string $contentDisposition = if ($file instanceof \SplFileInfo) { $file = new File($file->getPathname(), !$isTemporaryFile); } else { - $file = new File((string) $file); + $file = new File($file); } } diff --git a/File/File.php b/File/File.php index c369ecbfb..2194c178a 100644 --- a/File/File.php +++ b/File/File.php @@ -134,8 +134,7 @@ protected function getName(string $name): string { $originalName = str_replace('\\', '/', $name); $pos = strrpos($originalName, '/'); - $originalName = false === $pos ? $originalName : substr($originalName, $pos + 1); - return $originalName; + return false === $pos ? $originalName : substr($originalName, $pos + 1); } } diff --git a/HeaderBag.php b/HeaderBag.php index 12a2e1bdb..c2ede560b 100644 --- a/HeaderBag.php +++ b/HeaderBag.php @@ -118,7 +118,7 @@ public function get(string $key, ?string $default = null): ?string return null; } - return (string) $headers[0]; + return $headers[0]; } /** diff --git a/Request.php b/Request.php index 75618e1c6..3e20823de 100644 --- a/Request.php +++ b/Request.php @@ -1878,7 +1878,7 @@ protected function preparePathInfo(): string } $pathInfo = substr($requestUri, \strlen($baseUrl)); - if (false === $pathInfo || '' === $pathInfo) { + if ('' === $pathInfo) { // If substr() returns false then PATH_INFO is set to an empty string return '/'; } diff --git a/Test/Constraint/ResponseCookieValueSame.php b/Test/Constraint/ResponseCookieValueSame.php index d93a46187..dbf9add6f 100644 --- a/Test/Constraint/ResponseCookieValueSame.php +++ b/Test/Constraint/ResponseCookieValueSame.php @@ -34,9 +34,8 @@ public function toString(): string if ($this->domain) { $str .= \sprintf(' for domain "%s"', $this->domain); } - $str .= \sprintf(' with value "%s"', $this->value); - return $str; + return $str.\sprintf(' with value "%s"', $this->value); } /** From 33500b2572f1d684d4447112ea87f5e679950c37 Mon Sep 17 00:00:00 2001 From: Alexander Schranz Date: Fri, 2 Aug 2024 10:47:01 +0200 Subject: [PATCH 09/44] [HttpFoundation] Add `$requests` parameter to `RequestStack` constructor --- CHANGELOG.md | 5 +++++ RequestStack.php | 10 ++++++++++ Tests/RequestStackTest.php | 7 +++++++ 3 files changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 003470567..64c2d38f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.2 +--- + + * Add optional `$requests` argument to `RequestStack::__construct()` + 7.1 --- diff --git a/RequestStack.php b/RequestStack.php index ac8263e91..c358ea68c 100644 --- a/RequestStack.php +++ b/RequestStack.php @@ -26,6 +26,16 @@ class RequestStack */ private array $requests = []; + /** + * @param Request[] $requests + */ + public function __construct(array $requests = []) + { + foreach ($requests as $request) { + $this->push($request); + } + } + /** * Pushes a Request on the stack. * diff --git a/Tests/RequestStackTest.php b/Tests/RequestStackTest.php index 2b26ce5c6..6fba27589 100644 --- a/Tests/RequestStackTest.php +++ b/Tests/RequestStackTest.php @@ -17,6 +17,13 @@ class RequestStackTest extends TestCase { + public function testConstruct() + { + $request = Request::create('/foo'); + $requestStack = new RequestStack([$request]); + $this->assertSame($request, $requestStack->getCurrentRequest()); + } + public function testGetCurrentRequest() { $requestStack = new RequestStack(); From 79d451132e00b4c230398d5f7825fe6981ab1c1a Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Mon, 5 Aug 2024 09:12:25 +0200 Subject: [PATCH 10/44] Fix multiple CS errors --- Tests/RateLimiter/AbstractRequestRateLimiterTest.php | 1 - Tests/Session/Storage/Proxy/AbstractProxyTest.php | 1 - 2 files changed, 2 deletions(-) diff --git a/Tests/RateLimiter/AbstractRequestRateLimiterTest.php b/Tests/RateLimiter/AbstractRequestRateLimiterTest.php index 26f2fac90..087d7aeae 100644 --- a/Tests/RateLimiter/AbstractRequestRateLimiterTest.php +++ b/Tests/RateLimiter/AbstractRequestRateLimiterTest.php @@ -14,7 +14,6 @@ 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 diff --git a/Tests/Session/Storage/Proxy/AbstractProxyTest.php b/Tests/Session/Storage/Proxy/AbstractProxyTest.php index bb459bb9f..8d04830a7 100644 --- a/Tests/Session/Storage/Proxy/AbstractProxyTest.php +++ b/Tests/Session/Storage/Proxy/AbstractProxyTest.php @@ -11,7 +11,6 @@ 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; From cc18d66ec3ec568e26291209e32993bf6d61f72a Mon Sep 17 00:00:00 2001 From: Roy de Vos Burchart Date: Thu, 1 Aug 2024 17:21:17 +0200 Subject: [PATCH 11/44] Code style change in `@PER-CS2.0` affecting `@Symfony` (parentheses for anonymous classes) --- Tests/InputBagTest.php | 4 ++-- Tests/JsonResponseTest.php | 2 +- Tests/ParameterBagTest.php | 2 +- Tests/RequestTest.php | 2 +- Tests/Session/Storage/Proxy/AbstractProxyTest.php | 2 +- Tests/StreamedJsonResponseTest.php | 6 +++--- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Tests/InputBagTest.php b/Tests/InputBagTest.php index 01df837b2..36eb9ead5 100644 --- a/Tests/InputBagTest.php +++ b/Tests/InputBagTest.php @@ -20,7 +20,7 @@ class InputBagTest extends TestCase { public function testGet() { - $bag = new InputBag(['foo' => 'bar', 'null' => null, 'int' => 1, 'float' => 1.0, 'bool' => false, 'stringable' => new class() implements \Stringable { + $bag = new InputBag(['foo' => 'bar', 'null' => null, 'int' => 1, 'float' => 1.0, 'bool' => false, 'stringable' => new class implements \Stringable { public function __toString(): string { return 'strval'; @@ -58,7 +58,7 @@ public function testGetBooleanError() public function testGetString() { - $bag = new InputBag(['integer' => 123, 'bool_true' => true, 'bool_false' => false, 'string' => 'abc', 'stringable' => new class() implements \Stringable { + $bag = new InputBag(['integer' => 123, 'bool_true' => true, 'bool_false' => false, 'string' => 'abc', 'stringable' => new class implements \Stringable { public function __toString(): string { return 'strval'; diff --git a/Tests/JsonResponseTest.php b/Tests/JsonResponseTest.php index 2058280c5..67a038279 100644 --- a/Tests/JsonResponseTest.php +++ b/Tests/JsonResponseTest.php @@ -171,7 +171,7 @@ public function testConstructorWithNullAsDataThrowsAnUnexpectedValueException() public function testConstructorWithObjectWithToStringMethod() { - $class = new class() { + $class = new class { public function __toString(): string { return '{}'; diff --git a/Tests/ParameterBagTest.php b/Tests/ParameterBagTest.php index 9a34b5f99..ac2b4da5a 100644 --- a/Tests/ParameterBagTest.php +++ b/Tests/ParameterBagTest.php @@ -206,7 +206,7 @@ public function testGetIntExceptionWithInvalid() public function testGetString() { - $bag = new ParameterBag(['integer' => 123, 'bool_true' => true, 'bool_false' => false, 'string' => 'abc', 'stringable' => new class() implements \Stringable { + $bag = new ParameterBag(['integer' => 123, 'bool_true' => true, 'bool_false' => false, 'string' => 'abc', 'stringable' => new class implements \Stringable { public function __toString(): string { return 'strval'; diff --git a/Tests/RequestTest.php b/Tests/RequestTest.php index f1902ad8c..6f3543322 100644 --- a/Tests/RequestTest.php +++ b/Tests/RequestTest.php @@ -2213,7 +2213,7 @@ public function testFactory() public function testFactoryCallable() { - $requestFactory = new class() { + $requestFactory = new class { public function createRequest(): Request { return new NewRequest(); diff --git a/Tests/Session/Storage/Proxy/AbstractProxyTest.php b/Tests/Session/Storage/Proxy/AbstractProxyTest.php index bb459bb9f..1fcd9fd53 100644 --- a/Tests/Session/Storage/Proxy/AbstractProxyTest.php +++ b/Tests/Session/Storage/Proxy/AbstractProxyTest.php @@ -27,7 +27,7 @@ class AbstractProxyTest extends TestCase protected function setUp(): void { - $this->proxy = new class() extends AbstractProxy {}; + $this->proxy = new class extends AbstractProxy {}; } public function testGetSaveHandlerName() diff --git a/Tests/StreamedJsonResponseTest.php b/Tests/StreamedJsonResponseTest.php index db76cd3ae..3d94aa078 100644 --- a/Tests/StreamedJsonResponseTest.php +++ b/Tests/StreamedJsonResponseTest.php @@ -132,14 +132,14 @@ public function testResponseOtherTraversable() { $arrayObject = new \ArrayObject(['__symfony_json__' => '__symfony_json__']); - $iteratorAggregate = new class() implements \IteratorAggregate { + $iteratorAggregate = new class implements \IteratorAggregate { public function getIterator(): \Traversable { return new \ArrayIterator(['__symfony_json__']); } }; - $jsonSerializable = new class() implements \IteratorAggregate, \JsonSerializable { + $jsonSerializable = new class implements \IteratorAggregate, \JsonSerializable { public function getIterator(): \Traversable { return new \ArrayIterator(['This should be ignored']); @@ -187,7 +187,7 @@ public function testResponseStatusCode() public function testPlaceholderAsObjectStructure() { - $object = new class() { + $object = new class { public $__symfony_json__ = 'foo'; public $bar = '__symfony_json__'; }; From 79d0023c23d92d49c43be47c726e92366b4d3a81 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Tue, 13 Aug 2024 14:32:31 +0200 Subject: [PATCH 12/44] [httpFoundation] Use typed property --- Request.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Request.php b/Request.php index 3e20823de..c17d1d10c 100644 --- a/Request.php +++ b/Request.php @@ -193,8 +193,7 @@ class Request self::HEADER_X_FORWARDED_PREFIX => 'X_FORWARDED_PREFIX', ]; - /** @var bool */ - private $isIisRewrite = false; + private bool $isIisRewrite = false; /** * @param array $query The GET parameters From 1579e3dfac42cf04201154041945750effd3bb58 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Tue, 20 Aug 2024 10:02:53 +0200 Subject: [PATCH 13/44] [HttpFoundation] Add optional `$v4Bytes` and `$v6Bytes` parameters to `IpUtils::anonymize()` --- CHANGELOG.md | 3 +- IpUtils.php | 34 +++++++++++++++++++---- Tests/IpUtilsTest.php | 64 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64c2d38f2..ce1b33939 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ CHANGELOG 7.2 --- - * Add optional `$requests` argument to `RequestStack::__construct()` + * Add optional `$requests` parameter to `RequestStack::__construct()` + * Add optional `$v4Bytes` and `$v6Bytes` parameters to `IpUtils::anonymize()` 7.1 --- diff --git a/IpUtils.php b/IpUtils.php index db2c8efdc..5e1e29c95 100644 --- a/IpUtils.php +++ b/IpUtils.php @@ -178,25 +178,47 @@ public static function checkIp6(string $requestIp, string $ip): bool /** * Anonymizes an IP/IPv6. * - * Removes the last byte for v4 and the last 8 bytes for v6 IPs + * Removes the last bytes of IPv4 and IPv6 addresses (1 byte for IPv4 and 8 bytes for IPv6 by default). + * + * @param int<0, 4> $v4Bytes + * @param int<0, 16> $v6Bytes */ - public static function anonymize(string $ip): string + public static function anonymize(string $ip/* , int $v4Bytes = 1, int $v6Bytes = 8 */): string { + $v4Bytes = 1 < \func_num_args() ? func_get_arg(1) : 1; + $v6Bytes = 2 < \func_num_args() ? func_get_arg(2) : 8; + + if ($v4Bytes < 0 || $v6Bytes < 0) { + throw new \InvalidArgumentException('Cannot anonymize less than 0 bytes.'); + } + + if ($v4Bytes > 4 || $v6Bytes > 16) { + throw new \InvalidArgumentException('Cannot anonymize more than 4 bytes for IPv4 and 16 bytes for IPv6.'); + } + $wrappedIPv6 = false; if (str_starts_with($ip, '[') && str_ends_with($ip, ']')) { $wrappedIPv6 = true; $ip = substr($ip, 1, -1); } + $mappedIpV4MaskGenerator = function (string $mask, int $bytesToAnonymize) { + $mask .= str_repeat('ff', 4 - $bytesToAnonymize); + $mask .= str_repeat('00', $bytesToAnonymize); + + return '::'.implode(':', str_split($mask, 4)); + }; + $packedAddress = inet_pton($ip); if (4 === \strlen($packedAddress)) { - $mask = '255.255.255.0'; + $mask = rtrim(str_repeat('255.', 4 - $v4Bytes).str_repeat('0.', $v4Bytes), '.'); } elseif ($ip === inet_ntop($packedAddress & inet_pton('::ffff:ffff:ffff'))) { - $mask = '::ffff:ffff:ff00'; + $mask = $mappedIpV4MaskGenerator('ffff', $v4Bytes); } elseif ($ip === inet_ntop($packedAddress & inet_pton('::ffff:ffff'))) { - $mask = '::ffff:ff00'; + $mask = $mappedIpV4MaskGenerator('', $v4Bytes); } else { - $mask = 'ffff:ffff:ffff:ffff:0000:0000:0000:0000'; + $mask = str_repeat('ff', 16 - $v6Bytes).str_repeat('00', $v6Bytes); + $mask = implode(':', str_split($mask, 4)); } $ip = inet_ntop($packedAddress & inet_pton($mask)); diff --git a/Tests/IpUtilsTest.php b/Tests/IpUtilsTest.php index ce93c69e9..95044106b 100644 --- a/Tests/IpUtilsTest.php +++ b/Tests/IpUtilsTest.php @@ -150,6 +150,70 @@ public static function anonymizedIpData() ]; } + /** + * @dataProvider anonymizedIpDataWithBytes + */ + public function testAnonymizeWithBytes($ip, $expected, $bytesForV4, $bytesForV6) + { + $this->assertSame($expected, IpUtils::anonymize($ip, $bytesForV4, $bytesForV6)); + } + + public static function anonymizedIpDataWithBytes(): array + { + return [ + ['192.168.1.1', '192.168.0.0', 2, 8], + ['192.168.1.1', '192.0.0.0', 3, 8], + ['192.168.1.1', '0.0.0.0', 4, 8], + ['1.2.3.4', '1.2.3.0', 1, 8], + ['1.2.3.4', '1.2.3.4', 0, 8], + ['2a01:198:603:0:396e:4789:8e99:890f', '2a01:198:603:0:396e:4789:8e99:890f', 1, 0], + ['2a01:198:603:0:396e:4789:8e99:890f', '2a01:198:603:0:396e:4789::', 1, 4], + ['2a01:198:603:10:396e:4789:8e99:890f', '2a01:198:603:10:396e:4700::', 1, 5], + ['2a01:198:603:10:396e:4789:8e99:890f', '2a00::', 1, 15], + ['2a01:198:603:10:396e:4789:8e99:890f', '::', 1, 16], + ['::1', '::', 1, 1], + ['0:0:0:0:0:0:0:1', '::', 1, 1], + ['1:0:0:0:0:0:0:1', '1::', 1, 1], + ['0:0:603:50:396e:4789:8e99:0001', '0:0:603::', 1, 10], + ['[0:0:603:50:396e:4789:8e99:0001]', '[::603:50:396e:4789:8e00:0]', 1, 3], + ['[2a01:198::3]', '[2a01:198::]', 1, 2], + ['::ffff:123.234.235.236', '::ffff:123.234.235.0', 1, 8], // IPv4-mapped IPv6 addresses + ['::123.234.235.236', '::123.234.0.0', 2, 8], // deprecated IPv4-compatible IPv6 address + ]; + } + + public function testAnonymizeV4WithNegativeBytes() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Cannot anonymize less than 0 bytes.'); + + IpUtils::anonymize('anything', -1, 8); + } + + public function testAnonymizeV6WithNegativeBytes() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Cannot anonymize less than 0 bytes.'); + + IpUtils::anonymize('anything', 1, -1); + } + + public function testAnonymizeV4WithTooManyBytes() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Cannot anonymize more than 4 bytes for IPv4 and 16 bytes for IPv6.'); + + IpUtils::anonymize('anything', 5, 8); + } + + public function testAnonymizeV6WithTooManyBytes() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Cannot anonymize more than 4 bytes for IPv4 and 16 bytes for IPv6.'); + + IpUtils::anonymize('anything', 1, 17); + } + /** * @dataProvider getIp4SubnetMaskZeroData */ From 8359de79c85278958535311a7718a00b81827c7f Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 26 Aug 2024 17:35:30 +0200 Subject: [PATCH 14/44] Use Stringable whenever possible --- JsonResponse.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JsonResponse.php b/JsonResponse.php index cd6f177df..187173b68 100644 --- a/JsonResponse.php +++ b/JsonResponse.php @@ -40,7 +40,7 @@ public function __construct(mixed $data = null, int $status = 200, array $header { parent::__construct('', $status, $headers); - if ($json && !\is_string($data) && !is_numeric($data) && !\is_callable([$data, '__toString'])) { + if ($json && !\is_string($data) && !is_numeric($data) && !$data instanceof \Stringable) { throw new \TypeError(\sprintf('"%s": If $json is set to true, argument $data must be a string or object implementing __toString(), "%s" given.', __METHOD__, get_debug_type($data))); } From 981886d01290d8ed642c4b42ba25c5a2c5440377 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 3 Sep 2024 10:21:02 +0200 Subject: [PATCH 15/44] [HttpFoundation] Add `PRIVATE_SUBNETS` as a shortcut for private IP address ranges to `Request::setTrustedProxies()` --- CHANGELOG.md | 1 + Request.php | 24 +++++++++++++++--------- Tests/RequestTest.php | 31 +++++++++++++++++++++---------- 3 files changed, 37 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce1b33939..c3814fddd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * Add optional `$requests` parameter to `RequestStack::__construct()` * Add optional `$v4Bytes` and `$v6Bytes` parameters to `IpUtils::anonymize()` + * Add `PRIVATE_SUBNETS` as a shortcut for private IP address ranges to `Request::setTrustedProxies()` 7.1 --- diff --git a/Request.php b/Request.php index c17d1d10c..6c670cb08 100644 --- a/Request.php +++ b/Request.php @@ -520,20 +520,26 @@ public function overrideGlobals(): void * * You should only list the reverse proxies that you manage directly. * - * @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 + * @param array $proxies A list of trusted proxies, the string 'REMOTE_ADDR' will be replaced with $_SERVER['REMOTE_ADDR'] and 'PRIVATE_SUBNETS' by IpUtils::PRIVATE_SUBNETS + * @param int-mask-of $trustedHeaderSet A bit field to set which headers to trust from your proxies */ public static function setTrustedProxies(array $proxies, int $trustedHeaderSet): void { - self::$trustedProxies = array_reduce($proxies, function ($proxies, $proxy) { - if ('REMOTE_ADDR' !== $proxy) { - $proxies[] = $proxy; - } elseif (isset($_SERVER['REMOTE_ADDR'])) { - $proxies[] = $_SERVER['REMOTE_ADDR']; + if (false !== $i = array_search('REMOTE_ADDR', $proxies, true)) { + if (isset($_SERVER['REMOTE_ADDR'])) { + $proxies[$i] = $_SERVER['REMOTE_ADDR']; + } else { + unset($proxies[$i]); + $proxies = array_values($proxies); } + } + + if (false !== ($i = array_search('PRIVATE_SUBNETS', $proxies, true)) || false !== ($i = array_search('private_ranges', $proxies, true))) { + unset($proxies[$i]); + $proxies = array_merge($proxies, IpUtils::PRIVATE_SUBNETS); + } - return $proxies; - }, []); + self::$trustedProxies = $proxies; self::$trustedHeaderSet = $trustedHeaderSet; } diff --git a/Tests/RequestTest.php b/Tests/RequestTest.php index 6f3543322..e23a88dde 100644 --- a/Tests/RequestTest.php +++ b/Tests/RequestTest.php @@ -16,6 +16,7 @@ use Symfony\Component\HttpFoundation\Exception\JsonException; use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException; use Symfony\Component\HttpFoundation\InputBag; +use Symfony\Component\HttpFoundation\IpUtils; use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Session\Session; @@ -2564,6 +2565,26 @@ public function testTrustedProxiesRemoteAddr($serverRemoteAddr, $trustedProxies, $this->assertSame($result, Request::getTrustedProxies()); } + public static function trustedProxiesRemoteAddr() + { + return [ + ['1.1.1.1', ['REMOTE_ADDR'], ['1.1.1.1']], + ['1.1.1.1', ['REMOTE_ADDR', '2.2.2.2'], ['1.1.1.1', '2.2.2.2']], + [null, ['REMOTE_ADDR'], []], + [null, ['REMOTE_ADDR', '2.2.2.2'], ['2.2.2.2']], + ]; + } + + /** + * @testWith ["PRIVATE_SUBNETS"] + * ["private_ranges"] + */ + public function testTrustedProxiesPrivateSubnets(string $key) + { + Request::setTrustedProxies([$key], Request::HEADER_X_FORWARDED_FOR); + $this->assertSame(IpUtils::PRIVATE_SUBNETS, Request::getTrustedProxies()); + } + public function testTrustedValuesCache() { $request = Request::create('http://example.com/'); @@ -2581,16 +2602,6 @@ public function testTrustedValuesCache() $this->assertFalse($request->isSecure()); } - public static function trustedProxiesRemoteAddr() - { - return [ - ['1.1.1.1', ['REMOTE_ADDR'], ['1.1.1.1']], - ['1.1.1.1', ['REMOTE_ADDR', '2.2.2.2'], ['1.1.1.1', '2.2.2.2']], - [null, ['REMOTE_ADDR'], []], - [null, ['REMOTE_ADDR', '2.2.2.2'], ['2.2.2.2']], - ]; - } - /** * @dataProvider preferSafeContentData */ From a9d64431e0518e0139e197bceeafe3ac0f5a0209 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Tue, 3 Sep 2024 15:19:19 +0200 Subject: [PATCH 16/44] stop using TestCase::iniSet() --- Tests/UriSignerTest.php | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/Tests/UriSignerTest.php b/Tests/UriSignerTest.php index e39c0a292..949e34760 100644 --- a/Tests/UriSignerTest.php +++ b/Tests/UriSignerTest.php @@ -64,20 +64,25 @@ public function testCheck() 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'))); - - $this->assertSame( - 'http://example.com/foo?_expiration=2145916800&_hash=xLhnPMzV3KqqHaaUffBUJvtRDAZ4%2FZ9Y8Sw%2BgmS%2B82Q%3D&baz=bay&foo=bar', - $signer->sign('http://example.com/foo?foo=bar&baz=bay', new \DateTimeImmutable('2038-01-01 00:00:00', new \DateTimeZone('UTC'))) - ); - $this->assertTrue($signer->check($signer->sign('http://example.com/foo?foo=bar&baz=bay', new \DateTimeImmutable('2099-01-01 00:00:00')))); + $oldArgSeparatorOutputValue = ini_set('arg_separator.output', '&'); + + try { + $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'))); + + $this->assertSame( + 'http://example.com/foo?_expiration=2145916800&_hash=xLhnPMzV3KqqHaaUffBUJvtRDAZ4%2FZ9Y8Sw%2BgmS%2B82Q%3D&baz=bay&foo=bar', + $signer->sign('http://example.com/foo?foo=bar&baz=bay', new \DateTimeImmutable('2038-01-01 00:00:00', new \DateTimeZone('UTC'))) + ); + $this->assertTrue($signer->check($signer->sign('http://example.com/foo?foo=bar&baz=bay', new \DateTimeImmutable('2099-01-01 00:00:00')))); + } finally { + ini_set('arg_separator.output', $oldArgSeparatorOutputValue); + } } public function testCheckWithRequest() From fe6d48291699a78df5346c72095dec94f60ca682 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Thu, 5 Sep 2024 08:55:30 +0200 Subject: [PATCH 17/44] no longer use the internal TestFailure class --- .../RequestAttributeValueSameTest.php | 12 +++------- .../ResponseCookieValueSameTest.php | 12 +++------- .../Constraint/ResponseFormatSameTest.php | 23 +++++-------------- .../Test/Constraint/ResponseHasCookieTest.php | 12 +++------- .../Test/Constraint/ResponseHasHeaderTest.php | 12 +++------- .../Constraint/ResponseHeaderSameTest.php | 12 +++------- .../Constraint/ResponseIsRedirectedTest.php | 19 ++++----------- .../Constraint/ResponseIsSuccessfulTest.php | 19 ++++----------- .../ResponseIsUnprocessableTest.php | 19 ++++----------- .../Constraint/ResponseStatusCodeSameTest.php | 18 ++++----------- 10 files changed, 41 insertions(+), 117 deletions(-) diff --git a/Tests/Test/Constraint/RequestAttributeValueSameTest.php b/Tests/Test/Constraint/RequestAttributeValueSameTest.php index c7ee239f3..27dbd895e 100644 --- a/Tests/Test/Constraint/RequestAttributeValueSameTest.php +++ b/Tests/Test/Constraint/RequestAttributeValueSameTest.php @@ -13,7 +13,6 @@ use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\TestFailure; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Test\Constraint\RequestAttributeValueSame; @@ -28,14 +27,9 @@ public function testConstraint() $constraint = new RequestAttributeValueSame('bar', 'foo'); $this->assertFalse($constraint->evaluate($request, '', true)); - try { - $constraint->evaluate($request); - } catch (ExpectationFailedException $e) { - $this->assertEquals("Failed asserting that the Request has attribute \"bar\" with value \"foo\".\n", TestFailure::exceptionToString($e)); + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage('Failed asserting that the Request has attribute "bar" with value "foo".'); - return; - } - - $this->fail(); + $constraint->evaluate($request); } } diff --git a/Tests/Test/Constraint/ResponseCookieValueSameTest.php b/Tests/Test/Constraint/ResponseCookieValueSameTest.php index 1b68b20bd..e3ad61000 100644 --- a/Tests/Test/Constraint/ResponseCookieValueSameTest.php +++ b/Tests/Test/Constraint/ResponseCookieValueSameTest.php @@ -13,7 +13,6 @@ use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\TestFailure; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Test\Constraint\ResponseCookieValueSame; @@ -31,15 +30,10 @@ public function testConstraint() $constraint = new ResponseCookieValueSame('foo', 'babar', '/path'); $this->assertFalse($constraint->evaluate($response, '', true)); - try { - $constraint->evaluate($response); - } catch (ExpectationFailedException $e) { - $this->assertEquals("Failed asserting that the Response has cookie \"foo\" with path \"/path\" with value \"babar\".\n", TestFailure::exceptionToString($e)); + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage('Failed asserting that the Response has cookie "foo" with path "/path" with value "babar".'); - return; - } - - $this->fail(); + $constraint->evaluate($response); } public function testCookieWithNullValueIsComparedAsEmptyString() diff --git a/Tests/Test/Constraint/ResponseFormatSameTest.php b/Tests/Test/Constraint/ResponseFormatSameTest.php index aed9285f2..9ac6a1cf5 100644 --- a/Tests/Test/Constraint/ResponseFormatSameTest.php +++ b/Tests/Test/Constraint/ResponseFormatSameTest.php @@ -13,7 +13,6 @@ use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\TestFailure; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Test\Constraint\ResponseFormatSame; @@ -32,15 +31,10 @@ public function testConstraint() $this->assertTrue($constraint->evaluate(new Response('', 200, ['Content-Type' => 'application/vnd.myformat']), '', true)); $this->assertFalse($constraint->evaluate(new Response(), '', true)); - try { - $constraint->evaluate(new Response('', 200, ['Content-Type' => 'application/ld+json'])); - } catch (ExpectationFailedException $e) { - $this->assertStringContainsString("Failed asserting that the Response format is custom.\nHTTP/1.0 200 OK", TestFailure::exceptionToString($e)); + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage("Failed asserting that the Response format is custom.\nHTTP/1.0 200 OK"); - return; - } - - $this->fail(); + $constraint->evaluate(new Response('', 200, ['Content-Type' => 'application/ld+json'])); } public function testNullFormat() @@ -48,14 +42,9 @@ public function testNullFormat() $constraint = new ResponseFormatSame(new Request(), null); $this->assertTrue($constraint->evaluate(new Response(), '', true)); - try { - $constraint->evaluate(new Response('', 200, ['Content-Type' => 'application/ld+json'])); - } catch (ExpectationFailedException $e) { - $this->assertStringContainsString("Failed asserting that the Response format is null.\nHTTP/1.0 200 OK", TestFailure::exceptionToString($e)); - - return; - } + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage("Failed asserting that the Response format is null.\nHTTP/1.0 200 OK"); - $this->fail(); + $constraint->evaluate(new Response('', 200, ['Content-Type' => 'application/ld+json'])); } } diff --git a/Tests/Test/Constraint/ResponseHasCookieTest.php b/Tests/Test/Constraint/ResponseHasCookieTest.php index ba1d7f38a..380502aba 100644 --- a/Tests/Test/Constraint/ResponseHasCookieTest.php +++ b/Tests/Test/Constraint/ResponseHasCookieTest.php @@ -13,7 +13,6 @@ use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\TestFailure; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Test\Constraint\ResponseHasCookie; @@ -29,14 +28,9 @@ public function testConstraint() $constraint = new ResponseHasCookie('bar'); $this->assertFalse($constraint->evaluate($response, '', true)); - try { - $constraint->evaluate($response); - } catch (ExpectationFailedException $e) { - $this->assertEquals("Failed asserting that the Response has cookie \"bar\".\n", TestFailure::exceptionToString($e)); + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage('Failed asserting that the Response has cookie "bar".'); - return; - } - - $this->fail(); + $constraint->evaluate($response); } } diff --git a/Tests/Test/Constraint/ResponseHasHeaderTest.php b/Tests/Test/Constraint/ResponseHasHeaderTest.php index 9a8fc2560..5959451eb 100644 --- a/Tests/Test/Constraint/ResponseHasHeaderTest.php +++ b/Tests/Test/Constraint/ResponseHasHeaderTest.php @@ -13,7 +13,6 @@ use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\TestFailure; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Test\Constraint\ResponseHasHeader; @@ -26,14 +25,9 @@ public function testConstraint() $constraint = new ResponseHasHeader('X-Date'); $this->assertFalse($constraint->evaluate(new Response(), '', true)); - try { - $constraint->evaluate(new Response()); - } catch (ExpectationFailedException $e) { - $this->assertEquals("Failed asserting that the Response has header \"X-Date\".\n", TestFailure::exceptionToString($e)); + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage('Failed asserting that the Response has header "X-Date".'); - return; - } - - $this->fail(); + $constraint->evaluate(new Response()); } } diff --git a/Tests/Test/Constraint/ResponseHeaderSameTest.php b/Tests/Test/Constraint/ResponseHeaderSameTest.php index 17a3f2a99..7c8b4a80b 100644 --- a/Tests/Test/Constraint/ResponseHeaderSameTest.php +++ b/Tests/Test/Constraint/ResponseHeaderSameTest.php @@ -13,7 +13,6 @@ use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\TestFailure; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Test\Constraint\ResponseHeaderSame; @@ -26,14 +25,9 @@ public function testConstraint() $constraint = new ResponseHeaderSame('Cache-Control', 'public'); $this->assertFalse($constraint->evaluate(new Response(), '', true)); - try { - $constraint->evaluate(new Response()); - } catch (ExpectationFailedException $e) { - $this->assertEquals("Failed asserting that the Response has header \"Cache-Control\" with value \"public\".\n", TestFailure::exceptionToString($e)); + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage('Failed asserting that the Response has header "Cache-Control" with value "public".'); - return; - } - - $this->fail(); + $constraint->evaluate(new Response()); } } diff --git a/Tests/Test/Constraint/ResponseIsRedirectedTest.php b/Tests/Test/Constraint/ResponseIsRedirectedTest.php index c4df3bc93..7e011ca10 100644 --- a/Tests/Test/Constraint/ResponseIsRedirectedTest.php +++ b/Tests/Test/Constraint/ResponseIsRedirectedTest.php @@ -13,7 +13,6 @@ use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\TestFailure; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Test\Constraint\ResponseIsRedirected; @@ -26,17 +25,10 @@ public function testConstraint() $this->assertTrue($constraint->evaluate(new Response('', 301), '', true)); $this->assertFalse($constraint->evaluate(new Response(), '', true)); - try { - $constraint->evaluate(new Response('Body content')); - } catch (ExpectationFailedException $e) { - $exceptionMessage = TestFailure::exceptionToString($e); - $this->assertStringContainsString("Failed asserting that the Response is redirected.\nHTTP/1.0 200 OK", $exceptionMessage); - $this->assertStringContainsString('Body content', $exceptionMessage); + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessageMatches('/Failed asserting that the Response is redirected.\nHTTP\/1.0 200 OK.+Body content/s'); - return; - } - - $this->fail(); + $constraint->evaluate(new Response('Body content')); } public function testReducedVerbosity() @@ -45,9 +37,8 @@ public function testReducedVerbosity() try { $constraint->evaluate(new Response('Body content')); } catch (ExpectationFailedException $e) { - $exceptionMessage = TestFailure::exceptionToString($e); - $this->assertStringContainsString("Failed asserting that the Response is redirected.\nHTTP/1.0 200 OK", $exceptionMessage); - $this->assertStringNotContainsString('Body content', $exceptionMessage); + $this->assertStringContainsString("Failed asserting that the Response is redirected.\nHTTP/1.0 200 OK", $e->getMessage()); + $this->assertStringNotContainsString('Body content', $e->getMessage()); return; } diff --git a/Tests/Test/Constraint/ResponseIsSuccessfulTest.php b/Tests/Test/Constraint/ResponseIsSuccessfulTest.php index 9ecafae7e..18e1c5e30 100644 --- a/Tests/Test/Constraint/ResponseIsSuccessfulTest.php +++ b/Tests/Test/Constraint/ResponseIsSuccessfulTest.php @@ -13,7 +13,6 @@ use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\TestFailure; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Test\Constraint\ResponseIsSuccessful; @@ -26,17 +25,10 @@ public function testConstraint() $this->assertTrue($constraint->evaluate(new Response(), '', true)); $this->assertFalse($constraint->evaluate(new Response('', 404), '', true)); - try { - $constraint->evaluate(new Response('Response body', 404)); - } catch (ExpectationFailedException $e) { - $exceptionMessage = TestFailure::exceptionToString($e); - $this->assertStringContainsString("Failed asserting that the Response is successful.\nHTTP/1.0 404 Not Found", $exceptionMessage); - $this->assertStringContainsString('Response body', $exceptionMessage); + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessageMatches('/Failed asserting that the Response is successful.\nHTTP\/1.0 404 Not Found.+Response body/s'); - return; - } - - $this->fail(); + $constraint->evaluate(new Response('Response body', 404)); } public function testReducedVerbosity() @@ -46,9 +38,8 @@ public function testReducedVerbosity() try { $constraint->evaluate(new Response('Response body', 404)); } catch (ExpectationFailedException $e) { - $exceptionMessage = TestFailure::exceptionToString($e); - $this->assertStringContainsString("Failed asserting that the Response is successful.\nHTTP/1.0 404 Not Found", $exceptionMessage); - $this->assertStringNotContainsString('Response body', $exceptionMessage); + $this->assertStringContainsString("Failed asserting that the Response is successful.\nHTTP/1.0 404 Not Found", $e->getMessage()); + $this->assertStringNotContainsString('Response body', $e->getMessage()); return; } diff --git a/Tests/Test/Constraint/ResponseIsUnprocessableTest.php b/Tests/Test/Constraint/ResponseIsUnprocessableTest.php index a142ec4b0..38454c1d0 100644 --- a/Tests/Test/Constraint/ResponseIsUnprocessableTest.php +++ b/Tests/Test/Constraint/ResponseIsUnprocessableTest.php @@ -13,7 +13,6 @@ use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\TestFailure; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Test\Constraint\ResponseIsUnprocessable; @@ -26,17 +25,10 @@ public function testConstraint() $this->assertTrue($constraint->evaluate(new Response('', 422), '', true)); $this->assertFalse($constraint->evaluate(new Response(), '', true)); - try { - $constraint->evaluate(new Response('Response body')); - } catch (ExpectationFailedException $e) { - $exceptionMessage = TestFailure::exceptionToString($e); - $this->assertStringContainsString("Failed asserting that the Response is unprocessable.\nHTTP/1.0 200 OK", $exceptionMessage); - $this->assertStringContainsString('Response body', $exceptionMessage); + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessageMatches('/Failed asserting that the Response is unprocessable.\nHTTP\/1.0 200 OK.+Response body/s'); - return; - } - - $this->fail(); + $constraint->evaluate(new Response('Response body')); } public function testReducedVerbosity() @@ -46,9 +38,8 @@ public function testReducedVerbosity() try { $constraint->evaluate(new Response('Response body')); } catch (ExpectationFailedException $e) { - $exceptionMessage = TestFailure::exceptionToString($e); - $this->assertStringContainsString("Failed asserting that the Response is unprocessable.\nHTTP/1.0 200 OK", $exceptionMessage); - $this->assertStringNotContainsString('Response body', $exceptionMessage); + $this->assertStringContainsString("Failed asserting that the Response is unprocessable.\nHTTP/1.0 200 OK", $e->getMessage()); + $this->assertStringNotContainsString('Response body', $e->getMessage()); return; } diff --git a/Tests/Test/Constraint/ResponseStatusCodeSameTest.php b/Tests/Test/Constraint/ResponseStatusCodeSameTest.php index df840f1a8..f4e216018 100644 --- a/Tests/Test/Constraint/ResponseStatusCodeSameTest.php +++ b/Tests/Test/Constraint/ResponseStatusCodeSameTest.php @@ -13,7 +13,6 @@ use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\TestFailure; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Test\Constraint\ResponseStatusCodeSame; @@ -28,17 +27,11 @@ public function testConstraint() $this->assertTrue($constraint->evaluate(new Response('', 404), '', true)); $constraint = new ResponseStatusCodeSame(200); - try { - $constraint->evaluate(new Response('Response body', 404)); - } catch (ExpectationFailedException $e) { - $exceptionMessage = TestFailure::exceptionToString($e); - $this->assertStringContainsString("Failed asserting that the Response status code is 200.\nHTTP/1.0 404 Not Found", TestFailure::exceptionToString($e)); - $this->assertStringContainsString('Response body', $exceptionMessage); - return; - } + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessageMatches('/Failed asserting that the Response status code is 200.\nHTTP\/1.0 404 Not Found.+Response body/s'); - $this->fail(); + $constraint->evaluate(new Response('Response body', 404)); } public function testReducedVerbosity() @@ -48,9 +41,8 @@ public function testReducedVerbosity() try { $constraint->evaluate(new Response('Response body', 404)); } catch (ExpectationFailedException $e) { - $exceptionMessage = TestFailure::exceptionToString($e); - $this->assertStringContainsString("Failed asserting that the Response status code is 200.\nHTTP/1.0 404 Not Found", TestFailure::exceptionToString($e)); - $this->assertStringNotContainsString('Response body', $exceptionMessage); + $this->assertStringContainsString("Failed asserting that the Response status code is 200.\nHTTP/1.0 404 Not Found", $e->getMessage()); + $this->assertStringNotContainsString('Response body', $e->getMessage()); return; } From 393f43f33e75540dacf8d73d529214f604ff830f Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Thu, 12 Sep 2024 19:32:01 +0200 Subject: [PATCH 18/44] [HttpFoundation] Deprecate passing `referer_check`, `use_only_cookies`, `use_trans_sid`, `trans_sid_hosts` and `trans_sid_tags` options to `NativeSessionStorage` --- CHANGELOG.md | 1 + Session/Storage/NativeSessionStorage.php | 14 ++++++---- .../Storage/Handler/Fixtures/common.inc | 1 - .../Storage/NativeSessionStorageTest.php | 27 +++++++++++++++++++ composer.json | 1 + 5 files changed, 38 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3814fddd..6fe867ddf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG * Add optional `$requests` parameter to `RequestStack::__construct()` * Add optional `$v4Bytes` and `$v6Bytes` parameters to `IpUtils::anonymize()` * Add `PRIVATE_SUBNETS` as a shortcut for private IP address ranges to `Request::setTrustedProxies()` + * Deprecate passing `referer_check`, `use_only_cookies`, `use_trans_sid`, `trans_sid_hosts` and `trans_sid_tags` options to `NativeSessionStorage` 7.1 --- diff --git a/Session/Storage/NativeSessionStorage.php b/Session/Storage/NativeSessionStorage.php index 239064214..0794fd269 100644 --- a/Session/Storage/NativeSessionStorage.php +++ b/Session/Storage/NativeSessionStorage.php @@ -62,16 +62,16 @@ class NativeSessionStorage implements SessionStorageInterface * gc_probability, "1" * lazy_write, "1" * name, "PHPSESSID" - * referer_check, "" + * referer_check, "" (deprecated since Symfony 7.2, to be removed in Symfony 8.0) * serialize_handler, "php" * use_strict_mode, "1" * use_cookies, "1" - * use_only_cookies, "1" - * use_trans_sid, "0" + * use_only_cookies, "1" (deprecated since Symfony 7.2, to be removed in Symfony 8.0) + * use_trans_sid, "0" (deprecated since Symfony 7.2, to be removed in Symfony 8.0) * sid_length, "32" * sid_bits_per_character, "5" - * trans_sid_hosts, $_SERVER['HTTP_HOST'] - * trans_sid_tags, "a=href,area=href,frame=src,form=" + * trans_sid_hosts, $_SERVER['HTTP_HOST'] (deprecated since Symfony 7.2, to be removed in Symfony 8.0) + * trans_sid_tags, "a=href,area=href,frame=src,form=" (deprecated since Symfony 7.2, to be removed in Symfony 8.0) */ public function __construct(array $options = [], AbstractProxy|\SessionHandlerInterface|null $handler = null, ?MetadataBag $metaBag = null) { @@ -328,6 +328,10 @@ public function setOptions(array $options): void ]); foreach ($options as $key => $value) { + if (\in_array($key, ['referer_check', 'use_only_cookies', 'use_trans_sid', 'trans_sid_hosts', 'trans_sid_tags'], true)) { + trigger_deprecation('symfony/http-foundation', '7.2', 'NativeSessionStorage\'s "%s" option is deprecated and will be ignored in Symfony 8.0.', $key); + } + if (isset($validOptions[$key])) { if ('cookie_secure' === $key && 'auto' === $value) { continue; diff --git a/Tests/Session/Storage/Handler/Fixtures/common.inc b/Tests/Session/Storage/Handler/Fixtures/common.inc index 5f48d42cb..7aaedf7f8 100644 --- a/Tests/Session/Storage/Handler/Fixtures/common.inc +++ b/Tests/Session/Storage/Handler/Fixtures/common.inc @@ -28,7 +28,6 @@ ini_set('session.cookie_domain', ''); ini_set('session.cookie_secure', ''); ini_set('session.cookie_httponly', ''); ini_set('session.use_cookies', 1); -ini_set('session.use_only_cookies', 1); ini_set('session.cache_expire', 180); ini_set('session.cookie_path', '/'); ini_set('session.cookie_domain', ''); diff --git a/Tests/Session/Storage/NativeSessionStorageTest.php b/Tests/Session/Storage/NativeSessionStorageTest.php index a0d54deb7..a7189a37b 100644 --- a/Tests/Session/Storage/NativeSessionStorageTest.php +++ b/Tests/Session/Storage/NativeSessionStorageTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpFoundation\Tests\Session\Storage; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag; use Symfony\Component\HttpFoundation\Session\Flash\FlashBag; use Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeFileSessionHandler; @@ -32,6 +33,8 @@ */ class NativeSessionStorageTest extends TestCase { + use ExpectDeprecationTrait; + private string $savePath; private $initialSessionSaveHandler; @@ -215,10 +218,14 @@ public function testCacheExpireOption() } /** + * @group legacy + * * 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() { + $this->expectDeprecation('Since symfony/http-foundation 7.2: NativeSessionStorage\'s "trans_sid_tags" option is deprecated and will be ignored in Symfony 8.0.'); + $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; @@ -357,4 +364,24 @@ public function testSaveHandlesNullSessionGracefully() $this->addToAssertionCount(1); } + + /** + * @group legacy + */ + public function testPassingDeprecatedOptions() + { + $this->expectDeprecation('Since symfony/http-foundation 7.2: NativeSessionStorage\'s "referer_check" option is deprecated and will be ignored in Symfony 8.0.'); + $this->expectDeprecation('Since symfony/http-foundation 7.2: NativeSessionStorage\'s "use_only_cookies" option is deprecated and will be ignored in Symfony 8.0.'); + $this->expectDeprecation('Since symfony/http-foundation 7.2: NativeSessionStorage\'s "use_trans_sid" option is deprecated and will be ignored in Symfony 8.0.'); + $this->expectDeprecation('Since symfony/http-foundation 7.2: NativeSessionStorage\'s "trans_sid_hosts" option is deprecated and will be ignored in Symfony 8.0.'); + $this->expectDeprecation('Since symfony/http-foundation 7.2: NativeSessionStorage\'s "trans_sid_tags" option is deprecated and will be ignored in Symfony 8.0.'); + + $this->getStorage([ + 'referer_check' => 'foo', + 'use_only_cookies' => 'foo', + 'use_trans_sid' => 'foo', + 'trans_sid_hosts' => 'foo', + 'trans_sid_tags' => 'foo', + ]); + } } diff --git a/composer.json b/composer.json index 6e88fc15b..45c13cc56 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ ], "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", "symfony/polyfill-mbstring": "~1.1", "symfony/polyfill-php83": "^1.27" }, From 54fec5006bb6c5aa81ab7b0095abf55cdce0a787 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Tue, 23 Jul 2024 09:29:37 +0200 Subject: [PATCH 19/44] [FrameworkBundle] Deprecate `session.sid_length` and `session.sid_bits_per_character` config options --- CHANGELOG.md | 2 +- Session/Storage/NativeSessionStorage.php | 12 +++++++----- Tests/Session/Storage/NativeSessionStorageTest.php | 4 ++++ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fe867ddf..6616aa0ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ CHANGELOG * Add optional `$requests` parameter to `RequestStack::__construct()` * Add optional `$v4Bytes` and `$v6Bytes` parameters to `IpUtils::anonymize()` * Add `PRIVATE_SUBNETS` as a shortcut for private IP address ranges to `Request::setTrustedProxies()` - * Deprecate passing `referer_check`, `use_only_cookies`, `use_trans_sid`, `trans_sid_hosts` and `trans_sid_tags` options to `NativeSessionStorage` + * Deprecate passing `referer_check`, `use_only_cookies`, `use_trans_sid`, `trans_sid_hosts`, `trans_sid_tags`, `sid_bits_per_character` and `sid_length` options to `NativeSessionStorage` 7.1 --- diff --git a/Session/Storage/NativeSessionStorage.php b/Session/Storage/NativeSessionStorage.php index 0794fd269..3d08f5f6d 100644 --- a/Session/Storage/NativeSessionStorage.php +++ b/Session/Storage/NativeSessionStorage.php @@ -68,8 +68,8 @@ class NativeSessionStorage implements SessionStorageInterface * use_cookies, "1" * use_only_cookies, "1" (deprecated since Symfony 7.2, to be removed in Symfony 8.0) * use_trans_sid, "0" (deprecated since Symfony 7.2, to be removed in Symfony 8.0) - * sid_length, "32" - * sid_bits_per_character, "5" + * sid_length, "32" (@deprecated since Symfony 7.2, to be removed in 8.0) + * sid_bits_per_character, "5" (@deprecated since Symfony 7.2, to be removed in 8.0) * trans_sid_hosts, $_SERVER['HTTP_HOST'] (deprecated since Symfony 7.2, to be removed in Symfony 8.0) * trans_sid_tags, "a=href,area=href,frame=src,form=" (deprecated since Symfony 7.2, to be removed in Symfony 8.0) */ @@ -126,8 +126,8 @@ public function start(): bool * See https://www.php.net/manual/en/session.configuration.php#ini.session.sid-bits-per-character. * Allowed values are integers such as: * - 4 for range `a-f0-9` - * - 5 for range `a-v0-9` - * - 6 for range `a-zA-Z0-9,-` + * - 5 for range `a-v0-9` (@deprecated since Symfony 7.2, it will default to 4 and the option will be ignored in Symfony 8.0) + * - 6 for range `a-zA-Z0-9,-` (@deprecated since Symfony 7.2, it will default to 4 and the option will be ignored in Symfony 8.0) * * ---------- Part 2 * @@ -139,6 +139,8 @@ public function start(): bool * - The length of Windows and Linux filenames is limited to 255 bytes. Then the max must not exceed 255. * - The session filename prefix is `sess_`, a 5 bytes string. Then the max must not exceed 255 - 5 = 250. * + * This is @deprecated since Symfony 7.2, the sid length will default to 32 and the option will be ignored in Symfony 8.0. + * * ---------- Conclusion * * The parts 1 and 2 prevent the warning below: @@ -328,7 +330,7 @@ public function setOptions(array $options): void ]); foreach ($options as $key => $value) { - if (\in_array($key, ['referer_check', 'use_only_cookies', 'use_trans_sid', 'trans_sid_hosts', 'trans_sid_tags'], true)) { + if (\in_array($key, ['referer_check', 'use_only_cookies', 'use_trans_sid', 'trans_sid_hosts', 'trans_sid_tags', 'sid_length', 'sid_bits_per_character'], true)) { trigger_deprecation('symfony/http-foundation', '7.2', 'NativeSessionStorage\'s "%s" option is deprecated and will be ignored in Symfony 8.0.', $key); } diff --git a/Tests/Session/Storage/NativeSessionStorageTest.php b/Tests/Session/Storage/NativeSessionStorageTest.php index a7189a37b..d21ca888c 100644 --- a/Tests/Session/Storage/NativeSessionStorageTest.php +++ b/Tests/Session/Storage/NativeSessionStorageTest.php @@ -370,6 +370,8 @@ public function testSaveHandlesNullSessionGracefully() */ public function testPassingDeprecatedOptions() { + $this->expectDeprecation('Since symfony/http-foundation 7.2: NativeSessionStorage\'s "sid_length" option is deprecated and will be ignored in Symfony 8.0.'); + $this->expectDeprecation('Since symfony/http-foundation 7.2: NativeSessionStorage\'s "sid_bits_per_character" option is deprecated and will be ignored in Symfony 8.0.'); $this->expectDeprecation('Since symfony/http-foundation 7.2: NativeSessionStorage\'s "referer_check" option is deprecated and will be ignored in Symfony 8.0.'); $this->expectDeprecation('Since symfony/http-foundation 7.2: NativeSessionStorage\'s "use_only_cookies" option is deprecated and will be ignored in Symfony 8.0.'); $this->expectDeprecation('Since symfony/http-foundation 7.2: NativeSessionStorage\'s "use_trans_sid" option is deprecated and will be ignored in Symfony 8.0.'); @@ -377,6 +379,8 @@ public function testPassingDeprecatedOptions() $this->expectDeprecation('Since symfony/http-foundation 7.2: NativeSessionStorage\'s "trans_sid_tags" option is deprecated and will be ignored in Symfony 8.0.'); $this->getStorage([ + 'sid_length' => 42, + 'sid_bits_per_character' => 6, 'referer_check' => 'foo', 'use_only_cookies' => 'foo', 'use_trans_sid' => 'foo', From d87b1218baa01fa0bdebfcfdbefa7f3f74fbbc1c Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Thu, 19 Sep 2024 09:56:35 +0200 Subject: [PATCH 20/44] Switch to ExpectUserDeprecationMessageTrait --- .../Storage/NativeSessionStorageTest.php | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Tests/Session/Storage/NativeSessionStorageTest.php b/Tests/Session/Storage/NativeSessionStorageTest.php index d21ca888c..11c489f6b 100644 --- a/Tests/Session/Storage/NativeSessionStorageTest.php +++ b/Tests/Session/Storage/NativeSessionStorageTest.php @@ -12,7 +12,7 @@ namespace Symfony\Component\HttpFoundation\Tests\Session\Storage; use PHPUnit\Framework\TestCase; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; +use Symfony\Bridge\PhpUnit\ExpectUserDeprecationMessageTrait; use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag; use Symfony\Component\HttpFoundation\Session\Flash\FlashBag; use Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeFileSessionHandler; @@ -33,7 +33,7 @@ */ class NativeSessionStorageTest extends TestCase { - use ExpectDeprecationTrait; + use ExpectUserDeprecationMessageTrait; private string $savePath; @@ -224,7 +224,7 @@ public function testCacheExpireOption() */ public function testTransSidTagsOption() { - $this->expectDeprecation('Since symfony/http-foundation 7.2: NativeSessionStorage\'s "trans_sid_tags" option is deprecated and will be ignored in Symfony 8.0.'); + $this->expectUserDeprecationMessage('Since symfony/http-foundation 7.2: NativeSessionStorage\'s "trans_sid_tags" option is deprecated and will be ignored in Symfony 8.0.'); $previousErrorHandler = set_error_handler(function ($errno, $errstr) use (&$previousErrorHandler) { if ('ini_set(): Usage of session.trans_sid_tags INI setting is deprecated' !== $errstr) { @@ -370,13 +370,13 @@ public function testSaveHandlesNullSessionGracefully() */ public function testPassingDeprecatedOptions() { - $this->expectDeprecation('Since symfony/http-foundation 7.2: NativeSessionStorage\'s "sid_length" option is deprecated and will be ignored in Symfony 8.0.'); - $this->expectDeprecation('Since symfony/http-foundation 7.2: NativeSessionStorage\'s "sid_bits_per_character" option is deprecated and will be ignored in Symfony 8.0.'); - $this->expectDeprecation('Since symfony/http-foundation 7.2: NativeSessionStorage\'s "referer_check" option is deprecated and will be ignored in Symfony 8.0.'); - $this->expectDeprecation('Since symfony/http-foundation 7.2: NativeSessionStorage\'s "use_only_cookies" option is deprecated and will be ignored in Symfony 8.0.'); - $this->expectDeprecation('Since symfony/http-foundation 7.2: NativeSessionStorage\'s "use_trans_sid" option is deprecated and will be ignored in Symfony 8.0.'); - $this->expectDeprecation('Since symfony/http-foundation 7.2: NativeSessionStorage\'s "trans_sid_hosts" option is deprecated and will be ignored in Symfony 8.0.'); - $this->expectDeprecation('Since symfony/http-foundation 7.2: NativeSessionStorage\'s "trans_sid_tags" option is deprecated and will be ignored in Symfony 8.0.'); + $this->expectUserDeprecationMessage('Since symfony/http-foundation 7.2: NativeSessionStorage\'s "sid_length" option is deprecated and will be ignored in Symfony 8.0.'); + $this->expectUserDeprecationMessage('Since symfony/http-foundation 7.2: NativeSessionStorage\'s "sid_bits_per_character" option is deprecated and will be ignored in Symfony 8.0.'); + $this->expectUserDeprecationMessage('Since symfony/http-foundation 7.2: NativeSessionStorage\'s "referer_check" option is deprecated and will be ignored in Symfony 8.0.'); + $this->expectUserDeprecationMessage('Since symfony/http-foundation 7.2: NativeSessionStorage\'s "use_only_cookies" option is deprecated and will be ignored in Symfony 8.0.'); + $this->expectUserDeprecationMessage('Since symfony/http-foundation 7.2: NativeSessionStorage\'s "use_trans_sid" option is deprecated and will be ignored in Symfony 8.0.'); + $this->expectUserDeprecationMessage('Since symfony/http-foundation 7.2: NativeSessionStorage\'s "trans_sid_hosts" option is deprecated and will be ignored in Symfony 8.0.'); + $this->expectUserDeprecationMessage('Since symfony/http-foundation 7.2: NativeSessionStorage\'s "trans_sid_tags" option is deprecated and will be ignored in Symfony 8.0.'); $this->getStorage([ 'sid_length' => 42, From 0c5ecc929852c99bf6b0dd118bfb375f526358c6 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Tue, 24 Sep 2024 12:58:43 +0200 Subject: [PATCH 21/44] Fix `$this` calls to static ones when relevant --- Request.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Request.php b/Request.php index 2af29a52a..114010609 100644 --- a/Request.php +++ b/Request.php @@ -1572,7 +1572,7 @@ public function getLanguages(): array $this->languages = []; foreach ($languages as $acceptHeaderItem) { $lang = $acceptHeaderItem->getValue(); - $this->languages[] = $this->formatLocale($lang); + $this->languages[] = self::formatLocale($lang); } $this->languages = array_unique($this->languages); From 1028e55086ea8d31d6323a6b7b8f0f12901fd95c Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Tue, 24 Sep 2024 13:28:07 +0200 Subject: [PATCH 22/44] Remove useless parent method calls in tests --- Tests/Session/Flash/AutoExpireFlashBagTest.php | 2 -- Tests/Session/Flash/FlashBagTest.php | 2 -- .../Storage/Handler/AbstractRedisSessionHandlerTestCase.php | 2 -- Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php | 2 -- Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php | 2 -- Tests/Session/Storage/Handler/PdoSessionHandlerTest.php | 1 - Tests/Session/Storage/MetadataBagTest.php | 1 - 7 files changed, 12 deletions(-) diff --git a/Tests/Session/Flash/AutoExpireFlashBagTest.php b/Tests/Session/Flash/AutoExpireFlashBagTest.php index 0b418c006..8e9ee780b 100644 --- a/Tests/Session/Flash/AutoExpireFlashBagTest.php +++ b/Tests/Session/Flash/AutoExpireFlashBagTest.php @@ -27,7 +27,6 @@ class AutoExpireFlashBagTest extends TestCase protected function setUp(): void { - parent::setUp(); $this->bag = new FlashBag(); $this->array = ['new' => ['notice' => ['A previous flash message']]]; $this->bag->initialize($this->array); @@ -36,7 +35,6 @@ protected function setUp(): void protected function tearDown(): void { unset($this->bag); - parent::tearDown(); } public function testInitialize() diff --git a/Tests/Session/Flash/FlashBagTest.php b/Tests/Session/Flash/FlashBagTest.php index 8163ba769..efe9a4977 100644 --- a/Tests/Session/Flash/FlashBagTest.php +++ b/Tests/Session/Flash/FlashBagTest.php @@ -27,7 +27,6 @@ class FlashBagTest extends TestCase protected function setUp(): void { - parent::setUp(); $this->bag = new FlashBag(); $this->array = ['notice' => ['A previous flash message']]; $this->bag->initialize($this->array); @@ -36,7 +35,6 @@ protected function setUp(): void protected function tearDown(): void { unset($this->bag); - parent::tearDown(); } public function testInitialize() diff --git a/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php b/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php index 4df1553c8..f6ab99514 100644 --- a/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php +++ b/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php @@ -31,8 +31,6 @@ abstract protected function createRedisClient(string $host): \Redis|Relay|\Redis protected function setUp(): void { - parent::setUp(); - if (!\extension_loaded('redis')) { self::markTestSkipped('Extension redis required.'); } diff --git a/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php b/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php index 379fcb0d1..0f460e47a 100644 --- a/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php @@ -30,8 +30,6 @@ class MemcachedSessionHandlerTest extends TestCase protected function setUp(): void { - parent::setUp(); - if (version_compare(phpversion('memcached'), '2.2.0', '>=') && version_compare(phpversion('memcached'), '3.0.0b1', '<')) { $this->markTestSkipped('Tests can only be run with memcached extension 2.1.0 or lower, or 3.0.0b1 or higher'); } diff --git a/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php b/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php index 908acfe15..63b0263c2 100644 --- a/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php @@ -44,8 +44,6 @@ class MongoDbSessionHandlerTest extends TestCase protected function setUp(): void { - parent::setUp(); - $this->manager = new Manager('mongodb://'.getenv('MONGODB_HOST')); try { diff --git a/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php b/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php index ede4703aa..8106f3da4 100644 --- a/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php @@ -30,7 +30,6 @@ protected function tearDown(): void if ($this->dbFile) { @unlink($this->dbFile); } - parent::tearDown(); } protected function getPersistentSqliteDsn() diff --git a/Tests/Session/Storage/MetadataBagTest.php b/Tests/Session/Storage/MetadataBagTest.php index b2f3de42b..3acdcfcac 100644 --- a/Tests/Session/Storage/MetadataBagTest.php +++ b/Tests/Session/Storage/MetadataBagTest.php @@ -26,7 +26,6 @@ class MetadataBagTest extends TestCase protected function setUp(): void { - parent::setUp(); $this->bag = new MetadataBag(); $this->array = [MetadataBag::CREATED => 1234567, MetadataBag::UPDATED => 12345678, MetadataBag::LIFETIME => 0]; $this->bag->initialize($this->array); From 53b79fbb27e5f847ab89b3f531a9d1440db2a49b Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Fri, 4 Oct 2024 13:58:10 +0200 Subject: [PATCH 23/44] Fix test extension requirements --- .../Storage/Handler/AbstractRedisSessionHandlerTestCase.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php b/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php index f6ab99514..0b4c86edf 100644 --- a/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php +++ b/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php @@ -16,8 +16,6 @@ use Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler; /** - * @requires extension redis - * * @group time-sensitive */ abstract class AbstractRedisSessionHandlerTestCase extends TestCase From 4c415d52695870fe56d78e6b9ab88bb8d342eaf4 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Mon, 14 Oct 2024 20:03:05 +0200 Subject: [PATCH 24/44] Reduce common control flows --- Tests/RequestMatcher/SchemeRequestMatcherTest.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Tests/RequestMatcher/SchemeRequestMatcherTest.php b/Tests/RequestMatcher/SchemeRequestMatcherTest.php index 6614bfcc2..933b3d695 100644 --- a/Tests/RequestMatcher/SchemeRequestMatcherTest.php +++ b/Tests/RequestMatcher/SchemeRequestMatcherTest.php @@ -25,18 +25,16 @@ public function test(string $requestScheme, array|string $matcherScheme, bool $i $httpRequest = Request::create(''); $httpsRequest = Request::create('', 'get', [], [], [], ['HTTPS' => 'on']); + $matcher = new SchemeRequestMatcher($matcherScheme); if ($isMatch) { if ('https' === $requestScheme) { - $matcher = new SchemeRequestMatcher($matcherScheme); $this->assertFalse($matcher->matches($httpRequest)); $this->assertTrue($matcher->matches($httpsRequest)); } else { - $matcher = new SchemeRequestMatcher($matcherScheme); $this->assertFalse($matcher->matches($httpsRequest)); $this->assertTrue($matcher->matches($httpRequest)); } } else { - $matcher = new SchemeRequestMatcher($matcherScheme); $this->assertFalse($matcher->matches($httpRequest)); $this->assertFalse($matcher->matches($httpsRequest)); } From 735b8519a8bcbf603d3af0430fb1be5c8822af13 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Fri, 18 Oct 2024 16:04:52 +0200 Subject: [PATCH 25/44] Remove always true/false occurrences --- Request.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Request.php b/Request.php index 114010609..e80bfa61f 100644 --- a/Request.php +++ b/Request.php @@ -1536,7 +1536,7 @@ public function getPreferredLanguage(?array $locales = null): ?string return $preferredLanguages[0] ?? null; } - $locales = array_map($this->formatLocale(...), $locales ?? []); + $locales = array_map($this->formatLocale(...), $locales); if (!$preferredLanguages) { return $locales[0]; } From 069924c165fb05f1d0860f21addff0f1d0778e44 Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Tue, 10 Dec 2024 10:44:11 +0100 Subject: [PATCH 26/44] [HttpFoundation] Support iterable of string in `StreamedResponse` --- CHANGELOG.md | 7 ++++++- StreamedResponse.php | 27 ++++++++++++++++++++++----- Tests/StreamedResponseTest.php | 11 +++++++++++ 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6616aa0ad..6861b3b36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.3 +--- + + * Add support for iterable of string in `StreamedResponse` + 7.2 --- @@ -40,7 +45,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 + * Make `MongoDbSessionHandler` instantiable with the mongodb extension directly 6.3 --- diff --git a/StreamedResponse.php b/StreamedResponse.php index 3acaade17..6eedf1c49 100644 --- a/StreamedResponse.php +++ b/StreamedResponse.php @@ -14,7 +14,7 @@ /** * StreamedResponse represents a streamed HTTP response. * - * A StreamedResponse uses a callback for its content. + * A StreamedResponse uses a callback or an iterable of strings for its content. * * The callback should use the standard PHP functions like echo * to stream the response back to the client. The flush() function @@ -32,19 +32,36 @@ class StreamedResponse extends Response private bool $headersSent = false; /** - * @param int $status The HTTP status code (200 "OK" by default) + * @param callable|iterable|null $callbackOrChunks + * @param int $status The HTTP status code (200 "OK" by default) */ - public function __construct(?callable $callback = null, int $status = 200, array $headers = []) + public function __construct(callable|iterable|null $callbackOrChunks = null, int $status = 200, array $headers = []) { parent::__construct(null, $status, $headers); - if (null !== $callback) { - $this->setCallback($callback); + if (\is_callable($callbackOrChunks)) { + $this->setCallback($callbackOrChunks); + } elseif ($callbackOrChunks) { + $this->setChunks($callbackOrChunks); } $this->streamed = false; $this->headersSent = false; } + /** + * @param iterable $chunks + */ + public function setChunks(iterable $chunks): static + { + $this->callback = static function () use ($chunks): void { + foreach ($chunks as $chunk) { + echo $chunk; + } + }; + + return $this; + } + /** * Sets the PHP callback associated with this Response. * diff --git a/Tests/StreamedResponseTest.php b/Tests/StreamedResponseTest.php index 2a2b7e731..78a777aea 100644 --- a/Tests/StreamedResponseTest.php +++ b/Tests/StreamedResponseTest.php @@ -25,6 +25,17 @@ public function testConstructor() $this->assertEquals('text/plain', $response->headers->get('Content-Type')); } + public function testConstructorWithChunks() + { + $chunks = ['foo', 'bar', 'baz']; + $callback = (new StreamedResponse($chunks))->getCallback(); + + ob_start(); + $callback(); + + $this->assertSame('foobarbaz', ob_get_clean()); + } + public function testPrepareWith11Protocol() { $response = new StreamedResponse(function () { echo 'foo'; }); From c2508e48b252f02b1364980ff79fb168a074e199 Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Wed, 11 Dec 2024 14:08:35 +0100 Subject: [PATCH 27/44] chore: PHP CS Fixer fixes --- ResponseHeaderBag.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ResponseHeaderBag.php b/ResponseHeaderBag.php index 023651efb..b2bdb500c 100644 --- a/ResponseHeaderBag.php +++ b/ResponseHeaderBag.php @@ -221,7 +221,7 @@ public function getCookies(string $format = self::COOKIES_FLAT): array */ public function clearCookie(string $name, ?string $path = '/', ?string $domain = null, bool $secure = false, bool $httpOnly = true, ?string $sameSite = null /* , bool $partitioned = false */): void { - $partitioned = 6 < \func_num_args() ? \func_get_arg(6) : false; + $partitioned = 6 < \func_num_args() ? func_get_arg(6) : false; $this->setCookie(new Cookie($name, null, 1, $path, $domain, $secure, $httpOnly, false, $sameSite, $partitioned)); } From 9e485dd3094b0bcf2d9cd9f9b822ef7ebc0e5dbc Mon Sep 17 00:00:00 2001 From: valtzu Date: Wed, 27 Nov 2024 22:28:12 +0200 Subject: [PATCH 28/44] Generate url-safe signatures --- Tests/UriSignerTest.php | 18 ++++++++++++------ UriSigner.php | 9 +++++---- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/Tests/UriSignerTest.php b/Tests/UriSignerTest.php index 949e34760..927e2bda8 100644 --- a/Tests/UriSignerTest.php +++ b/Tests/UriSignerTest.php @@ -70,13 +70,13 @@ public function testCheckWithDifferentArgSeparator() $signer = new UriSigner('foobar'); $this->assertSame( - 'http://example.com/foo?_hash=rIOcC%2FF3DoEGo%2FvnESjSp7uU9zA9S%2F%2BOLhxgMexoPUM%3D&baz=bay&foo=bar', + 'http://example.com/foo?_hash=rIOcC_F3DoEGo_vnESjSp7uU9zA9S_-OLhxgMexoPUM&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'))); $this->assertSame( - 'http://example.com/foo?_expiration=2145916800&_hash=xLhnPMzV3KqqHaaUffBUJvtRDAZ4%2FZ9Y8Sw%2BgmS%2B82Q%3D&baz=bay&foo=bar', + 'http://example.com/foo?_expiration=2145916800&_hash=xLhnPMzV3KqqHaaUffBUJvtRDAZ4_Z9Y8Sw-gmS-82Q&baz=bay&foo=bar', $signer->sign('http://example.com/foo?foo=bar&baz=bay', new \DateTimeImmutable('2038-01-01 00:00:00', new \DateTimeZone('UTC'))) ); $this->assertTrue($signer->check($signer->sign('http://example.com/foo?foo=bar&baz=bay', new \DateTimeImmutable('2099-01-01 00:00:00')))); @@ -103,13 +103,13 @@ public function testCheckWithDifferentParameter() $signer = new UriSigner('foobar', 'qux', 'abc'); $this->assertSame( - 'http://example.com/foo?baz=bay&foo=bar&qux=rIOcC%2FF3DoEGo%2FvnESjSp7uU9zA9S%2F%2BOLhxgMexoPUM%3D', + 'http://example.com/foo?baz=bay&foo=bar&qux=rIOcC_F3DoEGo_vnESjSp7uU9zA9S_-OLhxgMexoPUM', $signer->sign('http://example.com/foo?foo=bar&baz=bay') ); $this->assertTrue($signer->check($signer->sign('http://example.com/foo?foo=bar&baz=bay'))); $this->assertSame( - 'http://example.com/foo?abc=2145916800&baz=bay&foo=bar&qux=kE4rK2MzeiwrYAKy%2B%2FGKvKA6bnzqCbACBdpC3yGnPVU%3D', + 'http://example.com/foo?abc=2145916800&baz=bay&foo=bar&qux=kE4rK2MzeiwrYAKy-_GKvKA6bnzqCbACBdpC3yGnPVU', $signer->sign('http://example.com/foo?foo=bar&baz=bay', new \DateTimeImmutable('2038-01-01 00:00:00', new \DateTimeZone('UTC'))) ); $this->assertTrue($signer->check($signer->sign('http://example.com/foo?foo=bar&baz=bay', new \DateTimeImmutable('2099-01-01 00:00:00')))); @@ -120,14 +120,14 @@ public function testSignerWorksWithFragments() $signer = new UriSigner('foobar'); $this->assertSame( - 'http://example.com/foo?_hash=EhpAUyEobiM3QTrKxoLOtQq5IsWyWedoXDPqIjzNj5o%3D&bar=foo&foo=bar#foobar', + 'http://example.com/foo?_hash=EhpAUyEobiM3QTrKxoLOtQq5IsWyWedoXDPqIjzNj5o&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'))); $this->assertSame( - 'http://example.com/foo?_expiration=2145916800&_hash=jTdrIE9MJSorNpQmkX6tmOtocxXtHDzIJawcAW4IFYo%3D&bar=foo&foo=bar#foobar', + 'http://example.com/foo?_expiration=2145916800&_hash=jTdrIE9MJSorNpQmkX6tmOtocxXtHDzIJawcAW4IFYo&bar=foo&foo=bar#foobar', $signer->sign('http://example.com/foo?bar=foo&foo=bar#foobar', new \DateTimeImmutable('2038-01-01 00:00:00', new \DateTimeZone('UTC'))) ); @@ -198,4 +198,10 @@ public function testCheckWithUriExpiration() $this->assertFalse($signer->check($relativeUriFromNow2)); $this->assertFalse($signer->check($relativeUriFromNow3)); } + + public function testNonUrlSafeBase64() + { + $signer = new UriSigner('foobar'); + $this->assertTrue($signer->check('http://example.com/foo?_hash=rIOcC%2FF3DoEGo%2FvnESjSp7uU9zA9S%2F%2BOLhxgMexoPUM%3D&baz=bay&foo=bar')); + } } diff --git a/UriSigner.php b/UriSigner.php index dd7443489..1c9e25a5c 100644 --- a/UriSigner.php +++ b/UriSigner.php @@ -46,7 +46,7 @@ public function __construct( * * The expiration is added as a query string parameter. */ - public function sign(string $uri/*, \DateTimeInterface|\DateInterval|int|null $expiration = null*/): string + public function sign(string $uri/* , \DateTimeInterface|\DateInterval|int|null $expiration = null */): string { $expiration = null; @@ -55,7 +55,7 @@ public function sign(string $uri/*, \DateTimeInterface|\DateInterval|int|null $e } if (null !== $expiration && !$expiration instanceof \DateTimeInterface && !$expiration instanceof \DateInterval && !\is_int($expiration)) { - throw new \TypeError(\sprintf('The second argument of %s() must be an instance of %s or %s, an integer or null (%s given).', __METHOD__, \DateTimeInterface::class, \DateInterval::class, get_debug_type($expiration))); + throw new \TypeError(\sprintf('The second argument of "%s()" must be an instance of "%s" or "%s", an integer or null (%s given).', __METHOD__, \DateTimeInterface::class, \DateInterval::class, get_debug_type($expiration))); } $url = parse_url($uri); @@ -103,7 +103,8 @@ public function check(string $uri): bool $hash = $params[$this->hashParameter]; unset($params[$this->hashParameter]); - if (!hash_equals($this->computeHash($this->buildUrl($url, $params)), $hash)) { + // In 8.0, remove support for non-url-safe tokens + if (!hash_equals($this->computeHash($this->buildUrl($url, $params)), strtr(rtrim($hash, '='), ['/' => '_', '+' => '-']))) { return false; } @@ -124,7 +125,7 @@ public function checkRequest(Request $request): bool private function computeHash(string $uri): string { - return base64_encode(hash_hmac('sha256', $uri, $this->secret, true)); + return strtr(rtrim(base64_encode(hash_hmac('sha256', $uri, $this->secret, true)), '='), ['/' => '_', '+' => '-']); } private function buildUrl(array $url, array $params = []): string From 658b7a44304949f426c640531aaea00ec15ea7dc Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 27 Dec 2024 10:36:28 +0100 Subject: [PATCH 29/44] [HttpFoundation] Document thrown exception by parameter and input bag --- InputBag.php | 10 ++++++++++ ParameterBag.php | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/InputBag.php b/InputBag.php index 97bd8b090..7411d755c 100644 --- a/InputBag.php +++ b/InputBag.php @@ -25,6 +25,8 @@ final class InputBag extends ParameterBag * Returns a scalar input value by name. * * @param string|int|float|bool|null $default The default value if the input key does not exist + * + * @throws BadRequestException if the input contains a non-scalar value */ public function get(string $key, mixed $default = null): string|int|float|bool|null { @@ -85,6 +87,8 @@ public function set(string $key, mixed $value): void * @return ?T * * @psalm-return ($default is null ? T|null : T) + * + * @throws BadRequestException if the input cannot be converted to an enum */ public function getEnum(string $key, string $class, ?\BackedEnum $default = null): ?\BackedEnum { @@ -97,6 +101,8 @@ public function getEnum(string $key, string $class, ?\BackedEnum $default = null /** * Returns the parameter value converted to string. + * + * @throws BadRequestException if the input contains a non-scalar value */ public function getString(string $key, string $default = ''): string { @@ -104,6 +110,10 @@ public function getString(string $key, string $default = ''): string return (string) $this->get($key, $default); } + /** + * @throws BadRequestException if the input value is an array and \FILTER_REQUIRE_ARRAY or \FILTER_FORCE_ARRAY is not set + * @throws BadRequestException if the input value is invalid and \FILTER_NULL_ON_FAILURE is not set + */ 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 35a0f1819..f37d7b3e2 100644 --- a/ParameterBag.php +++ b/ParameterBag.php @@ -32,6 +32,8 @@ public function __construct( * Returns the parameters. * * @param string|null $key The name of the parameter to return or null to get them all + * + * @throws BadRequestException if the value is not an array */ public function all(?string $key = null): array { @@ -98,6 +100,8 @@ public function remove(string $key): void /** * Returns the alphabetic characters of the parameter value. + * + * @throws UnexpectedValueException if the value cannot be converted to string */ public function getAlpha(string $key, string $default = ''): string { @@ -106,6 +110,8 @@ public function getAlpha(string $key, string $default = ''): string /** * Returns the alphabetic characters and digits of the parameter value. + * + * @throws UnexpectedValueException if the value cannot be converted to string */ public function getAlnum(string $key, string $default = ''): string { @@ -114,6 +120,8 @@ public function getAlnum(string $key, string $default = ''): string /** * Returns the digits of the parameter value. + * + * @throws UnexpectedValueException if the value cannot be converted to string */ public function getDigits(string $key, string $default = ''): string { @@ -122,6 +130,8 @@ public function getDigits(string $key, string $default = ''): string /** * Returns the parameter as string. + * + * @throws UnexpectedValueException if the value cannot be converted to string */ public function getString(string $key, string $default = ''): string { @@ -135,6 +145,8 @@ public function getString(string $key, string $default = ''): string /** * Returns the parameter value converted to integer. + * + * @throws UnexpectedValueException if the value cannot be converted to integer */ public function getInt(string $key, int $default = 0): int { @@ -143,6 +155,8 @@ public function getInt(string $key, int $default = 0): int /** * Returns the parameter value converted to boolean. + * + * @throws UnexpectedValueException if the value cannot be converted to a boolean */ public function getBoolean(string $key, bool $default = false): bool { @@ -160,6 +174,8 @@ public function getBoolean(string $key, bool $default = false): bool * @return ?T * * @psalm-return ($default is null ? T|null : T) + * + * @throws UnexpectedValueException if the parameter value cannot be converted to an enum */ public function getEnum(string $key, string $class, ?\BackedEnum $default = null): ?\BackedEnum { @@ -183,6 +199,9 @@ public function getEnum(string $key, string $class, ?\BackedEnum $default = null * @param int|array{flags?: int, options?: array} $options Flags from FILTER_* constants * * @see https://php.net/filter-var + * + * @throws UnexpectedValueException if the parameter value is a non-stringable object + * @throws UnexpectedValueException if the parameter value is invalid and \FILTER_NULL_ON_FAILURE is not set */ public function filter(string $key, mixed $default = null, int $filter = \FILTER_DEFAULT, mixed $options = []): mixed { From a4d4dd6587151385196a67ab5b2438a227d598cb Mon Sep 17 00:00:00 2001 From: Emilien Escalle Date: Mon, 6 Jan 2025 16:43:34 +0100 Subject: [PATCH 30/44] chore(HttpFoundation): define phpdoc type for Response "statusTexts" As `Symfony\Component\HttpFoundation\Response::$statusTexts` is public, it can be used by everyone. Some of us can use type analysis like PSALM or PHPStan... It is always useful, and for some of them mandatory to have a proper array typing to use this variable. --- Response.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Response.php b/Response.php index bf68d2741..638b5bf60 100644 --- a/Response.php +++ b/Response.php @@ -121,6 +121,8 @@ class Response * (last updated 2021-10-01). * * Unless otherwise noted, the status code is defined in RFC2616. + * + * @var array */ public static array $statusTexts = [ 100 => 'Continue', From 5c227d698654539a16ef2d149bbe8725c644327d Mon Sep 17 00:00:00 2001 From: Yonel Ceruto Date: Fri, 1 Nov 2024 15:10:26 -0400 Subject: [PATCH 31/44] Streamlining server event streaming --- CHANGELOG.md | 1 + EventStreamResponse.php | 110 ++++++++++++++++++++++ ServerEvent.php | 147 ++++++++++++++++++++++++++++++ Tests/EventStreamResponseTest.php | 127 ++++++++++++++++++++++++++ 4 files changed, 385 insertions(+) create mode 100644 EventStreamResponse.php create mode 100644 ServerEvent.php create mode 100644 Tests/EventStreamResponseTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 6861b3b36..0841fa9ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Add support for iterable of string in `StreamedResponse` + * Add `EventStreamResponse` and `ServerEvent` classes to streamline server event streaming 7.2 --- diff --git a/EventStreamResponse.php b/EventStreamResponse.php new file mode 100644 index 000000000..fe1a2872e --- /dev/null +++ b/EventStreamResponse.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * Represents a streaming HTTP response for sending server events + * as part of the Server-Sent Events (SSE) streaming technique. + * + * To broadcast events to multiple users at once, for long-running + * connections and for high-traffic websites, prefer using the Mercure + * Symfony Component, which relies on Software designed for these use + * cases: https://symfony.com/doc/current/mercure.html + * + * @see ServerEvent + * + * @author Yonel Ceruto + * + * Example usage: + * + * return new EventStreamResponse(function () { + * yield new ServerEvent(time()); + * + * sleep(1); + * + * yield new ServerEvent(time()); + * }); + */ +class EventStreamResponse extends StreamedResponse +{ + /** + * @param int|null $retry The number of milliseconds the client should wait + * before reconnecting in case of network failure + */ + public function __construct(?callable $callback = null, int $status = 200, array $headers = [], private ?int $retry = null) + { + $headers += [ + 'Connection' => 'keep-alive', + 'Content-Type' => 'text/event-stream', + 'Cache-Control' => 'private, no-cache, no-store, must-revalidate, max-age=0', + 'X-Accel-Buffering' => 'no', + 'Pragma' => 'no-cache', + 'Expire' => '0', + ]; + + parent::__construct($callback, $status, $headers); + } + + public function setCallback(callable $callback): static + { + if ($this->callback) { + return parent::setCallback($callback); + } + + $this->callback = function () use ($callback) { + if (is_iterable($events = $callback($this))) { + foreach ($events as $event) { + $this->sendEvent($event); + + if (connection_aborted()) { + break; + } + } + } + }; + + return $this; + } + + /** + * Sends a server event to the client. + * + * @return $this + */ + public function sendEvent(ServerEvent $event): static + { + if ($this->retry > 0 && !$event->getRetry()) { + $event->setRetry($this->retry); + } + + foreach ($event as $part) { + echo $part; + + if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) { + static::closeOutputBuffers(0, true); + flush(); + } + } + + return $this; + } + + public function getRetry(): ?int + { + return $this->retry; + } + + public function setRetry(int $retry): void + { + $this->retry = $retry; + } +} diff --git a/ServerEvent.php b/ServerEvent.php new file mode 100644 index 000000000..ea2b5c885 --- /dev/null +++ b/ServerEvent.php @@ -0,0 +1,147 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * An event generated on the server intended for streaming to the client + * as part of the SSE streaming technique. + * + * @implements \IteratorAggregate + * + * @author Yonel Ceruto + */ +class ServerEvent implements \IteratorAggregate +{ + /** + * @param string|iterable $data The event data field for the message + * @param string|null $type The event type + * @param int|null $retry The number of milliseconds the client should wait + * before reconnecting in case of network failure + * @param string|null $id The event ID to set the EventSource object's last event ID value + * @param string|null $comment The event comment + */ + public function __construct( + private string|iterable $data, + private ?string $type = null, + private ?int $retry = null, + private ?string $id = null, + private ?string $comment = null, + ) { + } + + public function getData(): iterable|string + { + return $this->data; + } + + /** + * @return $this + */ + public function setData(iterable|string $data): static + { + $this->data = $data; + + return $this; + } + + public function getType(): ?string + { + return $this->type; + } + + /** + * @return $this + */ + public function setType(string $type): static + { + $this->type = $type; + + return $this; + } + + public function getRetry(): ?int + { + return $this->retry; + } + + /** + * @return $this + */ + public function setRetry(?int $retry): static + { + $this->retry = $retry; + + return $this; + } + + public function getId(): ?string + { + return $this->id; + } + + /** + * @return $this + */ + public function setId(string $id): static + { + $this->id = $id; + + return $this; + } + + public function getComment(): ?string + { + return $this->comment; + } + + public function setComment(string $comment): static + { + $this->comment = $comment; + + return $this; + } + + /** + * @return \Traversable + */ + public function getIterator(): \Traversable + { + static $lastRetry = null; + + $head = ''; + if ($this->comment) { + $head .= \sprintf(': %s', $this->comment)."\n"; + } + if ($this->id) { + $head .= \sprintf('id: %s', $this->id)."\n"; + } + if ($this->retry > 0 && $this->retry !== $lastRetry) { + $head .= \sprintf('retry: %s', $lastRetry = $this->retry)."\n"; + } + if ($this->type) { + $head .= \sprintf('event: %s', $this->type)."\n"; + } + yield $head; + + if ($this->data) { + if (is_iterable($this->data)) { + foreach ($this->data as $data) { + yield \sprintf('data: %s', $data)."\n"; + } + } else { + yield \sprintf('data: %s', $this->data)."\n"; + } + } + + yield "\n"; + } +} diff --git a/Tests/EventStreamResponseTest.php b/Tests/EventStreamResponseTest.php new file mode 100644 index 000000000..4c430fbe8 --- /dev/null +++ b/Tests/EventStreamResponseTest.php @@ -0,0 +1,127 @@ + + * + * 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\EventStreamResponse; +use Symfony\Component\HttpFoundation\ServerEvent; + +class EventStreamResponseTest extends TestCase +{ + public function testInitializationWithDefaultValues() + { + $response = new EventStreamResponse(); + + $this->assertSame('text/event-stream', $response->headers->get('content-type')); + $this->assertSame('max-age=0, must-revalidate, no-cache, no-store, private', $response->headers->get('cache-control')); + $this->assertSame('keep-alive', $response->headers->get('connection')); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertNull($response->getRetry()); + } + + public function testStreamSingleEvent() + { + $response = new EventStreamResponse(function () { + yield new ServerEvent( + data: 'foo', + type: 'bar', + retry: 100, + id: '1', + comment: 'bla bla', + ); + }); + + $expected = <<assertSameResponseContent($expected, $response); + } + + public function testStreamEventsAndData() + { + $data = static function (): iterable { + yield 'first line'; + yield 'second line'; + yield 'third line'; + }; + + $response = new EventStreamResponse(function () use ($data) { + yield new ServerEvent('single line'); + yield new ServerEvent(['first line', 'second line']); + yield new ServerEvent($data()); + }); + + $expected = <<assertSameResponseContent($expected, $response); + } + + public function testStreamEventsWithRetryFallback() + { + $response = new EventStreamResponse(function () { + yield new ServerEvent('foo'); + yield new ServerEvent('bar'); + yield new ServerEvent('baz', retry: 1000); + }, retry: 1500); + + $expected = <<assertSameResponseContent($expected, $response); + } + + public function testStreamEventWithSendMethod() + { + $response = new EventStreamResponse(function (EventStreamResponse $response) { + $response->sendEvent(new ServerEvent('foo')); + }); + + $this->assertSameResponseContent("data: foo\n\n", $response); + } + + private function assertSameResponseContent(string $expected, EventStreamResponse $response): void + { + ob_start(); + $response->send(); + $actual = ob_get_clean(); + + $this->assertSame($expected, $actual); + } +} From ce130081367d3b2a4a722327f8b6c2b62da72a2a Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 26 Feb 2025 18:04:06 +0100 Subject: [PATCH 32/44] Add support for `valkey:` / `valkeys:` schemes --- CHANGELOG.md | 1 + Session/Storage/Handler/SessionHandlerFactory.php | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0841fa9ab..59070ee8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * Add support for iterable of string in `StreamedResponse` * Add `EventStreamResponse` and `ServerEvent` classes to streamline server event streaming + * Add support for `valkey:` / `valkeys:` schemes for sessions 7.2 --- diff --git a/Session/Storage/Handler/SessionHandlerFactory.php b/Session/Storage/Handler/SessionHandlerFactory.php index 13af07c21..cb0b6f8a9 100644 --- a/Session/Storage/Handler/SessionHandlerFactory.php +++ b/Session/Storage/Handler/SessionHandlerFactory.php @@ -57,6 +57,8 @@ public static function createHandler(object|string $connection, array $options = case str_starts_with($connection, 'redis:'): case str_starts_with($connection, 'rediss:'): + case str_starts_with($connection, 'valkey:'): + case str_starts_with($connection, 'valkeys:'): case str_starts_with($connection, 'memcached:'): if (!class_exists(AbstractAdapter::class)) { throw new \InvalidArgumentException('Unsupported Redis or Memcached DSN. Try running "composer require symfony/cache".'); From 28d6dfa2bf46226604c57c56f92e423a3d195352 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sun, 2 Mar 2025 16:03:52 +0100 Subject: [PATCH 33/44] replace assertEmpty() with stricter assertions --- Tests/ParameterBagTest.php | 2 +- Tests/ResponseTest.php | 2 +- Tests/Session/Storage/Handler/PdoSessionHandlerTest.php | 2 +- Tests/StreamedResponseTest.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/ParameterBagTest.php b/Tests/ParameterBagTest.php index ac2b4da5a..5729af2c2 100644 --- a/Tests/ParameterBagTest.php +++ b/Tests/ParameterBagTest.php @@ -253,7 +253,7 @@ public function testFilter() 'array' => ['bang'], ]); - $this->assertEmpty($bag->filter('nokey'), '->filter() should return empty by default if no key is found'); + $this->assertSame('', $bag->filter('nokey'), '->filter() should return empty by default if no key is found'); $this->assertEquals('0123', $bag->filter('digits', '', \FILTER_SANITIZE_NUMBER_INT), '->filter() gets a value of parameter as integer filtering out invalid characters'); diff --git a/Tests/ResponseTest.php b/Tests/ResponseTest.php index 491a50fd5..2c761a4f8 100644 --- a/Tests/ResponseTest.php +++ b/Tests/ResponseTest.php @@ -127,7 +127,7 @@ public function testSetNotModified() ob_start(); $modified->sendContent(); $string = ob_get_clean(); - $this->assertEmpty($string); + $this->assertSame('', $string); } public function testIsSuccessful() diff --git a/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php b/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php index 20ca3e269..0ee76ae0b 100644 --- a/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php @@ -352,7 +352,7 @@ public function testConfigureSchemaTableExistsPdo() $pdoSessionHandler = new PdoSessionHandler($this->getMemorySqlitePdo()); $pdoSessionHandler->configureSchema($schema, fn () => true); $table = $schema->getTable('sessions'); - $this->assertEmpty($table->getColumns(), 'The table was not overwritten'); + $this->assertSame([], $table->getColumns(), 'The table was not overwritten'); } public static function provideUrlDsnPairs() diff --git a/Tests/StreamedResponseTest.php b/Tests/StreamedResponseTest.php index 78a777aea..2a8fe5825 100644 --- a/Tests/StreamedResponseTest.php +++ b/Tests/StreamedResponseTest.php @@ -133,7 +133,7 @@ public function testSetNotModified() ob_start(); $modified->sendContent(); $string = ob_get_clean(); - $this->assertEmpty($string); + $this->assertSame('', $string); } public function testSendInformationalResponse() From 371272aeb6286f8135e028ca535f8e4d6f114126 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 24 Mar 2025 17:30:13 +0100 Subject: [PATCH 34/44] use Table::addPrimaryKeyConstraint() with Doctrine DBAL 4.3+ --- Session/Storage/Handler/PdoSessionHandler.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Session/Storage/Handler/PdoSessionHandler.php b/Session/Storage/Handler/PdoSessionHandler.php index f08471e9b..e2fb4f129 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\Name\Identifier; +use Doctrine\DBAL\Schema\Name\UnqualifiedName; +use Doctrine\DBAL\Schema\PrimaryKeyConstraint; use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Types\Types; @@ -224,7 +227,13 @@ public function configureSchema(Schema $schema, ?\Closure $isSameDatabase = null default: throw new \DomainException(\sprintf('Creating the session table is currently not implemented for PDO driver "%s".', $this->driver)); } - $table->setPrimaryKey([$this->idCol]); + + if (class_exists(PrimaryKeyConstraint::class)) { + $table->addPrimaryKeyConstraint(new PrimaryKeyConstraint(null, [new UnqualifiedName(Identifier::unquoted($this->idCol))], true)); + } else { + $table->setPrimaryKey([$this->idCol]); + } + $table->addIndex([$this->lifetimeCol], $this->lifetimeCol.'_idx'); } From 6f7fb440036ce73bfb46ceac0bdefaae1ccc6b14 Mon Sep 17 00:00:00 2001 From: chillbram <7299762+chillbram@users.noreply.github.com> Date: Thu, 3 Apr 2025 21:52:33 +0200 Subject: [PATCH 35/44] [HttpFoundation] Follow language preferences more accurately in `getPreferredLanguage()` --- CHANGELOG.md | 1 + Request.php | 4 ---- Tests/RequestTest.php | 12 ++++++------ 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59070ee8b..2d8065ba5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG * Add support for iterable of string in `StreamedResponse` * Add `EventStreamResponse` and `ServerEvent` classes to streamline server event streaming * Add support for `valkey:` / `valkeys:` schemes for sessions + * `Request::getPreferredLanguage()` now favors a more preferred language above exactly matching a locale 7.2 --- diff --git a/Request.php b/Request.php index db78105cc..9f421525d 100644 --- a/Request.php +++ b/Request.php @@ -1553,10 +1553,6 @@ public function getPreferredLanguage(?array $locales = null): ?string return $locales[0]; } - if ($matches = array_intersect($preferredLanguages, $locales)) { - return current($matches); - } - $combinations = array_merge(...array_map($this->getLanguageCombinations(...), $preferredLanguages)); foreach ($combinations as $combination) { foreach ($locales as $locale) { diff --git a/Tests/RequestTest.php b/Tests/RequestTest.php index d5a41390e..bb4eeb3b6 100644 --- a/Tests/RequestTest.php +++ b/Tests/RequestTest.php @@ -1550,16 +1550,16 @@ public static function providePreferredLanguage(): iterable yield '"fr" selected as first choice when no header is present' => ['fr', null, ['fr', 'en']]; yield '"en" selected as first choice when no header is present' => ['en', null, ['en', 'fr']]; yield '"fr_CH" selected as first choice when no header is present' => ['fr_CH', null, ['fr-ch', 'fr-fr']]; - yield '"en_US" is selected as an exact match is found (1)' => ['en_US', 'zh, en-us; q=0.8, en; q=0.6', ['en', 'en-us']]; - yield '"en_US" is selected as an exact match is found (2)' => ['en_US', 'ja-JP,fr_CA;q=0.7,fr;q=0.5,en_US;q=0.3', ['en_US', 'fr_FR']]; - yield '"en" is selected as an exact match is found' => ['en', 'zh, en-us; q=0.8, en; q=0.6', ['fr', 'en']]; - yield '"fr" is selected as an exact match is found' => ['fr', 'zh, en-us; q=0.8, fr-fr; q=0.6, fr; q=0.5', ['fr', 'en']]; + yield '"en_US" is selected as an exact match is found' => ['en_US', 'zh, en-us; q=0.8, en; q=0.6', ['en', 'en-us']]; + yield '"fr_FR" is selected as it has a higher priority than an exact match' => ['fr_FR', 'ja-JP,fr_CA;q=0.7,fr;q=0.5,en_US;q=0.3', ['en_US', 'fr_FR']]; + yield '"en" is selected as an exact match is found (1)' => ['en', 'zh, en-us; q=0.8, en; q=0.6', ['fr', 'en']]; + yield '"en" is selected as an exact match is found (2)' => ['en', 'zh, en-us; q=0.8, fr-fr; q=0.6, fr; q=0.5', ['fr', 'en']]; yield '"en" is selected as "en-us" is a similar dialect' => ['en', 'zh, en-us; q=0.8', ['fr', 'en']]; yield '"fr_FR" is selected as "fr_CA" is a similar dialect (1)' => ['fr_FR', 'ja-JP,fr_CA;q=0.7,fr;q=0.5', ['en_US', 'fr_FR']]; yield '"fr_FR" is selected as "fr_CA" is a similar dialect (2)' => ['fr_FR', 'ja-JP,fr_CA;q=0.7', ['en_US', 'fr_FR']]; - yield '"fr_FR" is selected as "fr" is a similar dialect' => ['fr_FR', 'ja-JP,fr;q=0.5', ['en_US', 'fr_FR']]; + yield '"fr_FR" is selected as "fr" is a similar dialect (1)' => ['fr_FR', 'ja-JP,fr;q=0.5', ['en_US', 'fr_FR']]; + yield '"fr_FR" is selected as "fr" is a similar dialect (2)' => ['fr_FR', 'ja-JP,fr;q=0.5,en_US;q=0.3', ['en_US', 'fr_FR']]; yield '"fr_FR" is selected as "fr_CA" is a similar dialect and has a greater "q" compared to "en_US" (2)' => ['fr_FR', 'ja-JP,fr_CA;q=0.7,ru-ru;q=0.3', ['en_US', 'fr_FR']]; - yield '"en_US" is selected it is an exact match' => ['en_US', 'ja-JP,fr;q=0.5,en_US;q=0.3', ['en_US', 'fr_FR']]; yield '"fr_FR" is selected as "fr_CA" is a similar dialect and has a greater "q" compared to "en"' => ['fr_FR', 'ja-JP,fr_CA;q=0.7,en;q=0.5', ['en_US', 'fr_FR']]; yield '"fr_FR" is selected as is is an exact match as well as "en_US", but with a greater "q" parameter' => ['fr_FR', 'en-us;q=0.5,fr-fr', ['en_US', 'fr_FR']]; yield '"hi_IN" is selected as "hi_Latn_IN" is a similar dialect' => ['hi_IN', 'fr-fr,hi_Latn_IN;q=0.5', ['hi_IN', 'en_US']]; From b0e234d4907616eda45c00ed479bc4074017b7df Mon Sep 17 00:00:00 2001 From: timesince Date: Wed, 9 Apr 2025 13:59:35 +0800 Subject: [PATCH 36/44] chore: fix some typos Signed-off-by: timesince --- Tests/InputBagTest.php | 2 +- Tests/ParameterBagTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/InputBagTest.php b/Tests/InputBagTest.php index e2112726e..d1e9015f1 100644 --- a/Tests/InputBagTest.php +++ b/Tests/InputBagTest.php @@ -78,7 +78,7 @@ public function __toString(): string $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'); + $this->assertSame('strval', $bag->getString('stringable'), '->getString() gets a value of a stringable parameter as string'); } public function testGetStringExceptionWithArray() diff --git a/Tests/ParameterBagTest.php b/Tests/ParameterBagTest.php index 42c1b67da..ad0cf99bf 100644 --- a/Tests/ParameterBagTest.php +++ b/Tests/ParameterBagTest.php @@ -226,7 +226,7 @@ public function __toString(): string $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'); + $this->assertSame('strval', $bag->getString('stringable'), '->getString() gets a value of a stringable parameter as string'); } public function testGetStringExceptionWithArray() From 54169d58f79677af67bde50657f0f7620075bd47 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Tue, 15 Apr 2025 11:04:08 -0400 Subject: [PATCH 37/44] [HttpFoundation][FrameworkBundle] clock support for `UriSigner` --- CHANGELOG.md | 1 + Tests/UriSignerTest.php | 24 ++++++++++++++++++++++++ UriSigner.php | 11 +++++++++-- composer.json | 1 + 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d8065ba5..5410cba63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG * Add `EventStreamResponse` and `ServerEvent` classes to streamline server event streaming * Add support for `valkey:` / `valkeys:` schemes for sessions * `Request::getPreferredLanguage()` now favors a more preferred language above exactly matching a locale + * Allow `UriSigner` to use a `ClockInterface` 7.2 --- diff --git a/Tests/UriSignerTest.php b/Tests/UriSignerTest.php index 927e2bda8..85a0b727c 100644 --- a/Tests/UriSignerTest.php +++ b/Tests/UriSignerTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpFoundation\Tests; use PHPUnit\Framework\TestCase; +use Symfony\Component\Clock\MockClock; use Symfony\Component\HttpFoundation\Exception\LogicException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\UriSigner; @@ -199,6 +200,29 @@ public function testCheckWithUriExpiration() $this->assertFalse($signer->check($relativeUriFromNow3)); } + public function testCheckWithUriExpirationWithClock() + { + $clock = new MockClock(); + $signer = new UriSigner('foobar', clock: $clock); + + $this->assertFalse($signer->check($signer->sign('http://example.com/foo', new \DateTimeImmutable('2000-01-01 00:00:00')))); + $this->assertFalse($signer->check($signer->sign('http://example.com/foo?foo=bar', new \DateTimeImmutable('2000-01-01 00:00:00')))); + $this->assertFalse($signer->check($signer->sign('http://example.com/foo?foo=bar&0=integer', new \DateTimeImmutable('2000-01-01 00:00:00')))); + + $this->assertFalse($signer->check($signer->sign('http://example.com/foo', 1577836800))); // 2000-01-01 + $this->assertFalse($signer->check($signer->sign('http://example.com/foo?foo=bar', 1577836800))); // 2000-01-01 + $this->assertFalse($signer->check($signer->sign('http://example.com/foo?foo=bar&0=integer', 1577836800))); // 2000-01-01 + + $relativeUriFromNow1 = $signer->sign('http://example.com/foo', new \DateInterval('PT3S')); + $relativeUriFromNow2 = $signer->sign('http://example.com/foo?foo=bar', new \DateInterval('PT3S')); + $relativeUriFromNow3 = $signer->sign('http://example.com/foo?foo=bar&0=integer', new \DateInterval('PT3S')); + $clock->sleep(10); + + $this->assertFalse($signer->check($relativeUriFromNow1)); + $this->assertFalse($signer->check($relativeUriFromNow2)); + $this->assertFalse($signer->check($relativeUriFromNow3)); + } + public function testNonUrlSafeBase64() { $signer = new UriSigner('foobar'); diff --git a/UriSigner.php b/UriSigner.php index 1c9e25a5c..b1109ae69 100644 --- a/UriSigner.php +++ b/UriSigner.php @@ -11,6 +11,7 @@ namespace Symfony\Component\HttpFoundation; +use Psr\Clock\ClockInterface; use Symfony\Component\HttpFoundation\Exception\LogicException; /** @@ -26,6 +27,7 @@ public function __construct( #[\SensitiveParameter] private string $secret, private string $hashParameter = '_hash', private string $expirationParameter = '_expiration', + private ?ClockInterface $clock = null, ) { if (!$secret) { throw new \InvalidArgumentException('A non-empty secret is required.'); @@ -109,7 +111,7 @@ public function check(string $uri): bool } if ($expiration = $params[$this->expirationParameter] ?? false) { - return time() < $expiration; + return $this->now()->getTimestamp() < $expiration; } return true; @@ -153,9 +155,14 @@ private function getExpirationTime(\DateTimeInterface|\DateInterval|int $expirat } if ($expiration instanceof \DateInterval) { - return \DateTimeImmutable::createFromFormat('U', time())->add($expiration)->format('U'); + return $this->now()->add($expiration)->format('U'); } return (string) $expiration; } + + private function now(): \DateTimeImmutable + { + return $this->clock?->now() ?? \DateTimeImmutable::createFromFormat('U', time()); + } } diff --git a/composer.json b/composer.json index cb2bbf8cb..a86b21b7c 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", "symfony/cache": "^6.4.12|^7.1.5", + "symfony/clock": "^6.4|^7.0", "symfony/dependency-injection": "^6.4|^7.0", "symfony/http-kernel": "^6.4|^7.0", "symfony/mime": "^6.4|^7.0", From 5a69e812075d8bae5da68d8e0ffa66699e556590 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Mon, 31 Mar 2025 18:06:51 -0400 Subject: [PATCH 38/44] [HttpFoundation] Add `UriSigner::verify()` that throws named exceptions --- CHANGELOG.md | 1 + Exception/ExpiredSignedUriException.php | 26 ++++++ Exception/SignedUriException.php | 19 ++++ Exception/UnsignedUriException.php | 26 ++++++ Exception/UnverifiedSignedUriException.php | 26 ++++++ Tests/UriSignerTest.php | 33 +++++++ UriSigner.php | 103 ++++++++++++++++----- 7 files changed, 210 insertions(+), 24 deletions(-) create mode 100644 Exception/ExpiredSignedUriException.php create mode 100644 Exception/SignedUriException.php create mode 100644 Exception/UnsignedUriException.php create mode 100644 Exception/UnverifiedSignedUriException.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 5410cba63..374c31889 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ CHANGELOG * Add support for `valkey:` / `valkeys:` schemes for sessions * `Request::getPreferredLanguage()` now favors a more preferred language above exactly matching a locale * Allow `UriSigner` to use a `ClockInterface` + * Add `UriSigner::verify()` 7.2 --- diff --git a/Exception/ExpiredSignedUriException.php b/Exception/ExpiredSignedUriException.php new file mode 100644 index 000000000..613e08ef4 --- /dev/null +++ b/Exception/ExpiredSignedUriException.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Exception; + +/** + * @author Kevin Bond + */ +final class ExpiredSignedUriException extends SignedUriException +{ + /** + * @internal + */ + public function __construct() + { + parent::__construct('The URI has expired.'); + } +} diff --git a/Exception/SignedUriException.php b/Exception/SignedUriException.php new file mode 100644 index 000000000..17b729d31 --- /dev/null +++ b/Exception/SignedUriException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Exception; + +/** + * @author Kevin Bond + */ +abstract class SignedUriException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/Exception/UnsignedUriException.php b/Exception/UnsignedUriException.php new file mode 100644 index 000000000..5eabb806b --- /dev/null +++ b/Exception/UnsignedUriException.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Exception; + +/** + * @author Kevin Bond + */ +final class UnsignedUriException extends SignedUriException +{ + /** + * @internal + */ + public function __construct() + { + parent::__construct('The URI is not signed.'); + } +} diff --git a/Exception/UnverifiedSignedUriException.php b/Exception/UnverifiedSignedUriException.php new file mode 100644 index 000000000..cc7e98bf2 --- /dev/null +++ b/Exception/UnverifiedSignedUriException.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Exception; + +/** + * @author Kevin Bond + */ +final class UnverifiedSignedUriException extends SignedUriException +{ + /** + * @internal + */ + public function __construct() + { + parent::__construct('The URI signature is invalid.'); + } +} diff --git a/Tests/UriSignerTest.php b/Tests/UriSignerTest.php index 85a0b727c..81b35c28e 100644 --- a/Tests/UriSignerTest.php +++ b/Tests/UriSignerTest.php @@ -13,7 +13,10 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Clock\MockClock; +use Symfony\Component\HttpFoundation\Exception\ExpiredSignedUriException; use Symfony\Component\HttpFoundation\Exception\LogicException; +use Symfony\Component\HttpFoundation\Exception\UnsignedUriException; +use Symfony\Component\HttpFoundation\Exception\UnverifiedSignedUriException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\UriSigner; @@ -228,4 +231,34 @@ public function testNonUrlSafeBase64() $signer = new UriSigner('foobar'); $this->assertTrue($signer->check('http://example.com/foo?_hash=rIOcC%2FF3DoEGo%2FvnESjSp7uU9zA9S%2F%2BOLhxgMexoPUM%3D&baz=bay&foo=bar')); } + + public function testVerifyUnSignedUri() + { + $signer = new UriSigner('foobar'); + $uri = 'http://example.com/foo'; + + $this->expectException(UnsignedUriException::class); + + $signer->verify($uri); + } + + public function testVerifyUnverifiedUri() + { + $signer = new UriSigner('foobar'); + $uri = 'http://example.com/foo?_hash=invalid'; + + $this->expectException(UnverifiedSignedUriException::class); + + $signer->verify($uri); + } + + public function testVerifyExpiredUri() + { + $signer = new UriSigner('foobar'); + $uri = $signer->sign('http://example.com/foo', 123456); + + $this->expectException(ExpiredSignedUriException::class); + + $signer->verify($uri); + } } diff --git a/UriSigner.php b/UriSigner.php index b1109ae69..bb870e43c 100644 --- a/UriSigner.php +++ b/UriSigner.php @@ -12,13 +12,22 @@ namespace Symfony\Component\HttpFoundation; use Psr\Clock\ClockInterface; +use Symfony\Component\HttpFoundation\Exception\ExpiredSignedUriException; use Symfony\Component\HttpFoundation\Exception\LogicException; +use Symfony\Component\HttpFoundation\Exception\SignedUriException; +use Symfony\Component\HttpFoundation\Exception\UnsignedUriException; +use Symfony\Component\HttpFoundation\Exception\UnverifiedSignedUriException; /** * @author Fabien Potencier */ class UriSigner { + private const STATUS_VALID = 1; + private const STATUS_INVALID = 2; + private const STATUS_MISSING = 3; + private const STATUS_EXPIRED = 4; + /** * @param string $hashParameter Query string parameter to use * @param string $expirationParameter Query string parameter to use for expiration @@ -91,38 +100,40 @@ public function sign(string $uri/* , \DateTimeInterface|\DateInterval|int|null $ */ public function check(string $uri): bool { - $url = parse_url($uri); - $params = []; - - if (isset($url['query'])) { - parse_str($url['query'], $params); - } + return self::STATUS_VALID === $this->doVerify($uri); + } - if (empty($params[$this->hashParameter])) { - return false; - } + public function checkRequest(Request $request): bool + { + return self::STATUS_VALID === $this->doVerify(self::normalize($request)); + } - $hash = $params[$this->hashParameter]; - unset($params[$this->hashParameter]); + /** + * Verify a Request or string URI. + * + * @throws UnsignedUriException If the URI is not signed + * @throws UnverifiedSignedUriException If the signature is invalid + * @throws ExpiredSignedUriException If the URI has expired + * @throws SignedUriException + */ + public function verify(Request|string $uri): void + { + $uri = self::normalize($uri); + $status = $this->doVerify($uri); - // In 8.0, remove support for non-url-safe tokens - if (!hash_equals($this->computeHash($this->buildUrl($url, $params)), strtr(rtrim($hash, '='), ['/' => '_', '+' => '-']))) { - return false; + if (self::STATUS_VALID === $status) { + return; } - if ($expiration = $params[$this->expirationParameter] ?? false) { - return $this->now()->getTimestamp() < $expiration; + if (self::STATUS_MISSING === $status) { + throw new UnsignedUriException(); } - return true; - } - - public function checkRequest(Request $request): bool - { - $qs = ($qs = $request->server->get('QUERY_STRING')) ? '?'.$qs : ''; + if (self::STATUS_INVALID === $status) { + throw new UnverifiedSignedUriException(); + } - // 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); + throw new ExpiredSignedUriException(); } private function computeHash(string $uri): string @@ -165,4 +176,48 @@ private function now(): \DateTimeImmutable { return $this->clock?->now() ?? \DateTimeImmutable::createFromFormat('U', time()); } + + /** + * @return self::STATUS_* + */ + private function doVerify(string $uri): int + { + $url = parse_url($uri); + $params = []; + + if (isset($url['query'])) { + parse_str($url['query'], $params); + } + + if (empty($params[$this->hashParameter])) { + return self::STATUS_MISSING; + } + + $hash = $params[$this->hashParameter]; + unset($params[$this->hashParameter]); + + if (!hash_equals($this->computeHash($this->buildUrl($url, $params)), strtr(rtrim($hash, '='), ['/' => '_', '+' => '-']))) { + return self::STATUS_INVALID; + } + + if (!$expiration = $params[$this->expirationParameter] ?? false) { + return self::STATUS_VALID; + } + + if ($this->now()->getTimestamp() < $expiration) { + return self::STATUS_VALID; + } + + return self::STATUS_EXPIRED; + } + + private static function normalize(Request|string $uri): string + { + if ($uri instanceof Request) { + $qs = ($qs = $uri->server->get('QUERY_STRING')) ? '?'.$qs : ''; + $uri = $uri->getSchemeAndHttpHost().$uri->getBaseUrl().$uri->getPathInfo().$qs; + } + + return $uri; + } } From abbe5faf754aebc557c4da9c8e2780b4f094c5ce Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Thu, 24 Apr 2025 08:52:37 +0200 Subject: [PATCH 39/44] [HttpFoundation] Flush after each echo in `StreamedResponse` --- StreamedResponse.php | 2 ++ Tests/StreamedResponseTest.php | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/StreamedResponse.php b/StreamedResponse.php index 6eedf1c49..4e755a7cd 100644 --- a/StreamedResponse.php +++ b/StreamedResponse.php @@ -56,6 +56,8 @@ public function setChunks(iterable $chunks): static $this->callback = static function () use ($chunks): void { foreach ($chunks as $chunk) { echo $chunk; + @ob_flush(); + flush(); } }; diff --git a/Tests/StreamedResponseTest.php b/Tests/StreamedResponseTest.php index 2a8fe5825..fdaee3a35 100644 --- a/Tests/StreamedResponseTest.php +++ b/Tests/StreamedResponseTest.php @@ -30,10 +30,14 @@ public function testConstructorWithChunks() $chunks = ['foo', 'bar', 'baz']; $callback = (new StreamedResponse($chunks))->getCallback(); - ob_start(); + $buffer = ''; + ob_start(function (string $chunk) use (&$buffer) { + $buffer .= $chunk; + }); $callback(); - $this->assertSame('foobarbaz', ob_get_clean()); + ob_get_clean(); + $this->assertSame('foobarbaz', $buffer); } public function testPrepareWith11Protocol() From 3f0c7ea41db479383b81d436b836d37168fd5b99 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 27 Apr 2025 15:26:02 +0200 Subject: [PATCH 40/44] Remove unneeded use statements --- Tests/RateLimiter/AbstractRequestRateLimiterTest.php | 1 - Tests/Session/Storage/Proxy/AbstractProxyTest.php | 1 - 2 files changed, 2 deletions(-) diff --git a/Tests/RateLimiter/AbstractRequestRateLimiterTest.php b/Tests/RateLimiter/AbstractRequestRateLimiterTest.php index 26f2fac90..087d7aeae 100644 --- a/Tests/RateLimiter/AbstractRequestRateLimiterTest.php +++ b/Tests/RateLimiter/AbstractRequestRateLimiterTest.php @@ -14,7 +14,6 @@ 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 diff --git a/Tests/Session/Storage/Proxy/AbstractProxyTest.php b/Tests/Session/Storage/Proxy/AbstractProxyTest.php index bb459bb9f..8d04830a7 100644 --- a/Tests/Session/Storage/Proxy/AbstractProxyTest.php +++ b/Tests/Session/Storage/Proxy/AbstractProxyTest.php @@ -11,7 +11,6 @@ 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; From 7965dc6fde8d57a626988a94f5447dcb47e8dca0 Mon Sep 17 00:00:00 2001 From: wkania Date: Sun, 27 Apr 2025 16:24:15 +0200 Subject: [PATCH 41/44] Fix overwriting an array element --- Tests/RequestTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/Tests/RequestTest.php b/Tests/RequestTest.php index 7a4807ecf..f1aa0ebea 100644 --- a/Tests/RequestTest.php +++ b/Tests/RequestTest.php @@ -604,7 +604,6 @@ public function testGetUri() $server['REDIRECT_QUERY_STRING'] = 'query=string'; $server['REDIRECT_URL'] = '/path/info'; - $server['SCRIPT_NAME'] = '/index.php'; $server['QUERY_STRING'] = 'query=string'; $server['REQUEST_URI'] = '/path/info?toto=test&1=1'; $server['SCRIPT_NAME'] = '/index.php'; @@ -731,7 +730,6 @@ public function testGetUriForPath() $server['REDIRECT_QUERY_STRING'] = 'query=string'; $server['REDIRECT_URL'] = '/path/info'; - $server['SCRIPT_NAME'] = '/index.php'; $server['QUERY_STRING'] = 'query=string'; $server['REQUEST_URI'] = '/path/info?toto=test&1=1'; $server['SCRIPT_NAME'] = '/index.php'; From 90313c6e0b8955dbde57f450ac7a484c2dc592b9 Mon Sep 17 00:00:00 2001 From: ivelin vasilev Date: Thu, 8 May 2025 01:06:34 +0300 Subject: [PATCH 42/44] [HttpFoundation] Emit PHP warning when Response::sendHeaders() while output has already been sent --- Response.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Response.php b/Response.php index 638b5bf60..6766f2c77 100644 --- a/Response.php +++ b/Response.php @@ -317,6 +317,11 @@ public function sendHeaders(?int $statusCode = null): static { // headers have already been sent by the developer if (headers_sent()) { + if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) { + $statusCode ??= $this->statusCode; + header(\sprintf('HTTP/%s %s %s', $this->version, $statusCode, $this->statusText), true, $statusCode); + } + return $this; } From 6b7c97fe1ddac8df3cc9ba6410c8abc683e148ae Mon Sep 17 00:00:00 2001 From: Athorcis Date: Mon, 28 Apr 2025 13:34:00 +0200 Subject: [PATCH 43/44] [HttpFoundation] Fix: Encode path in X-Accel-Redirect header we need to encode the path in X-Accel-Redirect header, otherwise nginx fail when certain characters are present in it (like % or ?) https://github.com/rack/rack/issues/1306 --- BinaryFileResponse.php | 2 +- Tests/BinaryFileResponseTest.php | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/BinaryFileResponse.php b/BinaryFileResponse.php index 41a244b81..c22f283cb 100644 --- a/BinaryFileResponse.php +++ b/BinaryFileResponse.php @@ -229,7 +229,7 @@ public function prepare(Request $request): static $path = $location.substr($path, \strlen($pathPrefix)); // Only set X-Accel-Redirect header if a valid URI can be produced // as nginx does not serve arbitrary file paths. - $this->headers->set($type, $path); + $this->headers->set($type, rawurlencode($path)); $this->maxlen = 0; break; } diff --git a/Tests/BinaryFileResponseTest.php b/Tests/BinaryFileResponseTest.php index c7d47a4d7..8f298b77f 100644 --- a/Tests/BinaryFileResponseTest.php +++ b/Tests/BinaryFileResponseTest.php @@ -314,7 +314,15 @@ public function testXAccelMapping($realpath, $mapping, $virtual) $property->setValue($response, $file); $response->prepare($request); - $this->assertEquals($virtual, $response->headers->get('X-Accel-Redirect')); + $header = $response->headers->get('X-Accel-Redirect'); + + if ($virtual) { + // Making sure the path doesn't contain characters unsupported by nginx + $this->assertMatchesRegularExpression('/^([^?%]|%[0-9A-F]{2})*$/', $header); + $header = rawurldecode($header); + } + + $this->assertEquals($virtual, $header); } public function testDeleteFileAfterSend() @@ -361,6 +369,7 @@ public static function getSampleXAccelMappings() ['/home/Foo/bar.txt', '/var/www/=/files/,/home/Foo/=/baz/', '/baz/bar.txt'], ['/home/Foo/bar.txt', '"/var/www/"="/files/", "/home/Foo/"="/baz/"', '/baz/bar.txt'], ['/tmp/bar.txt', '"/var/www/"="/files/", "/home/Foo/"="/baz/"', null], + ['/var/www/var/www/files/foo%.txt', '/var/www/=/files/', '/files/var/www/files/foo%.txt'], ]; } From 452d19f945ee41345fd8a50c18b60783546b7bd3 Mon Sep 17 00:00:00 2001 From: thecaliskan Date: Mon, 26 May 2025 11:39:29 +0300 Subject: [PATCH 44/44] fixed Via regex --- Request.php | 2 +- Tests/RequestTest.php | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Request.php b/Request.php index 922014133..42a3a8a2c 100644 --- a/Request.php +++ b/Request.php @@ -1466,7 +1466,7 @@ public function isMethodCacheable(): bool public function getProtocolVersion(): ?string { if ($this->isFromTrustedProxy()) { - preg_match('~^(HTTP/)?([1-9]\.[0-9]) ~', $this->headers->get('Via') ?? '', $matches); + preg_match('~^(HTTP/)?([1-9]\.[0-9])\b~', $this->headers->get('Via') ?? '', $matches); if ($matches) { return 'HTTP/'.$matches[2]; diff --git a/Tests/RequestTest.php b/Tests/RequestTest.php index f1aa0ebea..a2eace70e 100644 --- a/Tests/RequestTest.php +++ b/Tests/RequestTest.php @@ -2402,6 +2402,8 @@ public static function protocolVersionProvider() 'trusted with via and protocol name' => ['HTTP/2.0', true, 'HTTP/1.0 fred, HTTP/1.1 nowhere.com (Apache/1.1)', 'HTTP/1.0'], 'trusted with broken via' => ['HTTP/2.0', true, 'HTTP/1^0 foo', 'HTTP/2.0'], 'trusted with partially-broken via' => ['HTTP/2.0', true, '1.0 fred, foo', 'HTTP/1.0'], + 'trusted with simple via' => ['HTTP/2.0', true, 'HTTP/1.0', 'HTTP/1.0'], + 'trusted with only version via' => ['HTTP/2.0', true, '1.0', 'HTTP/1.0'], ]; }