mirror of
https://github.com/itflow-org/itflow
synced 2026-09-21 22:21:15 +00:00
Allow PHP-8.2 and up Compatibility instead of just PHP-8.4
This commit is contained in:
@@ -25,7 +25,7 @@ class_exists(AcceptHeaderItem::class);
|
||||
class AcceptHeader
|
||||
{
|
||||
/**
|
||||
* @var AcceptHeaderItem[]
|
||||
* @var array<string, AcceptHeaderItem>
|
||||
*/
|
||||
private array $items = [];
|
||||
|
||||
@@ -46,18 +46,15 @@ class AcceptHeader
|
||||
*/
|
||||
public static function fromString(?string $headerValue): self
|
||||
{
|
||||
$parts = HeaderUtils::split($headerValue ?? '', ',;=');
|
||||
$items = [];
|
||||
foreach (HeaderUtils::split($headerValue ?? '', ',;=') as $i => $parts) {
|
||||
$part = array_shift($parts);
|
||||
$item = new AcceptHeaderItem($part[0], HeaderUtils::combine($parts));
|
||||
|
||||
return new self(array_map(function ($subParts) {
|
||||
static $index = 0;
|
||||
$part = array_shift($subParts);
|
||||
$attributes = HeaderUtils::combine($subParts);
|
||||
$items[] = $item->setIndex($i);
|
||||
}
|
||||
|
||||
$item = new AcceptHeaderItem($part[0], $attributes);
|
||||
$item->setIndex($index++);
|
||||
|
||||
return $item;
|
||||
}, $parts));
|
||||
return new self($items);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,7 +70,9 @@ class AcceptHeader
|
||||
*/
|
||||
public function has(string $value): bool
|
||||
{
|
||||
return isset($this->items[$value]);
|
||||
$canonicalKey = $this->getCanonicalKey(AcceptHeaderItem::fromString($value));
|
||||
|
||||
return isset($this->items[$canonicalKey]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,7 +80,26 @@ class AcceptHeader
|
||||
*/
|
||||
public function get(string $value): ?AcceptHeaderItem
|
||||
{
|
||||
return $this->items[$value] ?? $this->items[explode('/', $value)[0].'/*'] ?? $this->items['*/*'] ?? $this->items['*'] ?? null;
|
||||
$queryItem = AcceptHeaderItem::fromString($value.';q=1');
|
||||
$canonicalKey = $this->getCanonicalKey($queryItem);
|
||||
|
||||
if (isset($this->items[$canonicalKey])) {
|
||||
return $this->items[$canonicalKey];
|
||||
}
|
||||
|
||||
// Collect and filter matching candidates
|
||||
if (!$candidates = array_filter($this->items, fn (AcceptHeaderItem $item) => $this->matches($item, $queryItem))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
usort(
|
||||
$candidates,
|
||||
fn ($a, $b) => $this->getSpecificity($b, $queryItem) <=> $this->getSpecificity($a, $queryItem) // Descending specificity
|
||||
?: $b->getQuality() <=> $a->getQuality() // Descending quality
|
||||
?: $a->getIndex() <=> $b->getIndex() // Ascending index (stability)
|
||||
);
|
||||
|
||||
return reset($candidates);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,7 +109,7 @@ class AcceptHeader
|
||||
*/
|
||||
public function add(AcceptHeaderItem $item): static
|
||||
{
|
||||
$this->items[$item->getValue()] = $item;
|
||||
$this->items[$this->getCanonicalKey($item)] = $item;
|
||||
$this->sorted = false;
|
||||
|
||||
return $this;
|
||||
@@ -114,7 +132,7 @@ class AcceptHeader
|
||||
*/
|
||||
public function filter(string $pattern): self
|
||||
{
|
||||
return new self(array_filter($this->items, fn (AcceptHeaderItem $item) => preg_match($pattern, $item->getValue())));
|
||||
return new self(array_filter($this->items, static fn ($item) => preg_match($pattern, $item->getValue())));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,18 +151,154 @@ class AcceptHeader
|
||||
private function sort(): void
|
||||
{
|
||||
if (!$this->sorted) {
|
||||
uasort($this->items, function (AcceptHeaderItem $a, AcceptHeaderItem $b) {
|
||||
$qA = $a->getQuality();
|
||||
$qB = $b->getQuality();
|
||||
|
||||
if ($qA === $qB) {
|
||||
return $a->getIndex() > $b->getIndex() ? 1 : -1;
|
||||
}
|
||||
|
||||
return $qA > $qB ? -1 : 1;
|
||||
});
|
||||
uasort($this->items, static fn ($a, $b) => $b->getQuality() <=> $a->getQuality() ?: $a->getIndex() <=> $b->getIndex());
|
||||
|
||||
$this->sorted = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the canonical key for storing/retrieving an item.
|
||||
*/
|
||||
private function getCanonicalKey(AcceptHeaderItem $item): string
|
||||
{
|
||||
$parts = [];
|
||||
|
||||
// Normalize and sort attributes for consistent key generation
|
||||
$attributes = $this->getMediaParams($item);
|
||||
ksort($attributes);
|
||||
|
||||
foreach ($attributes as $name => $value) {
|
||||
if (null === $value) {
|
||||
$parts[] = $name; // Flag parameter (e.g., "flowed")
|
||||
continue;
|
||||
}
|
||||
|
||||
// Quote values containing spaces, commas, semicolons, or equals per RFC 9110
|
||||
// This handles cases like 'format="value with space"' or similar.
|
||||
$quotedValue = \is_string($value) && preg_match('/[\s;,=]/', $value) ? '"'.addcslashes($value, '"\\').'"' : $value;
|
||||
|
||||
$parts[] = $name.'='.$quotedValue;
|
||||
}
|
||||
|
||||
return $item->getValue().($parts ? ';'.implode(';', $parts) : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given header item (range) matches a queried item (value).
|
||||
*
|
||||
* @param AcceptHeaderItem $rangeItem The item from the Accept header (e.g., text/*;format=flowed)
|
||||
* @param AcceptHeaderItem $queryItem The item being queried (e.g., text/plain;format=flowed;charset=utf-8)
|
||||
*/
|
||||
private function matches(AcceptHeaderItem $rangeItem, AcceptHeaderItem $queryItem): bool
|
||||
{
|
||||
$rangeValue = strtolower($rangeItem->getValue());
|
||||
$queryValue = strtolower($queryItem->getValue());
|
||||
|
||||
// Handle universal wildcard ranges
|
||||
if ('*' === $rangeValue || '*/*' === $rangeValue) {
|
||||
return $this->rangeParametersMatch($rangeItem, $queryItem);
|
||||
}
|
||||
|
||||
// Queries for '*' only match wildcard ranges (handled above)
|
||||
if ('*' === $queryValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure media vs. non-media consistency
|
||||
$isQueryMedia = str_contains($queryValue, '/');
|
||||
$isRangeMedia = str_contains($rangeValue, '/');
|
||||
|
||||
if ($isQueryMedia !== $isRangeMedia) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Non-media: exact match only (wildcards handled above)
|
||||
if (!$isQueryMedia) {
|
||||
return $rangeValue === $queryValue && $this->rangeParametersMatch($rangeItem, $queryItem);
|
||||
}
|
||||
|
||||
// Media type: type/subtype with wildcards
|
||||
[$queryType, $querySubtype] = explode('/', $queryValue, 2);
|
||||
[$rangeType, $rangeSubtype] = explode('/', $rangeValue, 2) + [1 => '*'];
|
||||
|
||||
if ('*' !== $rangeType && $rangeType !== $queryType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('*' !== $rangeSubtype && $rangeSubtype !== $querySubtype) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parameters must match
|
||||
return $this->rangeParametersMatch($rangeItem, $queryItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the parameters of a range item are satisfied by the query item.
|
||||
*
|
||||
* Parameters are case-insensitive; range params must be a subset of query params.
|
||||
*/
|
||||
private function rangeParametersMatch(AcceptHeaderItem $rangeItem, AcceptHeaderItem $queryItem): bool
|
||||
{
|
||||
$queryAttributes = $this->getMediaParams($queryItem);
|
||||
$rangeAttributes = $this->getMediaParams($rangeItem);
|
||||
|
||||
foreach ($rangeAttributes as $name => $rangeValue) {
|
||||
if (!\array_key_exists($name, $queryAttributes)) {
|
||||
return false; // Missing required param
|
||||
}
|
||||
|
||||
$queryValue = $queryAttributes[$name];
|
||||
|
||||
if (null === $rangeValue) {
|
||||
return null === $queryValue; // Both flags or neither
|
||||
}
|
||||
|
||||
if (null === $queryValue || strtolower($queryValue) !== strtolower($rangeValue)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates a specificity score for sorting: media precision + param count.
|
||||
*/
|
||||
private function getSpecificity(AcceptHeaderItem $item, AcceptHeaderItem $queryItem): int
|
||||
{
|
||||
$rangeValue = strtolower($item->getValue());
|
||||
$queryValue = strtolower($queryItem->getValue());
|
||||
|
||||
$paramCount = \count($this->getMediaParams($item));
|
||||
|
||||
$isQueryMedia = str_contains($queryValue, '/');
|
||||
$isRangeMedia = str_contains($rangeValue, '/');
|
||||
|
||||
if (!$isQueryMedia && !$isRangeMedia) {
|
||||
return ('*' !== $rangeValue ? 2000 : 1000) + $paramCount;
|
||||
}
|
||||
|
||||
[$rangeType, $rangeSubtype] = explode('/', $rangeValue, 2) + [1 => '*'];
|
||||
|
||||
$specificity = match (true) {
|
||||
'*' !== $rangeSubtype => 3000, // Exact subtype (text/plain)
|
||||
'*' !== $rangeType => 2000, // Type wildcard (text/*)
|
||||
default => 1000, // Full wildcard (*/* or *)
|
||||
};
|
||||
|
||||
return $specificity + $paramCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns normalized attributes: keys lowercased, excluding 'q'.
|
||||
*/
|
||||
private function getMediaParams(AcceptHeaderItem $item): array
|
||||
{
|
||||
$attributes = array_change_key_case($item->getAttributes(), \CASE_LOWER);
|
||||
unset($attributes['q']);
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,7 +268,7 @@ class BinaryFileResponse extends Response
|
||||
if ($start < 0 || $start > $end) {
|
||||
$this->setStatusCode(416);
|
||||
$this->headers->set('Content-Range', \sprintf('bytes */%s', $fileSize));
|
||||
} elseif ($end - $start < $fileSize - 1) {
|
||||
} else {
|
||||
$this->maxlen = $end < $fileSize ? $end - $start + 1 : -1;
|
||||
$this->offset = $start;
|
||||
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
7.4
|
||||
---
|
||||
|
||||
* Add `#[WithHttpStatus]` to define status codes: 404 for `SignedUriException` and 403 for `ExpiredSignedUriException`
|
||||
* Add support for the `QUERY` HTTP method
|
||||
* Add support for structured MIME suffix
|
||||
* Add `Request::set/getAllowedHttpMethodOverride()` to list which HTTP methods can be overridden
|
||||
* Deprecate using `Request::sendHeaders()` after headers have already been sent; use a `StreamedResponse` instead
|
||||
* Deprecate method `Request::get()`, use properties `->attributes`, `query` or `request` directly instead
|
||||
* Make `Request::createFromGlobals()` parse the body of PUT, DELETE, PATCH and QUERY requests
|
||||
* Deprecate HTTP method override for methods GET, HEAD, CONNECT and TRACE; it will be ignored in Symfony 8.0
|
||||
* Deprecate accepting null `$format` argument to `Request::setFormat()`
|
||||
|
||||
7.3
|
||||
---
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ class EventStreamResponse extends StreamedResponse
|
||||
'Cache-Control' => 'private, no-cache, no-store, must-revalidate, max-age=0',
|
||||
'X-Accel-Buffering' => 'no',
|
||||
'Pragma' => 'no-cache',
|
||||
'Expire' => '0',
|
||||
'Expires' => '0',
|
||||
];
|
||||
|
||||
parent::__construct($callback, $status, $headers);
|
||||
|
||||
@@ -11,9 +11,12 @@
|
||||
|
||||
namespace Symfony\Component\HttpFoundation\Exception;
|
||||
|
||||
use Symfony\Component\HttpKernel\Attribute\WithHttpStatus;
|
||||
|
||||
/**
|
||||
* @author Kevin Bond <kevinbond@gmail.com>
|
||||
*/
|
||||
#[WithHttpStatus(403)]
|
||||
final class ExpiredSignedUriException extends SignedUriException
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -11,9 +11,12 @@
|
||||
|
||||
namespace Symfony\Component\HttpFoundation\Exception;
|
||||
|
||||
use Symfony\Component\HttpKernel\Attribute\WithHttpStatus;
|
||||
|
||||
/**
|
||||
* @author Kevin Bond <kevinbond@gmail.com>
|
||||
*/
|
||||
#[WithHttpStatus(404)]
|
||||
abstract class SignedUriException extends \RuntimeException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ class File extends \SplFileInfo
|
||||
{
|
||||
$target = $this->getTargetFile($directory, $name);
|
||||
|
||||
set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; });
|
||||
set_error_handler(static function ($type, $msg) use (&$error) { $error = $msg; });
|
||||
try {
|
||||
$renamed = rename($this->getPathname(), $target);
|
||||
} finally {
|
||||
@@ -96,7 +96,7 @@ class File extends \SplFileInfo
|
||||
throw new FileException(\sprintf('Could not move the file "%s" to "%s" (%s).', $this->getPathname(), $target, strip_tags($error)));
|
||||
}
|
||||
|
||||
@chmod($target, 0666 & ~umask());
|
||||
@chmod($target, 0o666 & ~umask());
|
||||
|
||||
return $target;
|
||||
}
|
||||
@@ -114,10 +114,11 @@ class File extends \SplFileInfo
|
||||
|
||||
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));
|
||||
if (!is_dir($directory) && !@mkdir($directory, 0o777, true) && !is_dir($directory)) {
|
||||
if (is_file($directory)) {
|
||||
throw new FileException(\sprintf('Unable to create the "%s" directory: a similarly-named file exists.', $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));
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@ class UploadedFile extends File
|
||||
|
||||
$target = $this->getTargetFile($directory, $name);
|
||||
|
||||
set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; });
|
||||
set_error_handler(static function ($type, $msg) use (&$error) { $error = $msg; });
|
||||
try {
|
||||
$moved = move_uploaded_file($this->getPathname(), $target);
|
||||
} finally {
|
||||
@@ -197,29 +197,37 @@ class UploadedFile extends File
|
||||
throw new FileException(\sprintf('Could not move the file "%s" to "%s" (%s).', $this->getPathname(), $target, strip_tags($error)));
|
||||
}
|
||||
|
||||
@chmod($target, 0666 & ~umask());
|
||||
@chmod($target, 0o666 & ~umask());
|
||||
|
||||
return $target;
|
||||
}
|
||||
|
||||
switch ($this->error) {
|
||||
case \UPLOAD_ERR_INI_SIZE:
|
||||
throw new IniSizeFileException($this->getErrorMessage());
|
||||
throw new IniSizeFileException($this->getExceptionMessage());
|
||||
case \UPLOAD_ERR_FORM_SIZE:
|
||||
throw new FormSizeFileException($this->getErrorMessage());
|
||||
throw new FormSizeFileException($this->getExceptionMessage());
|
||||
case \UPLOAD_ERR_PARTIAL:
|
||||
throw new PartialFileException($this->getErrorMessage());
|
||||
throw new PartialFileException($this->getExceptionMessage());
|
||||
case \UPLOAD_ERR_NO_FILE:
|
||||
throw new NoFileException($this->getErrorMessage());
|
||||
throw new NoFileException($this->getExceptionMessage());
|
||||
case \UPLOAD_ERR_CANT_WRITE:
|
||||
throw new CannotWriteFileException($this->getErrorMessage());
|
||||
throw new CannotWriteFileException($this->getExceptionMessage());
|
||||
case \UPLOAD_ERR_NO_TMP_DIR:
|
||||
throw new NoTmpDirFileException($this->getErrorMessage());
|
||||
throw new NoTmpDirFileException($this->getExceptionMessage());
|
||||
case \UPLOAD_ERR_EXTENSION:
|
||||
throw new ExtensionFileException($this->getErrorMessage());
|
||||
throw new ExtensionFileException($this->getExceptionMessage());
|
||||
}
|
||||
|
||||
throw new FileException($this->getErrorMessage());
|
||||
throw new FileException($this->getExceptionMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a user-friendly error message for file upload issues, if any.
|
||||
*/
|
||||
public function getErrorMessage(): string
|
||||
{
|
||||
return \UPLOAD_ERR_OK !== $this->error ? $this->getExceptionMessage() : '';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -268,7 +276,7 @@ class UploadedFile extends File
|
||||
/**
|
||||
* Returns an informative upload error message.
|
||||
*/
|
||||
public function getErrorMessage(): string
|
||||
private function getExceptionMessage(): string
|
||||
{
|
||||
static $errors = [
|
||||
\UPLOAD_ERR_INI_SIZE => 'The file "%s" exceeds your upload_max_filesize ini directive (limit is %d KiB).',
|
||||
|
||||
@@ -164,7 +164,7 @@ class HeaderUtils
|
||||
*/
|
||||
public static function makeDisposition(string $disposition, string $filename, string $filenameFallback = ''): string
|
||||
{
|
||||
if (!\in_array($disposition, [self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE])) {
|
||||
if (!\in_array($disposition, [self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE], true)) {
|
||||
throw new \InvalidArgumentException(\sprintf('The disposition must be either "%s" or "%s".', self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE));
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ use Symfony\Component\HttpFoundation\Exception\UnexpectedValueException;
|
||||
/**
|
||||
* InputBag is a container for user input values such as $_GET, $_POST, $_REQUEST, and $_COOKIE.
|
||||
*
|
||||
* @template TInput of string|int|float|bool|null
|
||||
*
|
||||
* @author Saif Eddin Gmati <azjezz@protonmail.com>
|
||||
*/
|
||||
final class InputBag extends ParameterBag
|
||||
@@ -24,7 +26,11 @@ 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
|
||||
* @template TDefault of string|int|float|bool|null
|
||||
*
|
||||
* @param TDefault $default The default value if the input key does not exist
|
||||
*
|
||||
* @return TDefault|TInput
|
||||
*
|
||||
* @throws BadRequestException if the input contains a non-scalar value
|
||||
*/
|
||||
|
||||
@@ -29,8 +29,13 @@ class IpUtils
|
||||
'::1/128', // Loopback
|
||||
'fc00::/7', // Unique Local Address
|
||||
'fe80::/10', // Link Local Address
|
||||
'::ffff:0:0/96', // IPv4 translations
|
||||
'::ffff:0:0/96', // IPv4-mapped IPv6 addresses (RFC 4291 section 2.5.5.2)
|
||||
'::/128', // Unspecified address
|
||||
'::/96', // IPv4-compatible IPv6 addresses (RFC 4291 section 2.5.5.1)
|
||||
'2002::/16', // 6to4 (RFC 3056)
|
||||
'2001::/32', // Teredo tunneling (RFC 4380)
|
||||
'64:ff9b::/96', // NAT64 well-known prefix (RFC 6052)
|
||||
'64:ff9b:1::/48', // NAT64 local-use prefix (RFC 8215)
|
||||
];
|
||||
|
||||
private static array $checkedIps = [];
|
||||
@@ -212,7 +217,7 @@ class IpUtils
|
||||
$ip = substr($ip, 1, -1);
|
||||
}
|
||||
|
||||
$mappedIpV4MaskGenerator = function (string $mask, int $bytesToAnonymize) {
|
||||
$mappedIpV4MaskGenerator = static function (string $mask, int $bytesToAnonymize) {
|
||||
$mask .= str_repeat('ff', 4 - $bytesToAnonymize);
|
||||
$mask .= str_repeat('00', $bytesToAnonymize);
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@ use Symfony\Component\HttpFoundation\Exception\UnexpectedValueException;
|
||||
*/
|
||||
class ParameterBag implements \IteratorAggregate, \Countable
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $parameters
|
||||
*/
|
||||
public function __construct(
|
||||
protected array $parameters = [],
|
||||
) {
|
||||
@@ -31,7 +34,11 @@ class ParameterBag implements \IteratorAggregate, \Countable
|
||||
/**
|
||||
* Returns the parameters.
|
||||
*
|
||||
* @param string|null $key The name of the parameter to return or null to get them all
|
||||
* @template TKey of string|null
|
||||
*
|
||||
* @param TKey $key The name of the parameter to return or null to get them all
|
||||
*
|
||||
* @return (TKey is null ? array<string, mixed> : array<mixed>)
|
||||
*
|
||||
* @throws BadRequestException if the value is not an array
|
||||
*/
|
||||
@@ -50,6 +57,8 @@ class ParameterBag implements \IteratorAggregate, \Countable
|
||||
|
||||
/**
|
||||
* Returns the parameter keys.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public function keys(): array
|
||||
{
|
||||
@@ -58,6 +67,8 @@ class ParameterBag implements \IteratorAggregate, \Countable
|
||||
|
||||
/**
|
||||
* Replaces the current parameters by a new set.
|
||||
*
|
||||
* @param array<string, mixed> $parameters
|
||||
*/
|
||||
public function replace(array $parameters = []): void
|
||||
{
|
||||
@@ -66,6 +77,8 @@ class ParameterBag implements \IteratorAggregate, \Countable
|
||||
|
||||
/**
|
||||
* Adds parameters.
|
||||
*
|
||||
* @param array<string, mixed> $parameters
|
||||
*/
|
||||
public function add(array $parameters = []): void
|
||||
{
|
||||
@@ -188,7 +201,7 @@ class ParameterBag implements \IteratorAggregate, \Countable
|
||||
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: ', $key).$e->getMessage().'.', $e->getCode(), $e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
205
plugins/vendor/symfony/http-foundation/Request.php
vendored
205
plugins/vendor/symfony/http-foundation/Request.php
vendored
@@ -62,6 +62,7 @@ class Request
|
||||
public const METHOD_OPTIONS = 'OPTIONS';
|
||||
public const METHOD_TRACE = 'TRACE';
|
||||
public const METHOD_CONNECT = 'CONNECT';
|
||||
public const METHOD_QUERY = 'QUERY';
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
@@ -80,6 +81,13 @@ class Request
|
||||
|
||||
protected static bool $httpMethodParameterOverride = false;
|
||||
|
||||
/**
|
||||
* The HTTP methods that can be overridden.
|
||||
*
|
||||
* @var uppercase-string[]|null
|
||||
*/
|
||||
protected static ?array $allowedHttpMethodOverride = null;
|
||||
|
||||
/**
|
||||
* Custom parameters.
|
||||
*/
|
||||
@@ -94,6 +102,8 @@ class Request
|
||||
|
||||
/**
|
||||
* Query string parameters ($_GET).
|
||||
*
|
||||
* @var InputBag<string>
|
||||
*/
|
||||
public InputBag $query;
|
||||
|
||||
@@ -109,6 +119,8 @@ class Request
|
||||
|
||||
/**
|
||||
* Cookies ($_COOKIE).
|
||||
*
|
||||
* @var InputBag<string>
|
||||
*/
|
||||
public InputBag $cookies;
|
||||
|
||||
@@ -194,6 +206,28 @@ class Request
|
||||
self::HEADER_X_FORWARDED_PREFIX => 'X_FORWARDED_PREFIX',
|
||||
];
|
||||
|
||||
/**
|
||||
* This mapping is used when no exact MIME match is found in $formats.
|
||||
*
|
||||
* It enables mappings like application/soap+xml -> xml.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc6839
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc7303
|
||||
* @see https://www.iana.org/assignments/media-types/media-types.xhtml
|
||||
*/
|
||||
private const STRUCTURED_SUFFIX_FORMATS = [
|
||||
'json' => 'json',
|
||||
'xml' => 'xml',
|
||||
'xhtml' => 'html',
|
||||
'cbor' => 'cbor',
|
||||
'zip' => 'zip',
|
||||
'ber' => 'asn1',
|
||||
'der' => 'asn1',
|
||||
'tlv' => 'tlv',
|
||||
'wbxml' => 'xml',
|
||||
'yaml' => 'yaml',
|
||||
];
|
||||
|
||||
private bool $isIisRewrite = false;
|
||||
|
||||
/**
|
||||
@@ -251,16 +285,30 @@ class Request
|
||||
*/
|
||||
public static function createFromGlobals(): static
|
||||
{
|
||||
$request = self::createRequestFromFactory($_GET, $_POST, [], $_COOKIE, $_FILES, $_SERVER);
|
||||
|
||||
if (str_starts_with($request->headers->get('CONTENT_TYPE', ''), 'application/x-www-form-urlencoded')
|
||||
&& \in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), ['PUT', 'DELETE', 'PATCH'], true)
|
||||
) {
|
||||
parse_str($request->getContent(), $data);
|
||||
$request->request = new InputBag($data);
|
||||
if (!\in_array($_SERVER['REQUEST_METHOD'] ?? null, ['PUT', 'DELETE', 'PATCH', 'QUERY'], true)) {
|
||||
return self::createRequestFromFactory($_GET, $_POST, [], $_COOKIE, $_FILES, $_SERVER);
|
||||
}
|
||||
|
||||
return $request;
|
||||
if (\PHP_VERSION_ID < 80400) {
|
||||
if (!isset($_SERVER['CONTENT_TYPE']) || str_starts_with($_SERVER['CONTENT_TYPE'], 'application/x-www-form-urlencoded')) {
|
||||
$content = file_get_contents('php://input');
|
||||
parse_str($content, $post);
|
||||
} else {
|
||||
$content = null;
|
||||
$post = $_POST;
|
||||
}
|
||||
|
||||
return self::createRequestFromFactory($_GET, $post, [], $_COOKIE, $_FILES, $_SERVER, $content);
|
||||
}
|
||||
|
||||
try {
|
||||
[$post, $files] = request_parse_body();
|
||||
} catch (\RequestParseBodyException) {
|
||||
$post = $_POST;
|
||||
$files = $_FILES;
|
||||
}
|
||||
|
||||
return self::createRequestFromFactory($_GET, $post, [], $_COOKIE, $files, $_SERVER);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -353,14 +401,23 @@ class Request
|
||||
$server['PHP_AUTH_PW'] = $components['pass'];
|
||||
}
|
||||
|
||||
if (!isset($components['path'])) {
|
||||
if ('' === $path = $components['path'] ?? '') {
|
||||
$components['path'] = '/';
|
||||
} elseif (!isset($components['scheme']) && !isset($components['host']) && '/' !== $path[0]) {
|
||||
if (false !== $pos = strpos($path, '/')) {
|
||||
$path = substr($path, 0, $pos);
|
||||
}
|
||||
|
||||
if (str_contains($path, ':')) {
|
||||
throw new BadRequestException('Invalid URI: Path is malformed.');
|
||||
}
|
||||
}
|
||||
|
||||
switch (strtoupper($method)) {
|
||||
case 'POST':
|
||||
case 'PUT':
|
||||
case 'DELETE':
|
||||
case 'QUERY':
|
||||
if (!isset($server['CONTENT_TYPE'])) {
|
||||
$server['CONTENT_TYPE'] = 'application/x-www-form-urlencoded';
|
||||
}
|
||||
@@ -451,8 +508,8 @@ class Request
|
||||
$dup->method = null;
|
||||
$dup->format = null;
|
||||
|
||||
if (!$dup->get('_format') && $this->get('_format')) {
|
||||
$dup->attributes->set('_format', $this->get('_format'));
|
||||
if (!$dup->attributes->has('_format') && $this->attributes->has('_format')) {
|
||||
$dup->attributes->set('_format', $this->attributes->get('_format'));
|
||||
}
|
||||
|
||||
if (!$dup->getRequestFormat(null)) {
|
||||
@@ -596,7 +653,7 @@ class Request
|
||||
*/
|
||||
public static function setTrustedHosts(array $hostPatterns): void
|
||||
{
|
||||
self::$trustedHostPatterns = array_map(fn ($hostPattern) => \sprintf('{%s}i', $hostPattern), $hostPatterns);
|
||||
self::$trustedHostPatterns = array_map(static fn ($hostPattern) => \sprintf('{%s}i', $hostPattern), $hostPatterns);
|
||||
// we need to reset trusted hosts on trusted host patterns change
|
||||
self::$trustedHosts = [];
|
||||
}
|
||||
@@ -653,6 +710,34 @@ class Request
|
||||
return self::$httpMethodParameterOverride;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the list of HTTP methods that can be overridden.
|
||||
*
|
||||
* Set to null to allow all methods to be overridden (default). Set to an
|
||||
* empty array to disallow overrides entirely. Otherwise, provide the list
|
||||
* of uppercased method names that are allowed.
|
||||
*
|
||||
* @param uppercase-string[]|null $methods
|
||||
*/
|
||||
public static function setAllowedHttpMethodOverride(?array $methods): void
|
||||
{
|
||||
if (array_intersect($methods ?? [], ['GET', 'HEAD', 'CONNECT', 'TRACE'])) {
|
||||
throw new \InvalidArgumentException('The HTTP methods "GET", "HEAD", "CONNECT", and "TRACE" cannot be overridden.');
|
||||
}
|
||||
|
||||
self::$allowedHttpMethodOverride = $methods;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of HTTP methods that can be overridden.
|
||||
*
|
||||
* @return uppercase-string[]|null
|
||||
*/
|
||||
public static function getAllowedHttpMethodOverride(): ?array
|
||||
{
|
||||
return self::$allowedHttpMethodOverride;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a "parameter" value from any bag.
|
||||
*
|
||||
@@ -662,10 +747,12 @@ class Request
|
||||
*
|
||||
* Order of precedence: PATH (routing placeholders or custom attributes), GET, POST
|
||||
*
|
||||
* @internal use explicit input sources instead
|
||||
* @deprecated since Symfony 7.4, use properties `->attributes`, `query` or `request` directly instead
|
||||
*/
|
||||
public function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
trigger_deprecation('symfony/http-foundation', '7.4', 'Request::get() is deprecated, use properties ->attributes, query or request directly instead.');
|
||||
|
||||
if ($this !== $result = $this->attributes->get($key, $this)) {
|
||||
return $result;
|
||||
}
|
||||
@@ -770,10 +857,6 @@ class Request
|
||||
* being the original client, and each successive proxy that passed the request
|
||||
* adding the IP address where it received the request from.
|
||||
*
|
||||
* If your reverse proxy uses a different header name than "X-Forwarded-For",
|
||||
* ("Client-Ip" for instance), configure it via the $trustedHeaderSet
|
||||
* argument of the Request::setTrustedProxies() method instead.
|
||||
*
|
||||
* @see getClientIps()
|
||||
* @see https://wikipedia.org/wiki/X-Forwarded-For
|
||||
*/
|
||||
@@ -797,7 +880,7 @@ class Request
|
||||
*
|
||||
* Suppose this request is instantiated from /mysite on localhost:
|
||||
*
|
||||
* * http://localhost/mysite returns an empty string
|
||||
* * http://localhost/mysite returns '/'
|
||||
* * http://localhost/mysite/about returns '/about'
|
||||
* * http://localhost/mysite/enco%20ded returns '/enco%20ded'
|
||||
* * http://localhost/mysite/about?var=1 returns '/about'
|
||||
@@ -1075,7 +1158,7 @@ class Request
|
||||
|
||||
$https = $this->server->get('HTTPS');
|
||||
|
||||
return $https && 'off' !== strtolower($https);
|
||||
return $https && (!\is_string($https) || 'off' !== strtolower($https));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1092,10 +1175,8 @@ class Request
|
||||
{
|
||||
if ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_X_FORWARDED_HOST)) {
|
||||
$host = $host[0];
|
||||
} elseif (!$host = $this->headers->get('HOST')) {
|
||||
if (!$host = $this->server->get('SERVER_NAME')) {
|
||||
$host = $this->server->get('SERVER_ADDR', '');
|
||||
}
|
||||
} else {
|
||||
$host = $this->headers->get('HOST') ?: $this->server->get('SERVER_NAME') ?: $this->server->get('SERVER_ADDR', '');
|
||||
}
|
||||
|
||||
// trim and remove port number from host
|
||||
@@ -1168,7 +1249,7 @@ class Request
|
||||
|
||||
$this->method = strtoupper($this->server->get('REQUEST_METHOD', 'GET'));
|
||||
|
||||
if ('POST' !== $this->method) {
|
||||
if ('POST' !== $this->method || !(self::$allowedHttpMethodOverride ?? true)) {
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
@@ -1184,11 +1265,15 @@ class Request
|
||||
|
||||
$method = strtoupper($method);
|
||||
|
||||
if (\in_array($method, ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'PATCH', 'PURGE', 'TRACE'], true)) {
|
||||
return $this->method = $method;
|
||||
if (\in_array($method, ['GET', 'HEAD', 'CONNECT', 'TRACE'], true)) {
|
||||
trigger_deprecation('symfony/http-foundation', '7.4', 'HTTP method override is deprecated for methods GET, HEAD, CONNECT and TRACE; it will be ignored in Symfony 8.0.', $method);
|
||||
}
|
||||
|
||||
if (!preg_match('/^[A-Z]++$/D', $method)) {
|
||||
if (self::$allowedHttpMethodOverride && !\in_array($method, self::$allowedHttpMethodOverride, true)) {
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
if (\strlen($method) !== strspn($method, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')) {
|
||||
throw new SuspiciousOperationException('Invalid HTTP method override.');
|
||||
}
|
||||
|
||||
@@ -1233,9 +1318,22 @@ class Request
|
||||
|
||||
/**
|
||||
* Gets the format associated with the mime type.
|
||||
*
|
||||
* Resolution order:
|
||||
* 1) Exact match on the full MIME type (e.g. "application/json").
|
||||
* 2) Match on the canonical MIME type (i.e. before the first ";" parameter).
|
||||
* 3) If the type is "application/*+suffix", use the structured syntax suffix
|
||||
* mapping (e.g. "application/foo+json" → "json"), when available.
|
||||
* 4) If $subtypeFallback is true and no match was found:
|
||||
* - return the MIME subtype (without "x-" prefix), provided it does not
|
||||
* contain a "+" (e.g. "application/x-yaml" → "yaml", "text/csv" → "csv").
|
||||
*
|
||||
* @param string|null $mimeType The mime type to check
|
||||
* @param bool $subtypeFallback Whether to fall back to the subtype if no exact match is found
|
||||
*/
|
||||
public function getFormat(?string $mimeType): ?string
|
||||
public function getFormat(?string $mimeType/* , bool $subtypeFallback = false */): ?string
|
||||
{
|
||||
$subtypeFallback = 2 <= \func_num_args() ? func_get_arg(1) : false;
|
||||
$canonicalMimeType = null;
|
||||
if ($mimeType && false !== $pos = strpos($mimeType, ';')) {
|
||||
$canonicalMimeType = trim(substr($mimeType, 0, $pos));
|
||||
@@ -1261,21 +1359,48 @@ class Request
|
||||
return $format;
|
||||
}
|
||||
|
||||
if (!$canonicalMimeType ??= $mimeType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (str_starts_with($canonicalMimeType, 'application/') && str_contains($canonicalMimeType, '+')) {
|
||||
$suffix = substr(strrchr($canonicalMimeType, '+'), 1);
|
||||
if (isset(self::STRUCTURED_SUFFIX_FORMATS[$suffix])) {
|
||||
return self::STRUCTURED_SUFFIX_FORMATS[$suffix];
|
||||
}
|
||||
}
|
||||
|
||||
if ($subtypeFallback && str_contains($canonicalMimeType, '/')) {
|
||||
[, $subtype] = explode('/', $canonicalMimeType, 2);
|
||||
if (str_starts_with($subtype, 'x-')) {
|
||||
$subtype = substr($subtype, 2);
|
||||
}
|
||||
if (!str_contains($subtype, '+')) {
|
||||
return $subtype;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Associates a format with mime types.
|
||||
*
|
||||
* @param string $format The format to set
|
||||
* @param string|string[] $mimeTypes The associated mime types (the preferred one must be the first as it will be used as the content type)
|
||||
*/
|
||||
public function setFormat(?string $format, string|array $mimeTypes): void
|
||||
{
|
||||
if (null === $format) {
|
||||
trigger_deprecation('symfony/http-foundation', '7.4', 'Passing "null" as the first argument of "%s()" is deprecated. The argument will be non-nullable in Symfony 8.0.', __METHOD__);
|
||||
$format = '';
|
||||
}
|
||||
|
||||
if (null === static::$formats) {
|
||||
static::initializeFormats();
|
||||
}
|
||||
|
||||
static::$formats[$format ?? ''] = (array) $mimeTypes;
|
||||
static::$formats[$format] = (array) $mimeTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1367,7 +1492,7 @@ class Request
|
||||
*/
|
||||
public function isMethodSafe(): bool
|
||||
{
|
||||
return \in_array($this->getMethod(), ['GET', 'HEAD', 'OPTIONS', 'TRACE']);
|
||||
return \in_array($this->getMethod(), ['GET', 'HEAD', 'OPTIONS', 'TRACE', 'QUERY'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1375,7 +1500,7 @@ class Request
|
||||
*/
|
||||
public function isMethodIdempotent(): bool
|
||||
{
|
||||
return \in_array($this->getMethod(), ['HEAD', 'GET', 'PUT', 'DELETE', 'TRACE', 'OPTIONS', 'PURGE']);
|
||||
return \in_array($this->getMethod(), ['HEAD', 'GET', 'PUT', 'DELETE', 'TRACE', 'OPTIONS', 'PURGE', 'QUERY'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1385,7 +1510,7 @@ class Request
|
||||
*/
|
||||
public function isMethodCacheable(): bool
|
||||
{
|
||||
return \in_array($this->getMethod(), ['GET', 'HEAD']);
|
||||
return \in_array($this->getMethod(), ['GET', 'HEAD', 'QUERY'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1421,10 +1546,8 @@ class Request
|
||||
*/
|
||||
public function getContent(bool $asResource = false)
|
||||
{
|
||||
$currentContentIsResource = \is_resource($this->content);
|
||||
|
||||
if (true === $asResource) {
|
||||
if ($currentContentIsResource) {
|
||||
if ($asResource) {
|
||||
if (\is_resource($this->content)) {
|
||||
rewind($this->content);
|
||||
|
||||
return $this->content;
|
||||
@@ -1444,7 +1567,7 @@ class Request
|
||||
return fopen('php://input', 'r');
|
||||
}
|
||||
|
||||
if ($currentContentIsResource) {
|
||||
if (\is_resource($this->content)) {
|
||||
rewind($this->content);
|
||||
|
||||
return stream_get_contents($this->content);
|
||||
@@ -1932,6 +2055,14 @@ class Request
|
||||
'atom' => ['application/atom+xml'],
|
||||
'rss' => ['application/rss+xml'],
|
||||
'form' => ['application/x-www-form-urlencoded', 'multipart/form-data'],
|
||||
'soap' => ['application/soap+xml'],
|
||||
'problem' => ['application/problem+json'],
|
||||
'hal' => ['application/hal+json', 'application/hal+xml'],
|
||||
'jsonapi' => ['application/vnd.api+json'],
|
||||
'yaml' => ['text/yaml', 'application/x-yaml'],
|
||||
'wbxml' => ['application/vnd.wap.wbxml'],
|
||||
'pdf' => ['application/pdf'],
|
||||
'csv' => ['text/csv'],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -261,7 +261,7 @@ class Response
|
||||
}
|
||||
|
||||
// Fix Content-Type
|
||||
$charset = $this->charset ?: 'UTF-8';
|
||||
$charset = $this->charset ?: 'utf-8';
|
||||
if (!$headers->has('Content-Type')) {
|
||||
$headers->set('Content-Type', 'text/html; charset='.$charset);
|
||||
} elseif (0 === stripos($headers->get('Content-Type') ?? '', 'text/') && false === stripos($headers->get('Content-Type') ?? '', 'charset')) {
|
||||
@@ -317,6 +317,12 @@ class Response
|
||||
{
|
||||
// headers have already been sent by the developer
|
||||
if (headers_sent()) {
|
||||
if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
|
||||
$statusCode ??= $this->statusCode;
|
||||
trigger_deprecation('symfony/http-foundation', '7.4', 'Trying to use "%s::sendHeaders()" after headers have already been sent is deprecated and will throw a PHP warning in 8.0. Use a "StreamedResponse" instead.', static::class);
|
||||
// header(\sprintf('HTTP/%s %s %s', $this->version, $statusCode, $this->statusText), true, $statusCode);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -539,7 +545,7 @@ class Response
|
||||
*/
|
||||
public function isCacheable(): bool
|
||||
{
|
||||
if (!\in_array($this->statusCode, [200, 203, 300, 301, 302, 404, 410])) {
|
||||
if (!\in_array($this->statusCode, [200, 203, 300, 301, 302, 404, 410], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1248,7 +1254,7 @@ class Response
|
||||
*/
|
||||
public function isRedirect(?string $location = null): bool
|
||||
{
|
||||
return \in_array($this->statusCode, [201, 301, 302, 303, 307, 308]) && (null === $location ?: $location == $this->headers->get('Location'));
|
||||
return \in_array($this->statusCode, [201, 301, 302, 303, 307, 308], true) && (null === $location ?: $location == $this->headers->get('Location'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1258,7 +1264,7 @@ class Response
|
||||
*/
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return \in_array($this->statusCode, [204, 304]);
|
||||
return \in_array($this->statusCode, [204, 304], true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -194,7 +194,7 @@ class ResponseHeaderBag extends HeaderBag
|
||||
*/
|
||||
public function getCookies(string $format = self::COOKIES_FLAT): array
|
||||
{
|
||||
if (!\in_array($format, [self::COOKIES_FLAT, self::COOKIES_ARRAY])) {
|
||||
if (!\in_array($format, [self::COOKIES_FLAT, self::COOKIES_ARRAY], true)) {
|
||||
throw new \InvalidArgumentException(\sprintf('Format "%s" invalid (%s).', $format, implode(', ', [self::COOKIES_FLAT, self::COOKIES_ARRAY])));
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,16 @@ abstract class AbstractSessionHandler implements \SessionHandlerInterface, \Sess
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool
|
||||
{
|
||||
$this->igbinaryEmptyData ??= \function_exists('igbinary_serialize') ? igbinary_serialize([]) : '';
|
||||
if ('' === $data || $this->igbinaryEmptyData === $data) {
|
||||
return $this->destroy($sessionId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function write(#[\SensitiveParameter] string $sessionId, string $data): bool
|
||||
{
|
||||
// see https://github.com/igbinary/igbinary/issues/146
|
||||
|
||||
@@ -41,7 +41,7 @@ class NativeFileSessionHandler extends \SessionHandler
|
||||
$baseDir = ltrim(strrchr($savePath, ';'), ';');
|
||||
}
|
||||
|
||||
if ($baseDir && !is_dir($baseDir) && !@mkdir($baseDir, 0777, true) && !is_dir($baseDir)) {
|
||||
if ($baseDir && !is_dir($baseDir) && !@mkdir($baseDir, 0o777, true) && !is_dir($baseDir)) {
|
||||
throw new \RuntimeException(\sprintf('Session Storage was not able to create directory "%s".', $baseDir));
|
||||
}
|
||||
|
||||
|
||||
@@ -197,7 +197,6 @@ class PdoSessionHandler extends AbstractSessionHandler
|
||||
$table->addColumn($this->dataCol, Types::BLOB)->setNotnull(true);
|
||||
$table->addColumn($this->lifetimeCol, Types::INTEGER)->setUnsigned(true)->setNotnull(true);
|
||||
$table->addColumn($this->timeCol, Types::INTEGER)->setUnsigned(true)->setNotnull(true);
|
||||
$table->addOption('collate', 'utf8mb4_bin');
|
||||
$table->addOption('engine', 'InnoDB');
|
||||
break;
|
||||
case 'sqlite':
|
||||
@@ -259,7 +258,7 @@ class PdoSessionHandler extends AbstractSessionHandler
|
||||
// - trailing space removal
|
||||
// - case-insensitivity
|
||||
// - language processing like é == e
|
||||
'mysql' => "CREATE TABLE $this->table ($this->idCol VARBINARY(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER UNSIGNED NOT NULL, $this->timeCol INTEGER UNSIGNED NOT NULL) COLLATE utf8mb4_bin, ENGINE = InnoDB",
|
||||
'mysql' => "CREATE TABLE $this->table ($this->idCol VARBINARY(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER UNSIGNED NOT NULL, $this->timeCol INTEGER UNSIGNED NOT NULL) ENGINE = InnoDB",
|
||||
'sqlite' => "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)",
|
||||
'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)",
|
||||
@@ -802,6 +801,12 @@ class PdoSessionHandler extends AbstractSessionHandler
|
||||
rewind($data);
|
||||
$sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, EMPTY_BLOB(), :expiry, :time) RETURNING $this->dataCol into :data";
|
||||
break;
|
||||
case 'sqlsrv':
|
||||
$data = fopen('php://memory', 'r+');
|
||||
fwrite($data, $sessionData);
|
||||
rewind($data);
|
||||
$sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :expiry, :time)";
|
||||
break;
|
||||
default:
|
||||
$data = $sessionData;
|
||||
$sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :expiry, :time)";
|
||||
@@ -829,6 +834,12 @@ class PdoSessionHandler extends AbstractSessionHandler
|
||||
rewind($data);
|
||||
$sql = "UPDATE $this->table SET $this->dataCol = EMPTY_BLOB(), $this->lifetimeCol = :expiry, $this->timeCol = :time WHERE $this->idCol = :id RETURNING $this->dataCol into :data";
|
||||
break;
|
||||
case 'sqlsrv':
|
||||
$data = fopen('php://memory', 'r+');
|
||||
fwrite($data, $sessionData);
|
||||
rewind($data);
|
||||
$sql = "UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :expiry, $this->timeCol = :time WHERE $this->idCol = :id";
|
||||
break;
|
||||
default:
|
||||
$data = $sessionData;
|
||||
$sql = "UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :expiry, $this->timeCol = :time WHERE $this->idCol = :id";
|
||||
@@ -876,12 +887,16 @@ class PdoSessionHandler extends AbstractSessionHandler
|
||||
$mergeStmt = $this->pdo->prepare($mergeSql);
|
||||
|
||||
if ('sqlsrv' === $this->driver) {
|
||||
$dataStream = fopen('php://memory', 'r+');
|
||||
fwrite($dataStream, $data);
|
||||
rewind($dataStream);
|
||||
|
||||
$mergeStmt->bindParam(1, $sessionId, \PDO::PARAM_STR);
|
||||
$mergeStmt->bindParam(2, $sessionId, \PDO::PARAM_STR);
|
||||
$mergeStmt->bindParam(3, $data, \PDO::PARAM_LOB);
|
||||
$mergeStmt->bindParam(3, $dataStream, \PDO::PARAM_LOB);
|
||||
$mergeStmt->bindValue(4, time() + $maxlifetime, \PDO::PARAM_INT);
|
||||
$mergeStmt->bindValue(5, time(), \PDO::PARAM_INT);
|
||||
$mergeStmt->bindParam(6, $data, \PDO::PARAM_LOB);
|
||||
$mergeStmt->bindParam(6, $dataStream, \PDO::PARAM_LOB);
|
||||
$mergeStmt->bindValue(7, time() + $maxlifetime, \PDO::PARAM_INT);
|
||||
$mergeStmt->bindValue(8, time(), \PDO::PARAM_INT);
|
||||
} else {
|
||||
|
||||
@@ -64,6 +64,7 @@ class SessionHandlerFactory
|
||||
throw new \InvalidArgumentException('Unsupported Redis or Memcached DSN. Try running "composer require symfony/cache".');
|
||||
}
|
||||
$handlerClass = str_starts_with($connection, 'memcached:') ? MemcachedSessionHandler::class : RedisSessionHandler::class;
|
||||
$connection = preg_replace('/([?&])prefix=[^&]*+&?/', '\1', $connection);
|
||||
$connection = AbstractAdapter::createConnection($connection, ['lazy' => true]);
|
||||
|
||||
return new $handlerClass($connection, array_intersect_key($options, ['prefix' => 1, 'ttl' => 1]));
|
||||
|
||||
@@ -32,12 +32,14 @@ class MetadataBag implements SessionBagInterface
|
||||
private int $lastUsed;
|
||||
|
||||
/**
|
||||
* @param string $storageKey The key used to store bag in the session
|
||||
* @param int $updateThreshold The time to wait between two UPDATED updates
|
||||
* @param string $storageKey The key used to store bag in the session
|
||||
* @param int $updateThreshold The time to wait between two UPDATED updates
|
||||
* @param int|null $cookieLifetime The configured cookie lifetime; null to read from php.ini
|
||||
*/
|
||||
public function __construct(
|
||||
private string $storageKey = '_sf2_meta',
|
||||
private int $updateThreshold = 0,
|
||||
private ?int $cookieLifetime = null,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -126,6 +128,6 @@ class MetadataBag implements SessionBagInterface
|
||||
{
|
||||
$timeStamp = time();
|
||||
$this->meta[self::CREATED] = $this->meta[self::UPDATED] = $this->lastUsed = $timeStamp;
|
||||
$this->meta[self::LIFETIME] = $lifetime ?? (int) \ini_get('session.cookie_lifetime');
|
||||
$this->meta[self::LIFETIME] = $lifetime ?? $this->cookieLifetime ?? (int) \ini_get('session.cookie_lifetime');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ class MockFileSessionStorage extends MockArraySessionStorage
|
||||
{
|
||||
$savePath ??= sys_get_temp_dir();
|
||||
|
||||
if (!is_dir($savePath) && !@mkdir($savePath, 0777, true) && !is_dir($savePath)) {
|
||||
if (!is_dir($savePath) && !@mkdir($savePath, 0o777, true) && !is_dir($savePath)) {
|
||||
throw new \RuntimeException(\sprintf('Session Storage was not able to create directory "%s".', $savePath));
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ class MockFileSessionStorage extends MockArraySessionStorage
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
// this is needed when the session object is re-used across multiple requests
|
||||
// this is needed when the session object is reused across multiple requests
|
||||
// in functional tests.
|
||||
$this->started = false;
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ class StreamedJsonResponse extends StreamedResponse
|
||||
{
|
||||
$generators = [];
|
||||
|
||||
array_walk_recursive($data, function (&$item, $key) use (&$generators) {
|
||||
array_walk_recursive($data, static function (&$item, $key) use (&$generators) {
|
||||
if (self::PLACEHOLDER === $key) {
|
||||
// if the placeholder is already in the structure it should be replaced with a new one that explode
|
||||
// works like expected for the structure
|
||||
|
||||
@@ -121,19 +121,12 @@ class UriSigner
|
||||
$uri = self::normalize($uri);
|
||||
$status = $this->doVerify($uri);
|
||||
|
||||
if (self::STATUS_VALID === $status) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self::STATUS_MISSING === $status) {
|
||||
throw new UnsignedUriException();
|
||||
}
|
||||
|
||||
if (self::STATUS_INVALID === $status) {
|
||||
throw new UnverifiedSignedUriException();
|
||||
}
|
||||
|
||||
throw new ExpiredSignedUriException();
|
||||
match ($status) {
|
||||
self::STATUS_VALID => null,
|
||||
self::STATUS_INVALID => throw new UnverifiedSignedUriException(),
|
||||
self::STATUS_EXPIRED => throw new ExpiredSignedUriException(),
|
||||
default => throw new UnsignedUriException(),
|
||||
};
|
||||
}
|
||||
|
||||
private function computeHash(string $uri): string
|
||||
|
||||
@@ -17,20 +17,19 @@
|
||||
],
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/deprecation-contracts": "^2.5|^3.0",
|
||||
"symfony/polyfill-mbstring": "~1.1",
|
||||
"symfony/polyfill-php83": "^1.27"
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/polyfill-mbstring": "^1.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"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",
|
||||
"symfony/expression-language": "^6.4|^7.0",
|
||||
"symfony/rate-limiter": "^6.4|^7.0"
|
||||
"symfony/cache": "^6.4.12|^7.1.5|^8.0",
|
||||
"symfony/clock": "^6.4|^7.0|^8.0",
|
||||
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
|
||||
"symfony/http-kernel": "^6.4|^7.0|^8.0",
|
||||
"symfony/mime": "^6.4|^7.0|^8.0",
|
||||
"symfony/expression-language": "^6.4|^7.0|^8.0",
|
||||
"symfony/rate-limiter": "^6.4|^7.0|^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"doctrine/dbal": "<3.6",
|
||||
|
||||
Reference in New Issue
Block a user