Upgrade framework

This commit is contained in:
2023-11-14 16:54:35 +01:00
parent 1648a5cd42
commit 4fcf6fffcc
10548 changed files with 693138 additions and 466698 deletions

View File

@@ -1,3 +0,0 @@
vendor/
composer.lock
phpunit.xml

View File

@@ -11,6 +11,9 @@
namespace Symfony\Component\HttpFoundation;
// Help opcache.preload discover always-needed symbols
class_exists(AcceptHeaderItem::class);
/**
* Represents an Accept-* header.
*
@@ -24,16 +27,11 @@ class AcceptHeader
/**
* @var AcceptHeaderItem[]
*/
private $items = array();
private array $items = [];
private bool $sorted = true;
/**
* @var bool
*/
private $sorted = true;
/**
* Constructor.
*
* @param AcceptHeaderItem[] $items
*/
public function __construct(array $items)
@@ -45,65 +43,54 @@ class AcceptHeader
/**
* Builds an AcceptHeader instance from a string.
*
* @param string $headerValue
*
* @return self
*/
public static function fromString($headerValue)
public static function fromString(?string $headerValue): self
{
$index = 0;
return new self(array_map(function ($itemValue) use (&$index) {
$item = AcceptHeaderItem::fromString($itemValue);
$parts = HeaderUtils::split($headerValue ?? '', ',;=');
return new self(array_map(function ($subParts) use (&$index) {
$part = array_shift($subParts);
$attributes = HeaderUtils::combine($subParts);
$item = new AcceptHeaderItem($part[0], $attributes);
$item->setIndex($index++);
return $item;
}, preg_split('/\s*(?:,*("[^"]+"),*|,*(\'[^\']+\'),*|,+)\s*/', $headerValue, 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE)));
}, $parts));
}
/**
* Returns header value's string representation.
*
* @return string
*/
public function __toString()
public function __toString(): string
{
return implode(',', $this->items);
}
/**
* Tests if header has given value.
*
* @param string $value
*
* @return bool
*/
public function has($value)
public function has(string $value): bool
{
return isset($this->items[$value]);
}
/**
* Returns given value's item, if exists.
*
* @param string $value
*
* @return AcceptHeaderItem|null
*/
public function get($value)
public function get(string $value): ?AcceptHeaderItem
{
return isset($this->items[$value]) ? $this->items[$value] : null;
return $this->items[$value] ?? $this->items[explode('/', $value)[0].'/*'] ?? $this->items['*/*'] ?? $this->items['*'] ?? null;
}
/**
* Adds an item.
*
* @param AcceptHeaderItem $item
*
* @return $this
*/
public function add(AcceptHeaderItem $item)
public function add(AcceptHeaderItem $item): static
{
$this->items[$item->getValue()] = $item;
$this->sorted = false;
@@ -116,7 +103,7 @@ class AcceptHeader
*
* @return AcceptHeaderItem[]
*/
public function all()
public function all(): array
{
$this->sort();
@@ -125,12 +112,8 @@ class AcceptHeader
/**
* Filters items on their value using given regex.
*
* @param string $pattern
*
* @return self
*/
public function filter($pattern)
public function filter(string $pattern): self
{
return new self(array_filter($this->items, function (AcceptHeaderItem $item) use ($pattern) {
return preg_match($pattern, $item->getValue());
@@ -139,10 +122,8 @@ class AcceptHeader
/**
* Returns first item.
*
* @return AcceptHeaderItem|null
*/
public function first()
public function first(): ?AcceptHeaderItem
{
$this->sort();
@@ -152,10 +133,10 @@ class AcceptHeader
/**
* Sorts items by descending quality.
*/
private function sort()
private function sort(): void
{
if (!$this->sorted) {
uasort($this->items, function ($a, $b) {
uasort($this->items, function (AcceptHeaderItem $a, AcceptHeaderItem $b) {
$qA = $a->getQuality();
$qB = $b->getQuality();

View File

@@ -18,33 +18,12 @@ namespace Symfony\Component\HttpFoundation;
*/
class AcceptHeaderItem
{
/**
* @var string
*/
private $value;
private string $value;
private float $quality = 1.0;
private int $index = 0;
private array $attributes = [];
/**
* @var float
*/
private $quality = 1.0;
/**
* @var int
*/
private $index = 0;
/**
* @var array
*/
private $attributes = array();
/**
* Constructor.
*
* @param string $value
* @param array $attributes
*/
public function __construct($value, array $attributes = array())
public function __construct(string $value, array $attributes = [])
{
$this->value = $value;
foreach ($attributes as $name => $value) {
@@ -54,45 +33,25 @@ class AcceptHeaderItem
/**
* Builds an AcceptHeaderInstance instance from a string.
*
* @param string $itemValue
*
* @return self
*/
public static function fromString($itemValue)
public static function fromString(?string $itemValue): self
{
$bits = preg_split('/\s*(?:;*("[^"]+");*|;*(\'[^\']+\');*|;+)\s*/', $itemValue, 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
$value = array_shift($bits);
$attributes = array();
$parts = HeaderUtils::split($itemValue ?? '', ';=');
$lastNullAttribute = null;
foreach ($bits as $bit) {
if (($start = substr($bit, 0, 1)) === ($end = substr($bit, -1)) && ($start === '"' || $start === '\'')) {
$attributes[$lastNullAttribute] = substr($bit, 1, -1);
} elseif ('=' === $end) {
$lastNullAttribute = $bit = substr($bit, 0, -1);
$attributes[$bit] = null;
} else {
$parts = explode('=', $bit);
$attributes[$parts[0]] = isset($parts[1]) && strlen($parts[1]) > 0 ? $parts[1] : '';
}
}
$part = array_shift($parts);
$attributes = HeaderUtils::combine($parts);
return new self(($start = substr($value, 0, 1)) === ($end = substr($value, -1)) && ($start === '"' || $start === '\'') ? substr($value, 1, -1) : $value, $attributes);
return new self($part[0], $attributes);
}
/**
* Returns header value's string representation.
*
* @return string
* Returns header value's string representation.
*/
public function __toString()
public function __toString(): string
{
$string = $this->value.($this->quality < 1 ? ';q='.$this->quality : '');
if (count($this->attributes) > 0) {
$string .= ';'.implode(';', array_map(function ($name, $value) {
return sprintf(preg_match('/[,;=]/', $value) ? '%s="%s"' : '%s=%s', $name, $value);
}, array_keys($this->attributes), $this->attributes));
if (\count($this->attributes) > 0) {
$string .= '; '.HeaderUtils::toString($this->attributes, ';');
}
return $string;
@@ -101,11 +60,9 @@ class AcceptHeaderItem
/**
* Set the item value.
*
* @param string $value
*
* @return $this
*/
public function setValue($value)
public function setValue(string $value): static
{
$this->value = $value;
@@ -114,10 +71,8 @@ class AcceptHeaderItem
/**
* Returns the item value.
*
* @return string
*/
public function getValue()
public function getValue(): string
{
return $this->value;
}
@@ -125,11 +80,9 @@ class AcceptHeaderItem
/**
* Set the item quality.
*
* @param float $quality
*
* @return $this
*/
public function setQuality($quality)
public function setQuality(float $quality): static
{
$this->quality = $quality;
@@ -138,10 +91,8 @@ class AcceptHeaderItem
/**
* Returns the item quality.
*
* @return float
*/
public function getQuality()
public function getQuality(): float
{
return $this->quality;
}
@@ -149,11 +100,9 @@ class AcceptHeaderItem
/**
* Set the item index.
*
* @param int $index
*
* @return $this
*/
public function setIndex($index)
public function setIndex(int $index): static
{
$this->index = $index;
@@ -162,45 +111,32 @@ class AcceptHeaderItem
/**
* Returns the item index.
*
* @return int
*/
public function getIndex()
public function getIndex(): int
{
return $this->index;
}
/**
* Tests if an attribute exists.
*
* @param string $name
*
* @return bool
*/
public function hasAttribute($name)
public function hasAttribute(string $name): bool
{
return isset($this->attributes[$name]);
}
/**
* Returns an attribute by its name.
*
* @param string $name
* @param mixed $default
*
* @return mixed
*/
public function getAttribute($name, $default = null)
public function getAttribute(string $name, mixed $default = null): mixed
{
return isset($this->attributes[$name]) ? $this->attributes[$name] : $default;
return $this->attributes[$name] ?? $default;
}
/**
* Returns all attributes.
*
* @return array
*/
public function getAttributes()
public function getAttributes(): array
{
return $this->attributes;
}
@@ -208,17 +144,14 @@ class AcceptHeaderItem
/**
* Set an attribute.
*
* @param string $name
* @param string $value
*
* @return $this
*/
public function setAttribute($name, $value)
public function setAttribute(string $name, string $value): static
{
if ('q' === $name) {
$this->quality = (float) $value;
} else {
$this->attributes[$name] = (string) $value;
$this->attributes[$name] = $value;
}
return $this;

View File

@@ -1,43 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation;
/**
* Request represents an HTTP request from an Apache server.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class ApacheRequest extends Request
{
/**
* {@inheritdoc}
*/
protected function prepareRequestUri()
{
return $this->server->get('REQUEST_URI');
}
/**
* {@inheritdoc}
*/
protected function prepareBaseUrl()
{
$baseUrl = $this->server->get('SCRIPT_NAME');
if (false === strpos($this->server->get('REQUEST_URI'), $baseUrl)) {
// assume mod_rewrite
return rtrim(dirname($baseUrl), '/\\');
}
return $baseUrl;
}
}

View File

@@ -11,8 +11,8 @@
namespace Symfony\Component\HttpFoundation;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\File;
/**
* BinaryFileResponse represents an HTTP response delivering a file.
@@ -31,22 +31,21 @@ class BinaryFileResponse extends Response
* @var File
*/
protected $file;
protected $offset;
protected $maxlen;
protected $offset = 0;
protected $maxlen = -1;
protected $deleteFileAfterSend = false;
protected $chunkSize = 8 * 1024;
/**
* Constructor.
*
* @param \SplFileInfo|string $file The file to stream
* @param int $status The response status code
* @param array $headers An array of response headers
* @param bool $public Files are public by default
* @param null|string $contentDisposition The type of Content-Disposition to set automatically with the filename
* @param string|null $contentDisposition The type of Content-Disposition to set automatically with the filename
* @param bool $autoEtag Whether the ETag header should be automatically set
* @param bool $autoLastModified Whether the Last-Modified header should be automatically set
*/
public function __construct($file, $status = 200, $headers = array(), $public = true, $contentDisposition = null, $autoEtag = false, $autoLastModified = true)
public function __construct(\SplFileInfo|string $file, int $status = 200, array $headers = [], bool $public = true, string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true)
{
parent::__construct(null, $status, $headers);
@@ -57,35 +56,14 @@ class BinaryFileResponse extends Response
}
}
/**
* @param \SplFileInfo|string $file The file to stream
* @param int $status The response status code
* @param array $headers An array of response headers
* @param bool $public Files are public by default
* @param null|string $contentDisposition The type of Content-Disposition to set automatically with the filename
* @param bool $autoEtag Whether the ETag header should be automatically set
* @param bool $autoLastModified Whether the Last-Modified header should be automatically set
*
* @return static
*/
public static function create($file = null, $status = 200, $headers = array(), $public = true, $contentDisposition = null, $autoEtag = false, $autoLastModified = true)
{
return new static($file, $status, $headers, $public, $contentDisposition, $autoEtag, $autoLastModified);
}
/**
* Sets the file to stream.
*
* @param \SplFileInfo|string $file The file to stream
* @param string $contentDisposition
* @param bool $autoEtag
* @param bool $autoLastModified
*
* @return $this
*
* @throws FileException
*/
public function setFile($file, $contentDisposition = null, $autoEtag = false, $autoLastModified = true)
public function setFile(\SplFileInfo|string $file, string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true): static
{
if (!$file instanceof File) {
if ($file instanceof \SplFileInfo) {
@@ -118,18 +96,34 @@ class BinaryFileResponse extends Response
/**
* Gets the file.
*
* @return File The file to stream
*/
public function getFile()
public function getFile(): File
{
return $this->file;
}
/**
* Automatically sets the Last-Modified header according the file modification date.
* Sets the response stream chunk size.
*
* @return $this
*/
public function setAutoLastModified()
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.');
}
$this->chunkSize = $chunkSize;
return $this;
}
/**
* Automatically sets the Last-Modified header according the file modification date.
*
* @return $this
*/
public function setAutoLastModified(): static
{
$this->setLastModified(\DateTime::createFromFormat('U', $this->file->getMTime()));
@@ -138,10 +132,12 @@ class BinaryFileResponse extends Response
/**
* Automatically sets the ETag header according to the checksum of the file.
*
* @return $this
*/
public function setAutoEtag()
public function setAutoEtag(): static
{
$this->setEtag(sha1_file($this->file->getPathname()));
$this->setEtag(base64_encode(hash_file('sha256', $this->file->getPathname(), true)));
return $this;
}
@@ -150,24 +146,24 @@ class BinaryFileResponse extends Response
* Sets the Content-Disposition header with the given filename.
*
* @param string $disposition ResponseHeaderBag::DISPOSITION_INLINE or ResponseHeaderBag::DISPOSITION_ATTACHMENT
* @param string $filename Optionally use this filename instead of the real name of the file
* @param string $filename Optionally use this UTF-8 encoded filename instead of the real name of the file
* @param string $filenameFallback A fallback filename, containing only ASCII characters. Defaults to an automatically encoded filename
*
* @return $this
*/
public function setContentDisposition($disposition, $filename = '', $filenameFallback = '')
public function setContentDisposition(string $disposition, string $filename = '', string $filenameFallback = ''): static
{
if ($filename === '') {
if ('' === $filename) {
$filename = $this->file->getFilename();
}
if ('' === $filenameFallback && (!preg_match('/^[\x20-\x7e]*$/', $filename) || false !== strpos($filename, '%'))) {
$encoding = mb_detect_encoding($filename, null, true);
if ('' === $filenameFallback && (!preg_match('/^[\x20-\x7e]*$/', $filename) || str_contains($filename, '%'))) {
$encoding = mb_detect_encoding($filename, null, true) ?: '8bit';
for ($i = 0, $filenameLength = mb_strlen($filename, $encoding); $i < $filenameLength; ++$i) {
$char = mb_substr($filename, $i, 1, $encoding);
if ('%' === $char || ord($char) < 32 || ord($char) > 126) {
if ('%' === $char || \ord($char) < 32 || \ord($char) > 126) {
$filenameFallback .= '_';
} else {
$filenameFallback .= $char;
@@ -184,17 +180,21 @@ class BinaryFileResponse extends Response
/**
* {@inheritdoc}
*/
public function prepare(Request $request)
public function prepare(Request $request): static
{
if ($this->isInformational() || $this->isEmpty()) {
parent::prepare($request);
$this->maxlen = 0;
return $this;
}
if (!$this->headers->has('Content-Type')) {
$this->headers->set('Content-Type', $this->file->getMimeType() ?: 'application/octet-stream');
}
if ('HTTP/1.0' !== $request->server->get('SERVER_PROTOCOL')) {
$this->setProtocolVersion('1.1');
}
$this->ensureIEOverSSLCompatibility($request);
parent::prepare($request);
$this->offset = 0;
$this->maxlen = -1;
@@ -202,11 +202,12 @@ class BinaryFileResponse extends Response
if (false === $fileSize = $this->file->getSize()) {
return $this;
}
$this->headers->remove('Transfer-Encoding');
$this->headers->set('Content-Length', $fileSize);
if (!$this->headers->has('Accept-Ranges')) {
// Only accept ranges on safe HTTP methods
$this->headers->set('Accept-Ranges', $request->isMethodSafe(false) ? 'bytes' : 'none');
$this->headers->set('Accept-Ranges', $request->isMethodSafe() ? 'bytes' : 'none');
}
if (self::$trustXSendfileTypeHeader && $request->headers->has('X-Sendfile-Type')) {
@@ -217,61 +218,68 @@ class BinaryFileResponse extends Response
if (false === $path) {
$path = $this->file->getPathname();
}
if (strtolower($type) === 'x-accel-redirect') {
if ('x-accel-redirect' === strtolower($type)) {
// Do X-Accel-Mapping substitutions.
// @link http://wiki.nginx.org/X-accel#X-Accel-Redirect
foreach (explode(',', $request->headers->get('X-Accel-Mapping', '')) as $mapping) {
$mapping = explode('=', $mapping, 2);
if (2 === count($mapping)) {
$pathPrefix = trim($mapping[0]);
$location = trim($mapping[1]);
if (substr($path, 0, strlen($pathPrefix)) === $pathPrefix) {
$path = $location.substr($path, strlen($pathPrefix));
break;
}
// @link https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/#x-accel-redirect
$parts = HeaderUtils::split($request->headers->get('X-Accel-Mapping', ''), ',=');
foreach ($parts as $part) {
[$pathPrefix, $location] = $part;
if (substr($path, 0, \strlen($pathPrefix)) === $pathPrefix) {
$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->maxlen = 0;
break;
}
}
} else {
$this->headers->set($type, $path);
$this->maxlen = 0;
}
$this->headers->set($type, $path);
$this->maxlen = 0;
} elseif ($request->headers->has('Range')) {
} elseif ($request->headers->has('Range') && $request->isMethod('GET')) {
// Process the range headers.
if (!$request->headers->has('If-Range') || $this->hasValidIfRangeHeader($request->headers->get('If-Range'))) {
$range = $request->headers->get('Range');
list($start, $end) = explode('-', substr($range, 6), 2) + array(0);
if (str_starts_with($range, 'bytes=')) {
[$start, $end] = explode('-', substr($range, 6), 2) + [0];
$end = ('' === $end) ? $fileSize - 1 : (int) $end;
$end = ('' === $end) ? $fileSize - 1 : (int) $end;
if ('' === $start) {
$start = $fileSize - $end;
$end = $fileSize - 1;
} else {
$start = (int) $start;
}
if ('' === $start) {
$start = $fileSize - $end;
$end = $fileSize - 1;
} else {
$start = (int) $start;
}
if ($start <= $end) {
if ($start < 0 || $end > $fileSize - 1) {
$this->setStatusCode(416);
$this->headers->set('Content-Range', sprintf('bytes */%s', $fileSize));
} elseif ($start !== 0 || $end !== $fileSize - 1) {
$this->maxlen = $end < $fileSize ? $end - $start + 1 : -1;
$this->offset = $start;
if ($start <= $end) {
$end = min($end, $fileSize - 1);
if ($start < 0 || $start > $end) {
$this->setStatusCode(416);
$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-Length', $end - $start + 1);
$this->setStatusCode(206);
$this->headers->set('Content-Range', sprintf('bytes %s-%s/%s', $start, $end, $fileSize));
$this->headers->set('Content-Length', $end - $start + 1);
}
}
}
}
}
if ($request->isMethod('HEAD')) {
$this->maxlen = 0;
}
return $this;
}
private function hasValidIfRangeHeader($header)
private function hasValidIfRangeHeader(?string $header): bool
{
if ($this->getEtag() === $header) {
return true;
@@ -285,30 +293,46 @@ class BinaryFileResponse extends Response
}
/**
* Sends the file.
*
* {@inheritdoc}
*/
public function sendContent()
public function sendContent(): static
{
if (!$this->isSuccessful()) {
return parent::sendContent();
}
try {
if (!$this->isSuccessful()) {
return parent::sendContent();
}
if (0 === $this->maxlen) {
return $this;
}
if (0 === $this->maxlen) {
return $this;
}
$out = fopen('php://output', 'wb');
$file = fopen($this->file->getPathname(), 'rb');
$out = fopen('php://output', 'w');
$file = fopen($this->file->getPathname(), 'r');
stream_copy_to_stream($file, $out, $this->maxlen, $this->offset);
ignore_user_abort(true);
fclose($out);
fclose($file);
if (0 !== $this->offset) {
fseek($file, $this->offset);
}
if ($this->deleteFileAfterSend) {
unlink($this->file->getPathname());
$length = $this->maxlen;
while ($length && !feof($file)) {
$read = ($length > $this->chunkSize) ? $this->chunkSize : $length;
$length -= $read;
stream_copy_to_stream($file, $out, $read);
if (connection_aborted()) {
break;
}
}
fclose($out);
fclose($file);
} finally {
if ($this->deleteFileAfterSend && is_file($this->file->getPathname())) {
unlink($this->file->getPathname());
}
}
return $this;
@@ -319,19 +343,19 @@ class BinaryFileResponse extends Response
*
* @throws \LogicException when the content is not null
*/
public function setContent($content)
public function setContent(?string $content): static
{
if (null !== $content) {
throw new \LogicException('The content cannot be set on a BinaryFileResponse instance.');
}
return $this;
}
/**
* {@inheritdoc}
*
* @return false
*/
public function getContent()
public function getContent(): string|false
{
return false;
}
@@ -345,14 +369,12 @@ class BinaryFileResponse extends Response
}
/**
* If this is set to true, the file will be unlinked after the request is send
* If this is set to true, the file will be unlinked after the request is sent
* Note: If the X-Sendfile header is used, the deleteFileAfterSend setting will not be used.
*
* @param bool $shouldDelete
*
* @return $this
*/
public function deleteFileAfterSend($shouldDelete)
public function deleteFileAfterSend(bool $shouldDelete = true): static
{
$this->deleteFileAfterSend = $shouldDelete;

View File

@@ -1,11 +1,176 @@
CHANGELOG
=========
6.0
---
* Remove the `NamespacedAttributeBag` class
* Removed `Response::create()`, `JsonResponse::create()`,
`RedirectResponse::create()`, `StreamedResponse::create()` and
`BinaryFileResponse::create()` methods (use `__construct()` instead)
* Not passing a `Closure` together with `FILTER_CALLBACK` to `ParameterBag::filter()` throws an `\InvalidArgumentException`; wrap your filter in a closure instead
* Not passing a `Closure` together with `FILTER_CALLBACK` to `InputBag::filter()` throws an `\InvalidArgumentException`; wrap your filter in a closure instead
* Removed the `Request::HEADER_X_FORWARDED_ALL` constant, use either `Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO` or `Request::HEADER_X_FORWARDED_AWS_ELB` or `Request::HEADER_X_FORWARDED_TRAEFIK`constants instead
* Rename `RequestStack::getMasterRequest()` to `getMainRequest()`
* Not passing `FILTER_REQUIRE_ARRAY` or `FILTER_FORCE_ARRAY` flags to `InputBag::filter()` when filtering an array will throw `BadRequestException`
* Removed the `Request::HEADER_X_FORWARDED_ALL` constant
* Retrieving non-scalar values using `InputBag::get()` will throw `BadRequestException` (use `InputBad::all()` instead to retrieve an array)
* Passing non-scalar default value as the second argument `InputBag::get()` will throw `\InvalidArgumentException`
* Passing non-scalar, non-array value as the second argument `InputBag::set()` will throw `\InvalidArgumentException`
* Passing `null` as `$requestIp` to `IpUtils::__checkIp()`, `IpUtils::__checkIp4()` or `IpUtils::__checkIp6()` is not supported anymore.
5.4
---
* Deprecate passing `null` as `$requestIp` to `IpUtils::__checkIp()`, `IpUtils::__checkIp4()` or `IpUtils::__checkIp6()`, pass an empty string instead.
* Add the `litespeed_finish_request` method to work with Litespeed
* Deprecate `upload_progress.*` and `url_rewriter.tags` session options
* Allow setting session options via DSN
5.3
---
* Add the `SessionFactory`, `NativeSessionStorageFactory`, `PhpBridgeSessionStorageFactory` and `MockFileSessionStorageFactory` classes
* Calling `Request::getSession()` when there is no available session throws a `SessionNotFoundException`
* Add the `RequestStack::getSession` method
* Deprecate the `NamespacedAttributeBag` class
* Add `ResponseFormatSame` PHPUnit constraint
* Deprecate the `RequestStack::getMasterRequest()` method and add `getMainRequest()` as replacement
5.2.0
-----
* added support for `X-Forwarded-Prefix` header
* added `HeaderUtils::parseQuery()`: it does the same as `parse_str()` but preserves dots in variable names
* added `File::getContent()`
* added ability to use comma separated ip addresses for `RequestMatcher::matchIps()`
* added `Request::toArray()` to parse a JSON request body to an array
* added `RateLimiter\RequestRateLimiterInterface` and `RateLimiter\AbstractRequestRateLimiter`
* deprecated not passing a `Closure` together with `FILTER_CALLBACK` to `ParameterBag::filter()`; wrap your filter in a closure instead.
* Deprecated the `Request::HEADER_X_FORWARDED_ALL` constant, use either `HEADER_X_FORWARDED_FOR | HEADER_X_FORWARDED_HOST | HEADER_X_FORWARDED_PORT | HEADER_X_FORWARDED_PROTO` or `HEADER_X_FORWARDED_AWS_ELB` or `HEADER_X_FORWARDED_TRAEFIK` constants instead.
* Deprecated `BinaryFileResponse::create()`, use `__construct()` instead
5.1.0
-----
* added `Cookie::withValue`, `Cookie::withDomain`, `Cookie::withExpires`,
`Cookie::withPath`, `Cookie::withSecure`, `Cookie::withHttpOnly`,
`Cookie::withRaw`, `Cookie::withSameSite`
* Deprecate `Response::create()`, `JsonResponse::create()`,
`RedirectResponse::create()`, and `StreamedResponse::create()` methods (use
`__construct()` instead)
* added `Request::preferSafeContent()` and `Response::setContentSafe()` to handle "safe" HTTP preference
according to [RFC 8674](https://tools.ietf.org/html/rfc8674)
* made the Mime component an optional dependency
* added `MarshallingSessionHandler`, `IdentityMarshaller`
* made `Session` accept a callback to report when the session is being used
* Add support for all core cache control directives
* Added `Symfony\Component\HttpFoundation\InputBag`
* Deprecated retrieving non-string values using `InputBag::get()`, use `InputBag::all()` if you need access to the collection of values
5.0.0
-----
* made `Cookie` auto-secure and lax by default
* removed classes in the `MimeType` namespace, use the Symfony Mime component instead
* removed method `UploadedFile::getClientSize()` and the related constructor argument
* made `Request::getSession()` throw if the session has not been set before
* removed `Response::HTTP_RESERVED_FOR_WEBDAV_ADVANCED_COLLECTIONS_EXPIRED_PROPOSAL`
* passing a null url when instantiating a `RedirectResponse` is not allowed
4.4.0
-----
* passing arguments to `Request::isMethodSafe()` is deprecated.
* `ApacheRequest` is deprecated, use the `Request` class instead.
* passing a third argument to `HeaderBag::get()` is deprecated, use method `all()` instead
* [BC BREAK] `PdoSessionHandler` with MySQL changed the type of the lifetime column,
make sure to run `ALTER TABLE sessions MODIFY sess_lifetime INTEGER UNSIGNED NOT NULL` to
update your database.
* `PdoSessionHandler` now precalculates the expiry timestamp in the lifetime column,
make sure to run `CREATE INDEX EXPIRY ON sessions (sess_lifetime)` to update your database
to speed up garbage collection of expired sessions.
* added `SessionHandlerFactory` to create session handlers with a DSN
* added `IpUtils::anonymize()` to help with GDPR compliance.
4.3.0
-----
* added PHPUnit constraints: `RequestAttributeValueSame`, `ResponseCookieValueSame`, `ResponseHasCookie`,
`ResponseHasHeader`, `ResponseHeaderSame`, `ResponseIsRedirected`, `ResponseIsSuccessful`, and `ResponseStatusCodeSame`
* deprecated `MimeTypeGuesserInterface` and `ExtensionGuesserInterface` in favor of `Symfony\Component\Mime\MimeTypesInterface`.
* deprecated `MimeType` and `MimeTypeExtensionGuesser` in favor of `Symfony\Component\Mime\MimeTypes`.
* deprecated `FileBinaryMimeTypeGuesser` in favor of `Symfony\Component\Mime\FileBinaryMimeTypeGuesser`.
* deprecated `FileinfoMimeTypeGuesser` in favor of `Symfony\Component\Mime\FileinfoMimeTypeGuesser`.
* added `UrlHelper` that allows to get an absolute URL and a relative path for a given path
4.2.0
-----
* the default value of the "$secure" and "$samesite" arguments of Cookie's constructor
will respectively change from "false" to "null" and from "null" to "lax" in Symfony
5.0, you should define their values explicitly or use "Cookie::create()" instead.
* added `matchPort()` in RequestMatcher
4.1.3
-----
* [BC BREAK] Support for the IIS-only `X_ORIGINAL_URL` and `X_REWRITE_URL`
HTTP headers has been dropped for security reasons.
4.1.0
-----
* Query string normalization uses `parse_str()` instead of custom parsing logic.
* Passing the file size to the constructor of the `UploadedFile` class is deprecated.
* The `getClientSize()` method of the `UploadedFile` class is deprecated. Use `getSize()` instead.
* added `RedisSessionHandler` to use Redis as a session storage
* The `get()` method of the `AcceptHeader` class now takes into account the
`*` and `*/*` default values (if they are present in the Accept HTTP header)
when looking for items.
* deprecated `Request::getSession()` when no session has been set. Use `Request::hasSession()` instead.
* added `CannotWriteFileException`, `ExtensionFileException`, `FormSizeFileException`,
`IniSizeFileException`, `NoFileException`, `NoTmpDirFileException`, `PartialFileException` to
handle failed `UploadedFile`.
* added `MigratingSessionHandler` for migrating between two session handlers without losing sessions
* added `HeaderUtils`.
4.0.0
-----
* the `Request::setTrustedHeaderName()` and `Request::getTrustedHeaderName()`
methods have been removed
* the `Request::HEADER_CLIENT_IP` constant has been removed, use
`Request::HEADER_X_FORWARDED_FOR` instead
* the `Request::HEADER_CLIENT_HOST` constant has been removed, use
`Request::HEADER_X_FORWARDED_HOST` instead
* the `Request::HEADER_CLIENT_PROTO` constant has been removed, use
`Request::HEADER_X_FORWARDED_PROTO` instead
* the `Request::HEADER_CLIENT_PORT` constant has been removed, use
`Request::HEADER_X_FORWARDED_PORT` instead
* checking for cacheable HTTP methods using the `Request::isMethodSafe()`
method (by not passing `false` as its argument) is not supported anymore and
throws a `\BadMethodCallException`
* the `WriteCheckSessionHandler`, `NativeSessionHandler` and `NativeProxy` classes have been removed
* setting session save handlers that do not implement `\SessionHandlerInterface` in
`NativeSessionStorage::setSaveHandler()` is not supported anymore and throws a
`\TypeError`
3.4.0
-----
* implemented PHP 7.0's `SessionUpdateTimestampHandlerInterface` with a new
`AbstractSessionHandler` base class and a new `StrictSessionHandler` wrapper
* deprecated the `WriteCheckSessionHandler`, `NativeSessionHandler` and `NativeProxy` classes
* deprecated setting session save handlers that do not implement `\SessionHandlerInterface` in `NativeSessionStorage::setSaveHandler()`
* deprecated using `MongoDbSessionHandler` with the legacy mongo extension; use it with the mongodb/mongodb package and ext-mongodb instead
* deprecated `MemcacheSessionHandler`; use `MemcachedSessionHandler` instead
3.3.0
-----
* the `Request::setTrustedProxies()` method takes a new `$trustedHeaderSet` argument,
see http://symfony.com/doc/current/components/http_foundation/trusting_proxies.html for more info,
see https://symfony.com/doc/current/deployment/proxies.html for more info,
* deprecated the `Request::setTrustedHeaderName()` and `Request::getTrustedHeaderName()` methods,
* added `File\Stream`, to be passed to `BinaryFileResponse` when the size of the served file is unknown,
disabling `Range` and `Content-Length` handling, switching to chunked encoding instead
@@ -128,10 +293,10 @@ CHANGELOG
* Added `FlashBag`. Flashes expire when retrieved by `get()` or `all()`. This
implementation is ESI compatible.
* Added `AutoExpireFlashBag` (default) to replicate Symfony 2.0.x auto expire
behaviour of messages auto expiring after one page page load. Messages must
behavior of messages auto expiring after one page page load. Messages must
be retrieved by `get()` or `all()`.
* Added `Symfony\Component\HttpFoundation\Attribute\AttributeBag` to replicate
attributes storage behaviour from 2.0.x (default).
attributes storage behavior from 2.0.x (default).
* Added `Symfony\Component\HttpFoundation\Attribute\NamespacedAttributeBag` for
namespace session attributes.
* Flash API can stores messages in an array so there may be multiple messages

View File

@@ -18,6 +18,10 @@ namespace Symfony\Component\HttpFoundation;
*/
class Cookie
{
public const SAMESITE_NONE = 'none';
public const SAMESITE_LAX = 'lax';
public const SAMESITE_STRICT = 'strict';
protected $name;
protected $value;
protected $domain;
@@ -25,23 +29,21 @@ class Cookie
protected $path;
protected $secure;
protected $httpOnly;
private $raw;
private $sameSite;
const SAMESITE_LAX = 'lax';
const SAMESITE_STRICT = 'strict';
private bool $raw;
private ?string $sameSite = null;
private bool $secureDefault = false;
private const RESERVED_CHARS_LIST = "=,; \t\r\n\v\f";
private const RESERVED_CHARS_FROM = ['=', ',', ';', ' ', "\t", "\r", "\n", "\v", "\f"];
private const RESERVED_CHARS_TO = ['%3D', '%2C', '%3B', '%20', '%09', '%0D', '%0A', '%0B', '%0C'];
/**
* Creates cookie from raw header string.
*
* @param string $cookie
* @param bool $decode
*
* @return static
*/
public static function fromString($cookie, $decode = false)
public static function fromString(string $cookie, bool $decode = false): static
{
$data = array(
$data = [
'expires' => 0,
'path' => '/',
'domain' => null,
@@ -49,56 +51,46 @@ class Cookie
'httponly' => false,
'raw' => !$decode,
'samesite' => null,
);
foreach (explode(';', $cookie) as $part) {
if (false === strpos($part, '=')) {
$key = trim($part);
$value = true;
} else {
list($key, $value) = explode('=', trim($part), 2);
$key = trim($key);
$value = trim($value);
}
if (!isset($data['name'])) {
$data['name'] = $decode ? urldecode($key) : $key;
$data['value'] = true === $value ? null : ($decode ? urldecode($value) : $value);
continue;
}
switch ($key = strtolower($key)) {
case 'name':
case 'value':
break;
case 'max-age':
$data['expires'] = time() + (int) $value;
break;
default:
$data[$key] = $value;
break;
}
];
$parts = HeaderUtils::split($cookie, ';=');
$part = array_shift($parts);
$name = $decode ? urldecode($part[0]) : $part[0];
$value = isset($part[1]) ? ($decode ? urldecode($part[1]) : $part[1]) : null;
$data = HeaderUtils::combine($parts) + $data;
$data['expires'] = self::expiresTimestamp($data['expires']);
if (isset($data['max-age']) && ($data['max-age'] > 0 || $data['expires'] > time())) {
$data['expires'] = time() + (int) $data['max-age'];
}
return new static($data['name'], $data['value'], $data['expires'], $data['path'], $data['domain'], $data['secure'], $data['httponly'], $data['raw'], $data['samesite']);
return new static($name, $value, $data['expires'], $data['path'], $data['domain'], $data['secure'], $data['httponly'], $data['raw'], $data['samesite']);
}
public static function create(string $name, string $value = null, int|string|\DateTimeInterface $expire = 0, ?string $path = '/', string $domain = null, bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = self::SAMESITE_LAX): self
{
return new self($name, $value, $expire, $path, $domain, $secure, $httpOnly, $raw, $sameSite);
}
/**
* Constructor.
*
* @param string $name The name of the cookie
* @param string|null $value The value of the cookie
* @param int|string|\DateTimeInterface $expire The time the cookie expires
* @param string $path The path on the server in which the cookie will be available on
* @param string|null $domain The domain that the cookie is available to
* @param bool $secure Whether the cookie should only be transmitted over a secure HTTPS connection from the client
* @param bool|null $secure Whether the client should send back the cookie only over HTTPS or null to auto-enable this when the request is already using HTTPS
* @param bool $httpOnly Whether the cookie will be made accessible only through the HTTP protocol
* @param bool $raw Whether the cookie value should be sent with no url encoding
* @param string|null $sameSite Whether the cookie will be available for cross-site requests
*
* @throws \InvalidArgumentException
*/
public function __construct($name, $value = null, $expire = 0, $path = '/', $domain = null, $secure = false, $httpOnly = true, $raw = false, $sameSite = null)
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 = 'lax')
{
// from PHP source code
if (preg_match("/[=,; \t\r\n\013\014]/", $name)) {
if ($raw && false !== strpbrk($name, self::RESERVED_CHARS_LIST)) {
throw new \InvalidArgumentException(sprintf('The cookie name "%s" contains invalid characters.', $name));
}
@@ -106,6 +98,55 @@ class Cookie
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 = empty($path) ? '/' : $path;
$this->secure = $secure;
$this->httpOnly = $httpOnly;
$this->raw = $raw;
$this->sameSite = $this->withSameSite($sameSite)->sameSite;
}
/**
* Creates a cookie copy with a new value.
*/
public function withValue(?string $value): static
{
$cookie = clone $this;
$cookie->value = $value;
return $cookie;
}
/**
* Creates a cookie copy with a new domain that the cookie is available to.
*/
public function withDomain(?string $domain): static
{
$cookie = clone $this;
$cookie->domain = $domain;
return $cookie;
}
/**
* Creates a cookie copy with a new time the cookie expires.
*/
public function withExpires(int|string|\DateTimeInterface $expire = 0): static
{
$cookie = clone $this;
$cookie->expire = self::expiresTimestamp($expire);
return $cookie;
}
/**
* Converts expires formats to a unix timestamp.
*/
private static function expiresTimestamp(int|string|\DateTimeInterface $expire = 0): int
{
// convert expiration time to a Unix timestamp
if ($expire instanceof \DateTimeInterface) {
$expire = $expire->format('U');
@@ -117,38 +158,98 @@ class Cookie
}
}
$this->name = $name;
$this->value = $value;
$this->domain = $domain;
$this->expire = 0 < $expire ? (int) $expire : 0;
$this->path = empty($path) ? '/' : $path;
$this->secure = (bool) $secure;
$this->httpOnly = (bool) $httpOnly;
$this->raw = (bool) $raw;
return 0 < $expire ? (int) $expire : 0;
}
if (!in_array($sameSite, array(self::SAMESITE_LAX, self::SAMESITE_STRICT, null), true)) {
/**
* Creates a cookie copy with a new path on the server in which the cookie will be available on.
*/
public function withPath(string $path): static
{
$cookie = clone $this;
$cookie->path = '' === $path ? '/' : $path;
return $cookie;
}
/**
* Creates a cookie copy that only be transmitted over a secure HTTPS connection from the client.
*/
public function withSecure(bool $secure = true): static
{
$cookie = clone $this;
$cookie->secure = $secure;
return $cookie;
}
/**
* Creates a cookie copy that be accessible only through the HTTP protocol.
*/
public function withHttpOnly(bool $httpOnly = true): static
{
$cookie = clone $this;
$cookie->httpOnly = $httpOnly;
return $cookie;
}
/**
* Creates a cookie copy that uses no url encoding.
*/
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));
}
$cookie = clone $this;
$cookie->raw = $raw;
return $cookie;
}
/**
* Creates a cookie copy with SameSite attribute.
*/
public function withSameSite(?string $sameSite): static
{
if ('' === $sameSite) {
$sameSite = null;
} elseif (null !== $sameSite) {
$sameSite = strtolower($sameSite);
}
if (!\in_array($sameSite, [self::SAMESITE_LAX, self::SAMESITE_STRICT, self::SAMESITE_NONE, null], true)) {
throw new \InvalidArgumentException('The "sameSite" parameter value is not valid.');
}
$this->sameSite = $sameSite;
$cookie = clone $this;
$cookie->sameSite = $sameSite;
return $cookie;
}
/**
* Returns the cookie as a string.
*
* @return string The cookie
*/
public function __toString()
public function __toString(): string
{
$str = ($this->isRaw() ? $this->getName() : urlencode($this->getName())).'=';
if ($this->isRaw()) {
$str = $this->getName();
} else {
$str = str_replace(self::RESERVED_CHARS_FROM, self::RESERVED_CHARS_TO, $this->getName());
}
$str .= '=';
if ('' === (string) $this->getValue()) {
$str .= 'deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; max-age=-31536001';
$str .= 'deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; Max-Age=0';
} else {
$str .= $this->isRaw() ? $this->getValue() : rawurlencode($this->getValue());
if (0 !== $this->getExpiresTime()) {
$str .= '; expires='.gmdate('D, d-M-Y H:i:s T', $this->getExpiresTime()).'; max-age='.$this->getMaxAge();
$str .= '; expires='.gmdate('D, d-M-Y H:i:s T', $this->getExpiresTime()).'; Max-Age='.$this->getMaxAge();
}
}
@@ -177,111 +278,99 @@ class Cookie
/**
* Gets the name of the cookie.
*
* @return string
*/
public function getName()
public function getName(): string
{
return $this->name;
}
/**
* Gets the value of the cookie.
*
* @return string|null
*/
public function getValue()
public function getValue(): ?string
{
return $this->value;
}
/**
* Gets the domain that the cookie is available to.
*
* @return string|null
*/
public function getDomain()
public function getDomain(): ?string
{
return $this->domain;
}
/**
* Gets the time the cookie expires.
*
* @return int
*/
public function getExpiresTime()
public function getExpiresTime(): int
{
return $this->expire;
}
/**
* Gets the max-age attribute.
*
* @return int
*/
public function getMaxAge()
public function getMaxAge(): int
{
return 0 !== $this->expire ? $this->expire - time() : 0;
$maxAge = $this->expire - time();
return 0 >= $maxAge ? 0 : $maxAge;
}
/**
* Gets the path on the server in which the cookie will be available on.
*
* @return string
*/
public function getPath()
public function getPath(): string
{
return $this->path;
}
/**
* Checks whether the cookie should only be transmitted over a secure HTTPS connection from the client.
*
* @return bool
*/
public function isSecure()
public function isSecure(): bool
{
return $this->secure;
return $this->secure ?? $this->secureDefault;
}
/**
* Checks whether the cookie will be made accessible only through the HTTP protocol.
*
* @return bool
*/
public function isHttpOnly()
public function isHttpOnly(): bool
{
return $this->httpOnly;
}
/**
* Whether this cookie is about to be cleared.
*
* @return bool
*/
public function isCleared()
public function isCleared(): bool
{
return $this->expire < time();
return 0 !== $this->expire && $this->expire < time();
}
/**
* Checks if the cookie value should be sent with no url encoding.
*
* @return bool
*/
public function isRaw()
public function isRaw(): bool
{
return $this->raw;
}
/**
* Gets the SameSite attribute.
*
* @return string|null
*/
public function getSameSite()
public function getSameSite(): ?string
{
return $this->sameSite;
}
/**
* @param bool $default The default value of the "secure" flag when it is set to null
*/
public function setSecureDefault(bool $default): void
{
$this->secureDefault = $default;
}
}

View File

@@ -9,13 +9,11 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
namespace Symfony\Component\HttpFoundation\Exception;
/**
* Adds SessionHandler functionality if available.
*
* @see http://php.net/sessionhandler
* Raised when a user sends a malformed request.
*/
class NativeSessionHandler extends \SessionHandler
class BadRequestException extends \UnexpectedValueException implements RequestExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Exception;
/**
* Thrown by Request::toArray() when the content cannot be JSON-decoded.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class JsonException extends \UnexpectedValueException implements RequestExceptionInterface
{
}

View File

@@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Exception;
/**
* Raised when a session does not exist. This happens in the following cases:
* - the session is not enabled
* - attempt to read a session outside a request context (ie. cli script).
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class SessionNotFoundException extends \LogicException implements RequestExceptionInterface
{
public function __construct(string $message = 'There is currently no session available.', int $code = 0, \Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

View File

@@ -11,6 +11,7 @@
namespace Symfony\Component\HttpFoundation;
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
/**
@@ -23,25 +24,25 @@ class ExpressionRequestMatcher extends RequestMatcher
private $language;
private $expression;
public function setExpression(ExpressionLanguage $language, $expression)
public function setExpression(ExpressionLanguage $language, Expression|string $expression)
{
$this->language = $language;
$this->expression = $expression;
}
public function matches(Request $request)
public function matches(Request $request): bool
{
if (!$this->language) {
if (!isset($this->language)) {
throw new \LogicException('Unable to match the request as the expression language is not available.');
}
return $this->language->evaluate($this->expression, array(
return $this->language->evaluate($this->expression, [
'request' => $request,
'method' => $request->getMethod(),
'path' => rawurldecode($request->getPathInfo()),
'host' => $request->getHost(),
'ip' => $request->getClientIp(),
'attributes' => $request->attributes->all(),
)) && parent::matches($request);
]) && parent::matches($request);
}
}

View File

@@ -18,12 +18,7 @@ namespace Symfony\Component\HttpFoundation\File\Exception;
*/
class AccessDeniedException extends FileException
{
/**
* Constructor.
*
* @param string $path The path to the accessed file
*/
public function __construct($path)
public function __construct(string $path)
{
parent::__construct(sprintf('The file %s could not be accessed', $path));
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\File\Exception;
/**
* Thrown when an UPLOAD_ERR_CANT_WRITE error occurred with UploadedFile.
*
* @author Florent Mata <florentmata@gmail.com>
*/
class CannotWriteFileException extends FileException
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\File\Exception;
/**
* Thrown when an UPLOAD_ERR_EXTENSION error occurred with UploadedFile.
*
* @author Florent Mata <florentmata@gmail.com>
*/
class ExtensionFileException extends FileException
{
}

View File

@@ -18,12 +18,7 @@ namespace Symfony\Component\HttpFoundation\File\Exception;
*/
class FileNotFoundException extends FileException
{
/**
* Constructor.
*
* @param string $path The path to the file that was not found
*/
public function __construct($path)
public function __construct(string $path)
{
parent::__construct(sprintf('The file "%s" does not exist', $path));
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\File\Exception;
/**
* Thrown when an UPLOAD_ERR_FORM_SIZE error occurred with UploadedFile.
*
* @author Florent Mata <florentmata@gmail.com>
*/
class FormSizeFileException extends FileException
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\File\Exception;
/**
* Thrown when an UPLOAD_ERR_INI_SIZE error occurred with UploadedFile.
*
* @author Florent Mata <florentmata@gmail.com>
*/
class IniSizeFileException extends FileException
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\File\Exception;
/**
* Thrown when an UPLOAD_ERR_NO_FILE error occurred with UploadedFile.
*
* @author Florent Mata <florentmata@gmail.com>
*/
class NoFileException extends FileException
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\File\Exception;
/**
* Thrown when an UPLOAD_ERR_NO_TMP_DIR error occurred with UploadedFile.
*
* @author Florent Mata <florentmata@gmail.com>
*/
class NoTmpDirFileException extends FileException
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\File\Exception;
/**
* Thrown when an UPLOAD_ERR_PARTIAL error occurred with UploadedFile.
*
* @author Florent Mata <florentmata@gmail.com>
*/
class PartialFileException extends FileException
{
}

View File

@@ -13,8 +13,8 @@ namespace Symfony\Component\HttpFoundation\File\Exception;
class UnexpectedTypeException extends FileException
{
public function __construct($value, $expectedType)
public function __construct(mixed $value, string $expectedType)
{
parent::__construct(sprintf('Expected argument of type %s, %s given', $expectedType, is_object($value) ? get_class($value) : gettype($value)));
parent::__construct(sprintf('Expected argument of type %s, %s given', $expectedType, get_debug_type($value)));
}
}

View File

@@ -13,8 +13,7 @@ namespace Symfony\Component\HttpFoundation\File;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesser;
use Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser;
use Symfony\Component\Mime\MimeTypes;
/**
* A file in the file system.
@@ -31,7 +30,7 @@ class File extends \SplFileInfo
*
* @throws FileNotFoundException If the given path is not a file
*/
public function __construct($path, $checkPath = true)
public function __construct(string $path, bool $checkPath = true)
{
if ($checkPath && !is_file($path)) {
throw new FileNotFoundException($path);
@@ -48,54 +47,53 @@ class File extends \SplFileInfo
* This method uses the mime type as guessed by getMimeType()
* to guess the file extension.
*
* @return string|null The guessed extension or null if it cannot be guessed
*
* @see ExtensionGuesser
* @see MimeTypes
* @see getMimeType()
*/
public function guessExtension()
public function guessExtension(): ?string
{
$type = $this->getMimeType();
$guesser = ExtensionGuesser::getInstance();
if (!class_exists(MimeTypes::class)) {
throw new \LogicException('You cannot guess the extension as the Mime component is not installed. Try running "composer require symfony/mime".');
}
return $guesser->guess($type);
return MimeTypes::getDefault()->getExtensions($this->getMimeType())[0] ?? null;
}
/**
* Returns the mime type of the file.
*
* The mime type is guessed using a MimeTypeGuesser instance, which uses finfo(),
* mime_content_type() and the system binary "file" (in this order), depending on
* which of those are available.
* The mime type is guessed using a MimeTypeGuesserInterface instance,
* which uses finfo_file() then the "file" system binary,
* depending on which of those are available.
*
* @return string|null The guessed mime type (e.g. "application/pdf")
*
* @see MimeTypeGuesser
* @see MimeTypes
*/
public function getMimeType()
public function getMimeType(): ?string
{
$guesser = MimeTypeGuesser::getInstance();
if (!class_exists(MimeTypes::class)) {
throw new \LogicException('You cannot guess the mime type as the Mime component is not installed. Try running "composer require symfony/mime".');
}
return $guesser->guess($this->getPathname());
return MimeTypes::getDefault()->guessMimeType($this->getPathname());
}
/**
* Moves the file to a new location.
*
* @param string $directory The destination folder
* @param string $name The new file name
*
* @return self A File object representing the new file
*
* @throws FileException if the target file could not be created
*/
public function move($directory, $name = null)
public function move(string $directory, string $name = null): self
{
$target = $this->getTargetFile($directory, $name);
if (!@rename($this->getPathname(), $target)) {
$error = error_get_last();
throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $this->getPathname(), $target, strip_tags($error['message'])));
set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; });
try {
$renamed = rename($this->getPathname(), $target);
} finally {
restore_error_handler();
}
if (!$renamed) {
throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s).', $this->getPathname(), $target, strip_tags($error)));
}
@chmod($target, 0666 & ~umask());
@@ -103,29 +101,36 @@ class File extends \SplFileInfo
return $target;
}
protected function getTargetFile($directory, $name = null)
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()));
}
return $content;
}
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));
$target = rtrim($directory, '/\\').\DIRECTORY_SEPARATOR.(null === $name ? $this->getBasename() : $this->getName($name));
return new self($target, false);
}
/**
* Returns locale independent base name of the given path.
*
* @param string $name The new file name
*
* @return string containing
*/
protected function getName($name)
protected function getName(string $name): string
{
$originalName = str_replace('\\', '/', $name);
$pos = strrpos($originalName, '/');

View File

@@ -1,96 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\File\MimeType;
/**
* A singleton mime type to file extension guesser.
*
* A default guesser is provided.
* You can register custom guessers by calling the register()
* method on the singleton instance:
*
* $guesser = ExtensionGuesser::getInstance();
* $guesser->register(new MyCustomExtensionGuesser());
*
* The last registered guesser is preferred over previously registered ones.
*/
class ExtensionGuesser implements ExtensionGuesserInterface
{
/**
* The singleton instance.
*
* @var ExtensionGuesser
*/
private static $instance = null;
/**
* All registered ExtensionGuesserInterface instances.
*
* @var array
*/
protected $guessers = array();
/**
* Returns the singleton instance.
*
* @return self
*/
public static function getInstance()
{
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Registers all natively provided extension guessers.
*/
private function __construct()
{
$this->register(new MimeTypeExtensionGuesser());
}
/**
* Registers a new extension guesser.
*
* When guessing, this guesser is preferred over previously registered ones.
*
* @param ExtensionGuesserInterface $guesser
*/
public function register(ExtensionGuesserInterface $guesser)
{
array_unshift($this->guessers, $guesser);
}
/**
* Tries to guess the extension.
*
* The mime type is passed to each registered mime type guesser in reverse order
* of their registration (last registered is queried first). Once a guesser
* returns a value that is not NULL, this method terminates and returns the
* value.
*
* @param string $mimeType The mime type
*
* @return string The guessed extension or NULL, if none could be guessed
*/
public function guess($mimeType)
{
foreach ($this->guessers as $guesser) {
if (null !== $extension = $guesser->guess($mimeType)) {
return $extension;
}
}
}
}

View File

@@ -1,27 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\File\MimeType;
/**
* Guesses the file extension corresponding to a given mime type.
*/
interface ExtensionGuesserInterface
{
/**
* Makes a best guess for a file extension, given a mime type.
*
* @param string $mimeType The mime type
*
* @return string The guessed extension or NULL, if none could be guessed
*/
public function guess($mimeType);
}

View File

@@ -1,87 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\File\MimeType;
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException;
/**
* Guesses the mime type with the binary "file" (only available on *nix).
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FileBinaryMimeTypeGuesser implements MimeTypeGuesserInterface
{
private $cmd;
/**
* Constructor.
*
* The $cmd pattern must contain a "%s" string that will be replaced
* with the file name to guess.
*
* The command output must start with the mime type of the file.
*
* @param string $cmd The command to run to get the mime type of a file
*/
public function __construct($cmd = 'file -b --mime %s 2>/dev/null')
{
$this->cmd = $cmd;
}
/**
* Returns whether this guesser is supported on the current OS.
*
* @return bool
*/
public static function isSupported()
{
return '\\' !== DIRECTORY_SEPARATOR && function_exists('passthru') && function_exists('escapeshellarg');
}
/**
* {@inheritdoc}
*/
public function guess($path)
{
if (!is_file($path)) {
throw new FileNotFoundException($path);
}
if (!is_readable($path)) {
throw new AccessDeniedException($path);
}
if (!self::isSupported()) {
return;
}
ob_start();
// need to use --mime instead of -i. see #6641
passthru(sprintf($this->cmd, escapeshellarg($path)), $return);
if ($return > 0) {
ob_end_clean();
return;
}
$type = trim(ob_get_clean());
if (!preg_match('#^([a-z0-9\-]+/[a-z0-9\-\.]+)#i', $type, $match)) {
// it's not a type, but an error message
return;
}
return $match[1];
}
}

View File

@@ -1,71 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\File\MimeType;
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException;
/**
* Guesses the mime type using the PECL extension FileInfo.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FileinfoMimeTypeGuesser implements MimeTypeGuesserInterface
{
private $magicFile;
/**
* Constructor.
*
* @param string $magicFile A magic file to use with the finfo instance
*
* @see http://www.php.net/manual/en/function.finfo-open.php
*/
public function __construct($magicFile = null)
{
$this->magicFile = $magicFile;
}
/**
* Returns whether this guesser is supported on the current OS/PHP setup.
*
* @return bool
*/
public static function isSupported()
{
return function_exists('finfo_open');
}
/**
* {@inheritdoc}
*/
public function guess($path)
{
if (!is_file($path)) {
throw new FileNotFoundException($path);
}
if (!is_readable($path)) {
throw new AccessDeniedException($path);
}
if (!self::isSupported()) {
return;
}
if (!$finfo = new \finfo(FILEINFO_MIME_TYPE, $this->magicFile)) {
return;
}
return $finfo->file($path);
}
}

View File

@@ -1,809 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\File\MimeType;
/**
* Provides a best-guess mapping of mime type to file extension.
*/
class MimeTypeExtensionGuesser implements ExtensionGuesserInterface
{
/**
* A map of mime types and their default extensions.
*
* This list has been placed under the public domain by the Apache HTTPD project.
* This list has been updated from upstream on 2013-04-23.
*
* @see http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types
*
* @var array
*/
protected $defaultExtensions = array(
'application/andrew-inset' => 'ez',
'application/applixware' => 'aw',
'application/atom+xml' => 'atom',
'application/atomcat+xml' => 'atomcat',
'application/atomsvc+xml' => 'atomsvc',
'application/ccxml+xml' => 'ccxml',
'application/cdmi-capability' => 'cdmia',
'application/cdmi-container' => 'cdmic',
'application/cdmi-domain' => 'cdmid',
'application/cdmi-object' => 'cdmio',
'application/cdmi-queue' => 'cdmiq',
'application/cu-seeme' => 'cu',
'application/davmount+xml' => 'davmount',
'application/docbook+xml' => 'dbk',
'application/dssc+der' => 'dssc',
'application/dssc+xml' => 'xdssc',
'application/ecmascript' => 'ecma',
'application/emma+xml' => 'emma',
'application/epub+zip' => 'epub',
'application/exi' => 'exi',
'application/font-tdpfr' => 'pfr',
'application/gml+xml' => 'gml',
'application/gpx+xml' => 'gpx',
'application/gxf' => 'gxf',
'application/hyperstudio' => 'stk',
'application/inkml+xml' => 'ink',
'application/ipfix' => 'ipfix',
'application/java-archive' => 'jar',
'application/java-serialized-object' => 'ser',
'application/java-vm' => 'class',
'application/javascript' => 'js',
'application/json' => 'json',
'application/jsonml+json' => 'jsonml',
'application/lost+xml' => 'lostxml',
'application/mac-binhex40' => 'hqx',
'application/mac-compactpro' => 'cpt',
'application/mads+xml' => 'mads',
'application/marc' => 'mrc',
'application/marcxml+xml' => 'mrcx',
'application/mathematica' => 'ma',
'application/mathml+xml' => 'mathml',
'application/mbox' => 'mbox',
'application/mediaservercontrol+xml' => 'mscml',
'application/metalink+xml' => 'metalink',
'application/metalink4+xml' => 'meta4',
'application/mets+xml' => 'mets',
'application/mods+xml' => 'mods',
'application/mp21' => 'm21',
'application/mp4' => 'mp4s',
'application/msword' => 'doc',
'application/mxf' => 'mxf',
'application/octet-stream' => 'bin',
'application/oda' => 'oda',
'application/oebps-package+xml' => 'opf',
'application/ogg' => 'ogx',
'application/omdoc+xml' => 'omdoc',
'application/onenote' => 'onetoc',
'application/oxps' => 'oxps',
'application/patch-ops-error+xml' => 'xer',
'application/pdf' => 'pdf',
'application/pgp-encrypted' => 'pgp',
'application/pgp-signature' => 'asc',
'application/pics-rules' => 'prf',
'application/pkcs10' => 'p10',
'application/pkcs7-mime' => 'p7m',
'application/pkcs7-signature' => 'p7s',
'application/pkcs8' => 'p8',
'application/pkix-attr-cert' => 'ac',
'application/pkix-cert' => 'cer',
'application/pkix-crl' => 'crl',
'application/pkix-pkipath' => 'pkipath',
'application/pkixcmp' => 'pki',
'application/pls+xml' => 'pls',
'application/postscript' => 'ai',
'application/prs.cww' => 'cww',
'application/pskc+xml' => 'pskcxml',
'application/rdf+xml' => 'rdf',
'application/reginfo+xml' => 'rif',
'application/relax-ng-compact-syntax' => 'rnc',
'application/resource-lists+xml' => 'rl',
'application/resource-lists-diff+xml' => 'rld',
'application/rls-services+xml' => 'rs',
'application/rpki-ghostbusters' => 'gbr',
'application/rpki-manifest' => 'mft',
'application/rpki-roa' => 'roa',
'application/rsd+xml' => 'rsd',
'application/rss+xml' => 'rss',
'application/rtf' => 'rtf',
'application/sbml+xml' => 'sbml',
'application/scvp-cv-request' => 'scq',
'application/scvp-cv-response' => 'scs',
'application/scvp-vp-request' => 'spq',
'application/scvp-vp-response' => 'spp',
'application/sdp' => 'sdp',
'application/set-payment-initiation' => 'setpay',
'application/set-registration-initiation' => 'setreg',
'application/shf+xml' => 'shf',
'application/smil+xml' => 'smi',
'application/sparql-query' => 'rq',
'application/sparql-results+xml' => 'srx',
'application/srgs' => 'gram',
'application/srgs+xml' => 'grxml',
'application/sru+xml' => 'sru',
'application/ssdl+xml' => 'ssdl',
'application/ssml+xml' => 'ssml',
'application/tei+xml' => 'tei',
'application/thraud+xml' => 'tfi',
'application/timestamped-data' => 'tsd',
'application/vnd.3gpp.pic-bw-large' => 'plb',
'application/vnd.3gpp.pic-bw-small' => 'psb',
'application/vnd.3gpp.pic-bw-var' => 'pvb',
'application/vnd.3gpp2.tcap' => 'tcap',
'application/vnd.3m.post-it-notes' => 'pwn',
'application/vnd.accpac.simply.aso' => 'aso',
'application/vnd.accpac.simply.imp' => 'imp',
'application/vnd.acucobol' => 'acu',
'application/vnd.acucorp' => 'atc',
'application/vnd.adobe.air-application-installer-package+zip' => 'air',
'application/vnd.adobe.formscentral.fcdt' => 'fcdt',
'application/vnd.adobe.fxp' => 'fxp',
'application/vnd.adobe.xdp+xml' => 'xdp',
'application/vnd.adobe.xfdf' => 'xfdf',
'application/vnd.ahead.space' => 'ahead',
'application/vnd.airzip.filesecure.azf' => 'azf',
'application/vnd.airzip.filesecure.azs' => 'azs',
'application/vnd.amazon.ebook' => 'azw',
'application/vnd.americandynamics.acc' => 'acc',
'application/vnd.amiga.ami' => 'ami',
'application/vnd.android.package-archive' => 'apk',
'application/vnd.anser-web-certificate-issue-initiation' => 'cii',
'application/vnd.anser-web-funds-transfer-initiation' => 'fti',
'application/vnd.antix.game-component' => 'atx',
'application/vnd.apple.installer+xml' => 'mpkg',
'application/vnd.apple.mpegurl' => 'm3u8',
'application/vnd.aristanetworks.swi' => 'swi',
'application/vnd.astraea-software.iota' => 'iota',
'application/vnd.audiograph' => 'aep',
'application/vnd.blueice.multipass' => 'mpm',
'application/vnd.bmi' => 'bmi',
'application/vnd.businessobjects' => 'rep',
'application/vnd.chemdraw+xml' => 'cdxml',
'application/vnd.chipnuts.karaoke-mmd' => 'mmd',
'application/vnd.cinderella' => 'cdy',
'application/vnd.claymore' => 'cla',
'application/vnd.cloanto.rp9' => 'rp9',
'application/vnd.clonk.c4group' => 'c4g',
'application/vnd.cluetrust.cartomobile-config' => 'c11amc',
'application/vnd.cluetrust.cartomobile-config-pkg' => 'c11amz',
'application/vnd.commonspace' => 'csp',
'application/vnd.contact.cmsg' => 'cdbcmsg',
'application/vnd.cosmocaller' => 'cmc',
'application/vnd.crick.clicker' => 'clkx',
'application/vnd.crick.clicker.keyboard' => 'clkk',
'application/vnd.crick.clicker.palette' => 'clkp',
'application/vnd.crick.clicker.template' => 'clkt',
'application/vnd.crick.clicker.wordbank' => 'clkw',
'application/vnd.criticaltools.wbs+xml' => 'wbs',
'application/vnd.ctc-posml' => 'pml',
'application/vnd.cups-ppd' => 'ppd',
'application/vnd.curl.car' => 'car',
'application/vnd.curl.pcurl' => 'pcurl',
'application/vnd.dart' => 'dart',
'application/vnd.data-vision.rdz' => 'rdz',
'application/vnd.dece.data' => 'uvf',
'application/vnd.dece.ttml+xml' => 'uvt',
'application/vnd.dece.unspecified' => 'uvx',
'application/vnd.dece.zip' => 'uvz',
'application/vnd.denovo.fcselayout-link' => 'fe_launch',
'application/vnd.dna' => 'dna',
'application/vnd.dolby.mlp' => 'mlp',
'application/vnd.dpgraph' => 'dpg',
'application/vnd.dreamfactory' => 'dfac',
'application/vnd.ds-keypoint' => 'kpxx',
'application/vnd.dvb.ait' => 'ait',
'application/vnd.dvb.service' => 'svc',
'application/vnd.dynageo' => 'geo',
'application/vnd.ecowin.chart' => 'mag',
'application/vnd.enliven' => 'nml',
'application/vnd.epson.esf' => 'esf',
'application/vnd.epson.msf' => 'msf',
'application/vnd.epson.quickanime' => 'qam',
'application/vnd.epson.salt' => 'slt',
'application/vnd.epson.ssf' => 'ssf',
'application/vnd.eszigno3+xml' => 'es3',
'application/vnd.ezpix-album' => 'ez2',
'application/vnd.ezpix-package' => 'ez3',
'application/vnd.fdf' => 'fdf',
'application/vnd.fdsn.mseed' => 'mseed',
'application/vnd.fdsn.seed' => 'seed',
'application/vnd.flographit' => 'gph',
'application/vnd.fluxtime.clip' => 'ftc',
'application/vnd.framemaker' => 'fm',
'application/vnd.frogans.fnc' => 'fnc',
'application/vnd.frogans.ltf' => 'ltf',
'application/vnd.fsc.weblaunch' => 'fsc',
'application/vnd.fujitsu.oasys' => 'oas',
'application/vnd.fujitsu.oasys2' => 'oa2',
'application/vnd.fujitsu.oasys3' => 'oa3',
'application/vnd.fujitsu.oasysgp' => 'fg5',
'application/vnd.fujitsu.oasysprs' => 'bh2',
'application/vnd.fujixerox.ddd' => 'ddd',
'application/vnd.fujixerox.docuworks' => 'xdw',
'application/vnd.fujixerox.docuworks.binder' => 'xbd',
'application/vnd.fuzzysheet' => 'fzs',
'application/vnd.genomatix.tuxedo' => 'txd',
'application/vnd.geogebra.file' => 'ggb',
'application/vnd.geogebra.tool' => 'ggt',
'application/vnd.geometry-explorer' => 'gex',
'application/vnd.geonext' => 'gxt',
'application/vnd.geoplan' => 'g2w',
'application/vnd.geospace' => 'g3w',
'application/vnd.gmx' => 'gmx',
'application/vnd.google-earth.kml+xml' => 'kml',
'application/vnd.google-earth.kmz' => 'kmz',
'application/vnd.grafeq' => 'gqf',
'application/vnd.groove-account' => 'gac',
'application/vnd.groove-help' => 'ghf',
'application/vnd.groove-identity-message' => 'gim',
'application/vnd.groove-injector' => 'grv',
'application/vnd.groove-tool-message' => 'gtm',
'application/vnd.groove-tool-template' => 'tpl',
'application/vnd.groove-vcard' => 'vcg',
'application/vnd.hal+xml' => 'hal',
'application/vnd.handheld-entertainment+xml' => 'zmm',
'application/vnd.hbci' => 'hbci',
'application/vnd.hhe.lesson-player' => 'les',
'application/vnd.hp-hpgl' => 'hpgl',
'application/vnd.hp-hpid' => 'hpid',
'application/vnd.hp-hps' => 'hps',
'application/vnd.hp-jlyt' => 'jlt',
'application/vnd.hp-pcl' => 'pcl',
'application/vnd.hp-pclxl' => 'pclxl',
'application/vnd.hydrostatix.sof-data' => 'sfd-hdstx',
'application/vnd.ibm.minipay' => 'mpy',
'application/vnd.ibm.modcap' => 'afp',
'application/vnd.ibm.rights-management' => 'irm',
'application/vnd.ibm.secure-container' => 'sc',
'application/vnd.iccprofile' => 'icc',
'application/vnd.igloader' => 'igl',
'application/vnd.immervision-ivp' => 'ivp',
'application/vnd.immervision-ivu' => 'ivu',
'application/vnd.insors.igm' => 'igm',
'application/vnd.intercon.formnet' => 'xpw',
'application/vnd.intergeo' => 'i2g',
'application/vnd.intu.qbo' => 'qbo',
'application/vnd.intu.qfx' => 'qfx',
'application/vnd.ipunplugged.rcprofile' => 'rcprofile',
'application/vnd.irepository.package+xml' => 'irp',
'application/vnd.is-xpr' => 'xpr',
'application/vnd.isac.fcs' => 'fcs',
'application/vnd.jam' => 'jam',
'application/vnd.jcp.javame.midlet-rms' => 'rms',
'application/vnd.jisp' => 'jisp',
'application/vnd.joost.joda-archive' => 'joda',
'application/vnd.kahootz' => 'ktz',
'application/vnd.kde.karbon' => 'karbon',
'application/vnd.kde.kchart' => 'chrt',
'application/vnd.kde.kformula' => 'kfo',
'application/vnd.kde.kivio' => 'flw',
'application/vnd.kde.kontour' => 'kon',
'application/vnd.kde.kpresenter' => 'kpr',
'application/vnd.kde.kspread' => 'ksp',
'application/vnd.kde.kword' => 'kwd',
'application/vnd.kenameaapp' => 'htke',
'application/vnd.kidspiration' => 'kia',
'application/vnd.kinar' => 'kne',
'application/vnd.koan' => 'skp',
'application/vnd.kodak-descriptor' => 'sse',
'application/vnd.las.las+xml' => 'lasxml',
'application/vnd.llamagraphics.life-balance.desktop' => 'lbd',
'application/vnd.llamagraphics.life-balance.exchange+xml' => 'lbe',
'application/vnd.lotus-1-2-3' => '123',
'application/vnd.lotus-approach' => 'apr',
'application/vnd.lotus-freelance' => 'pre',
'application/vnd.lotus-notes' => 'nsf',
'application/vnd.lotus-organizer' => 'org',
'application/vnd.lotus-screencam' => 'scm',
'application/vnd.lotus-wordpro' => 'lwp',
'application/vnd.macports.portpkg' => 'portpkg',
'application/vnd.mcd' => 'mcd',
'application/vnd.medcalcdata' => 'mc1',
'application/vnd.mediastation.cdkey' => 'cdkey',
'application/vnd.mfer' => 'mwf',
'application/vnd.mfmp' => 'mfm',
'application/vnd.micrografx.flo' => 'flo',
'application/vnd.micrografx.igx' => 'igx',
'application/vnd.mif' => 'mif',
'application/vnd.mobius.daf' => 'daf',
'application/vnd.mobius.dis' => 'dis',
'application/vnd.mobius.mbk' => 'mbk',
'application/vnd.mobius.mqy' => 'mqy',
'application/vnd.mobius.msl' => 'msl',
'application/vnd.mobius.plc' => 'plc',
'application/vnd.mobius.txf' => 'txf',
'application/vnd.mophun.application' => 'mpn',
'application/vnd.mophun.certificate' => 'mpc',
'application/vnd.mozilla.xul+xml' => 'xul',
'application/vnd.ms-artgalry' => 'cil',
'application/vnd.ms-cab-compressed' => 'cab',
'application/vnd.ms-excel' => 'xls',
'application/vnd.ms-excel.addin.macroenabled.12' => 'xlam',
'application/vnd.ms-excel.sheet.binary.macroenabled.12' => 'xlsb',
'application/vnd.ms-excel.sheet.macroenabled.12' => 'xlsm',
'application/vnd.ms-excel.template.macroenabled.12' => 'xltm',
'application/vnd.ms-fontobject' => 'eot',
'application/vnd.ms-htmlhelp' => 'chm',
'application/vnd.ms-ims' => 'ims',
'application/vnd.ms-lrm' => 'lrm',
'application/vnd.ms-officetheme' => 'thmx',
'application/vnd.ms-pki.seccat' => 'cat',
'application/vnd.ms-pki.stl' => 'stl',
'application/vnd.ms-powerpoint' => 'ppt',
'application/vnd.ms-powerpoint.addin.macroenabled.12' => 'ppam',
'application/vnd.ms-powerpoint.presentation.macroenabled.12' => 'pptm',
'application/vnd.ms-powerpoint.slide.macroenabled.12' => 'sldm',
'application/vnd.ms-powerpoint.slideshow.macroenabled.12' => 'ppsm',
'application/vnd.ms-powerpoint.template.macroenabled.12' => 'potm',
'application/vnd.ms-project' => 'mpp',
'application/vnd.ms-word.document.macroenabled.12' => 'docm',
'application/vnd.ms-word.template.macroenabled.12' => 'dotm',
'application/vnd.ms-works' => 'wps',
'application/vnd.ms-wpl' => 'wpl',
'application/vnd.ms-xpsdocument' => 'xps',
'application/vnd.mseq' => 'mseq',
'application/vnd.musician' => 'mus',
'application/vnd.muvee.style' => 'msty',
'application/vnd.mynfc' => 'taglet',
'application/vnd.neurolanguage.nlu' => 'nlu',
'application/vnd.nitf' => 'ntf',
'application/vnd.noblenet-directory' => 'nnd',
'application/vnd.noblenet-sealer' => 'nns',
'application/vnd.noblenet-web' => 'nnw',
'application/vnd.nokia.n-gage.data' => 'ngdat',
'application/vnd.nokia.n-gage.symbian.install' => 'n-gage',
'application/vnd.nokia.radio-preset' => 'rpst',
'application/vnd.nokia.radio-presets' => 'rpss',
'application/vnd.novadigm.edm' => 'edm',
'application/vnd.novadigm.edx' => 'edx',
'application/vnd.novadigm.ext' => 'ext',
'application/vnd.oasis.opendocument.chart' => 'odc',
'application/vnd.oasis.opendocument.chart-template' => 'otc',
'application/vnd.oasis.opendocument.database' => 'odb',
'application/vnd.oasis.opendocument.formula' => 'odf',
'application/vnd.oasis.opendocument.formula-template' => 'odft',
'application/vnd.oasis.opendocument.graphics' => 'odg',
'application/vnd.oasis.opendocument.graphics-template' => 'otg',
'application/vnd.oasis.opendocument.image' => 'odi',
'application/vnd.oasis.opendocument.image-template' => 'oti',
'application/vnd.oasis.opendocument.presentation' => 'odp',
'application/vnd.oasis.opendocument.presentation-template' => 'otp',
'application/vnd.oasis.opendocument.spreadsheet' => 'ods',
'application/vnd.oasis.opendocument.spreadsheet-template' => 'ots',
'application/vnd.oasis.opendocument.text' => 'odt',
'application/vnd.oasis.opendocument.text-master' => 'odm',
'application/vnd.oasis.opendocument.text-template' => 'ott',
'application/vnd.oasis.opendocument.text-web' => 'oth',
'application/vnd.olpc-sugar' => 'xo',
'application/vnd.oma.dd2+xml' => 'dd2',
'application/vnd.openofficeorg.extension' => 'oxt',
'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx',
'application/vnd.openxmlformats-officedocument.presentationml.slide' => 'sldx',
'application/vnd.openxmlformats-officedocument.presentationml.slideshow' => 'ppsx',
'application/vnd.openxmlformats-officedocument.presentationml.template' => 'potx',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
'application/vnd.openxmlformats-officedocument.spreadsheetml.template' => 'xltx',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
'application/vnd.openxmlformats-officedocument.wordprocessingml.template' => 'dotx',
'application/vnd.osgeo.mapguide.package' => 'mgp',
'application/vnd.osgi.dp' => 'dp',
'application/vnd.osgi.subsystem' => 'esa',
'application/vnd.palm' => 'pdb',
'application/vnd.pawaafile' => 'paw',
'application/vnd.pg.format' => 'str',
'application/vnd.pg.osasli' => 'ei6',
'application/vnd.picsel' => 'efif',
'application/vnd.pmi.widget' => 'wg',
'application/vnd.pocketlearn' => 'plf',
'application/vnd.powerbuilder6' => 'pbd',
'application/vnd.previewsystems.box' => 'box',
'application/vnd.proteus.magazine' => 'mgz',
'application/vnd.publishare-delta-tree' => 'qps',
'application/vnd.pvi.ptid1' => 'ptid',
'application/vnd.quark.quarkxpress' => 'qxd',
'application/vnd.realvnc.bed' => 'bed',
'application/vnd.recordare.musicxml' => 'mxl',
'application/vnd.recordare.musicxml+xml' => 'musicxml',
'application/vnd.rig.cryptonote' => 'cryptonote',
'application/vnd.rim.cod' => 'cod',
'application/vnd.rn-realmedia' => 'rm',
'application/vnd.rn-realmedia-vbr' => 'rmvb',
'application/vnd.route66.link66+xml' => 'link66',
'application/vnd.sailingtracker.track' => 'st',
'application/vnd.seemail' => 'see',
'application/vnd.sema' => 'sema',
'application/vnd.semd' => 'semd',
'application/vnd.semf' => 'semf',
'application/vnd.shana.informed.formdata' => 'ifm',
'application/vnd.shana.informed.formtemplate' => 'itp',
'application/vnd.shana.informed.interchange' => 'iif',
'application/vnd.shana.informed.package' => 'ipk',
'application/vnd.simtech-mindmapper' => 'twd',
'application/vnd.smaf' => 'mmf',
'application/vnd.smart.teacher' => 'teacher',
'application/vnd.solent.sdkm+xml' => 'sdkm',
'application/vnd.spotfire.dxp' => 'dxp',
'application/vnd.spotfire.sfs' => 'sfs',
'application/vnd.stardivision.calc' => 'sdc',
'application/vnd.stardivision.draw' => 'sda',
'application/vnd.stardivision.impress' => 'sdd',
'application/vnd.stardivision.math' => 'smf',
'application/vnd.stardivision.writer' => 'sdw',
'application/vnd.stardivision.writer-global' => 'sgl',
'application/vnd.stepmania.package' => 'smzip',
'application/vnd.stepmania.stepchart' => 'sm',
'application/vnd.sun.xml.calc' => 'sxc',
'application/vnd.sun.xml.calc.template' => 'stc',
'application/vnd.sun.xml.draw' => 'sxd',
'application/vnd.sun.xml.draw.template' => 'std',
'application/vnd.sun.xml.impress' => 'sxi',
'application/vnd.sun.xml.impress.template' => 'sti',
'application/vnd.sun.xml.math' => 'sxm',
'application/vnd.sun.xml.writer' => 'sxw',
'application/vnd.sun.xml.writer.global' => 'sxg',
'application/vnd.sun.xml.writer.template' => 'stw',
'application/vnd.sus-calendar' => 'sus',
'application/vnd.svd' => 'svd',
'application/vnd.symbian.install' => 'sis',
'application/vnd.syncml+xml' => 'xsm',
'application/vnd.syncml.dm+wbxml' => 'bdm',
'application/vnd.syncml.dm+xml' => 'xdm',
'application/vnd.tao.intent-module-archive' => 'tao',
'application/vnd.tcpdump.pcap' => 'pcap',
'application/vnd.tmobile-livetv' => 'tmo',
'application/vnd.trid.tpt' => 'tpt',
'application/vnd.triscape.mxs' => 'mxs',
'application/vnd.trueapp' => 'tra',
'application/vnd.ufdl' => 'ufd',
'application/vnd.uiq.theme' => 'utz',
'application/vnd.umajin' => 'umj',
'application/vnd.unity' => 'unityweb',
'application/vnd.uoml+xml' => 'uoml',
'application/vnd.vcx' => 'vcx',
'application/vnd.visio' => 'vsd',
'application/vnd.visionary' => 'vis',
'application/vnd.vsf' => 'vsf',
'application/vnd.wap.wbxml' => 'wbxml',
'application/vnd.wap.wmlc' => 'wmlc',
'application/vnd.wap.wmlscriptc' => 'wmlsc',
'application/vnd.webturbo' => 'wtb',
'application/vnd.wolfram.player' => 'nbp',
'application/vnd.wordperfect' => 'wpd',
'application/vnd.wqd' => 'wqd',
'application/vnd.wt.stf' => 'stf',
'application/vnd.xara' => 'xar',
'application/vnd.xfdl' => 'xfdl',
'application/vnd.yamaha.hv-dic' => 'hvd',
'application/vnd.yamaha.hv-script' => 'hvs',
'application/vnd.yamaha.hv-voice' => 'hvp',
'application/vnd.yamaha.openscoreformat' => 'osf',
'application/vnd.yamaha.openscoreformat.osfpvg+xml' => 'osfpvg',
'application/vnd.yamaha.smaf-audio' => 'saf',
'application/vnd.yamaha.smaf-phrase' => 'spf',
'application/vnd.yellowriver-custom-menu' => 'cmp',
'application/vnd.zul' => 'zir',
'application/vnd.zzazz.deck+xml' => 'zaz',
'application/voicexml+xml' => 'vxml',
'application/widget' => 'wgt',
'application/winhlp' => 'hlp',
'application/wsdl+xml' => 'wsdl',
'application/wspolicy+xml' => 'wspolicy',
'application/x-7z-compressed' => '7z',
'application/x-abiword' => 'abw',
'application/x-ace-compressed' => 'ace',
'application/x-apple-diskimage' => 'dmg',
'application/x-authorware-bin' => 'aab',
'application/x-authorware-map' => 'aam',
'application/x-authorware-seg' => 'aas',
'application/x-bcpio' => 'bcpio',
'application/x-bittorrent' => 'torrent',
'application/x-blorb' => 'blb',
'application/x-bzip' => 'bz',
'application/x-bzip2' => 'bz2',
'application/x-cbr' => 'cbr',
'application/x-cdlink' => 'vcd',
'application/x-cfs-compressed' => 'cfs',
'application/x-chat' => 'chat',
'application/x-chess-pgn' => 'pgn',
'application/x-conference' => 'nsc',
'application/x-cpio' => 'cpio',
'application/x-csh' => 'csh',
'application/x-debian-package' => 'deb',
'application/x-dgc-compressed' => 'dgc',
'application/x-director' => 'dir',
'application/x-doom' => 'wad',
'application/x-dtbncx+xml' => 'ncx',
'application/x-dtbook+xml' => 'dtb',
'application/x-dtbresource+xml' => 'res',
'application/x-dvi' => 'dvi',
'application/x-envoy' => 'evy',
'application/x-eva' => 'eva',
'application/x-font-bdf' => 'bdf',
'application/x-font-ghostscript' => 'gsf',
'application/x-font-linux-psf' => 'psf',
'application/x-font-otf' => 'otf',
'application/x-font-pcf' => 'pcf',
'application/x-font-snf' => 'snf',
'application/x-font-ttf' => 'ttf',
'application/x-font-type1' => 'pfa',
'application/x-font-woff' => 'woff',
'application/x-freearc' => 'arc',
'application/x-futuresplash' => 'spl',
'application/x-gca-compressed' => 'gca',
'application/x-glulx' => 'ulx',
'application/x-gnumeric' => 'gnumeric',
'application/x-gramps-xml' => 'gramps',
'application/x-gtar' => 'gtar',
'application/x-hdf' => 'hdf',
'application/x-install-instructions' => 'install',
'application/x-iso9660-image' => 'iso',
'application/x-java-jnlp-file' => 'jnlp',
'application/x-latex' => 'latex',
'application/x-lzh-compressed' => 'lzh',
'application/x-mie' => 'mie',
'application/x-mobipocket-ebook' => 'prc',
'application/x-ms-application' => 'application',
'application/x-ms-shortcut' => 'lnk',
'application/x-ms-wmd' => 'wmd',
'application/x-ms-wmz' => 'wmz',
'application/x-ms-xbap' => 'xbap',
'application/x-msaccess' => 'mdb',
'application/x-msbinder' => 'obd',
'application/x-mscardfile' => 'crd',
'application/x-msclip' => 'clp',
'application/x-msdownload' => 'exe',
'application/x-msmediaview' => 'mvb',
'application/x-msmetafile' => 'wmf',
'application/x-msmoney' => 'mny',
'application/x-mspublisher' => 'pub',
'application/x-msschedule' => 'scd',
'application/x-msterminal' => 'trm',
'application/x-mswrite' => 'wri',
'application/x-netcdf' => 'nc',
'application/x-nzb' => 'nzb',
'application/x-pkcs12' => 'p12',
'application/x-pkcs7-certificates' => 'p7b',
'application/x-pkcs7-certreqresp' => 'p7r',
'application/x-rar-compressed' => 'rar',
'application/x-rar' => 'rar',
'application/x-research-info-systems' => 'ris',
'application/x-sh' => 'sh',
'application/x-shar' => 'shar',
'application/x-shockwave-flash' => 'swf',
'application/x-silverlight-app' => 'xap',
'application/x-sql' => 'sql',
'application/x-stuffit' => 'sit',
'application/x-stuffitx' => 'sitx',
'application/x-subrip' => 'srt',
'application/x-sv4cpio' => 'sv4cpio',
'application/x-sv4crc' => 'sv4crc',
'application/x-t3vm-image' => 't3',
'application/x-tads' => 'gam',
'application/x-tar' => 'tar',
'application/x-tcl' => 'tcl',
'application/x-tex' => 'tex',
'application/x-tex-tfm' => 'tfm',
'application/x-texinfo' => 'texinfo',
'application/x-tgif' => 'obj',
'application/x-ustar' => 'ustar',
'application/x-wais-source' => 'src',
'application/x-x509-ca-cert' => 'der',
'application/x-xfig' => 'fig',
'application/x-xliff+xml' => 'xlf',
'application/x-xpinstall' => 'xpi',
'application/x-xz' => 'xz',
'application/x-zmachine' => 'z1',
'application/xaml+xml' => 'xaml',
'application/xcap-diff+xml' => 'xdf',
'application/xenc+xml' => 'xenc',
'application/xhtml+xml' => 'xhtml',
'application/xml' => 'xml',
'application/xml-dtd' => 'dtd',
'application/xop+xml' => 'xop',
'application/xproc+xml' => 'xpl',
'application/xslt+xml' => 'xslt',
'application/xspf+xml' => 'xspf',
'application/xv+xml' => 'mxml',
'application/yang' => 'yang',
'application/yin+xml' => 'yin',
'application/zip' => 'zip',
'audio/adpcm' => 'adp',
'audio/basic' => 'au',
'audio/midi' => 'mid',
'audio/mp4' => 'mp4a',
'audio/mpeg' => 'mpga',
'audio/ogg' => 'oga',
'audio/s3m' => 's3m',
'audio/silk' => 'sil',
'audio/vnd.dece.audio' => 'uva',
'audio/vnd.digital-winds' => 'eol',
'audio/vnd.dra' => 'dra',
'audio/vnd.dts' => 'dts',
'audio/vnd.dts.hd' => 'dtshd',
'audio/vnd.lucent.voice' => 'lvp',
'audio/vnd.ms-playready.media.pya' => 'pya',
'audio/vnd.nuera.ecelp4800' => 'ecelp4800',
'audio/vnd.nuera.ecelp7470' => 'ecelp7470',
'audio/vnd.nuera.ecelp9600' => 'ecelp9600',
'audio/vnd.rip' => 'rip',
'audio/webm' => 'weba',
'audio/x-aac' => 'aac',
'audio/x-aiff' => 'aif',
'audio/x-caf' => 'caf',
'audio/x-flac' => 'flac',
'audio/x-matroska' => 'mka',
'audio/x-mpegurl' => 'm3u',
'audio/x-ms-wax' => 'wax',
'audio/x-ms-wma' => 'wma',
'audio/x-pn-realaudio' => 'ram',
'audio/x-pn-realaudio-plugin' => 'rmp',
'audio/x-wav' => 'wav',
'audio/xm' => 'xm',
'chemical/x-cdx' => 'cdx',
'chemical/x-cif' => 'cif',
'chemical/x-cmdf' => 'cmdf',
'chemical/x-cml' => 'cml',
'chemical/x-csml' => 'csml',
'chemical/x-xyz' => 'xyz',
'image/bmp' => 'bmp',
'image/x-ms-bmp' => 'bmp',
'image/cgm' => 'cgm',
'image/g3fax' => 'g3',
'image/gif' => 'gif',
'image/ief' => 'ief',
'image/jpeg' => 'jpeg',
'image/pjpeg' => 'jpeg',
'image/ktx' => 'ktx',
'image/png' => 'png',
'image/prs.btif' => 'btif',
'image/sgi' => 'sgi',
'image/svg+xml' => 'svg',
'image/tiff' => 'tiff',
'image/vnd.adobe.photoshop' => 'psd',
'image/vnd.dece.graphic' => 'uvi',
'image/vnd.dvb.subtitle' => 'sub',
'image/vnd.djvu' => 'djvu',
'image/vnd.dwg' => 'dwg',
'image/vnd.dxf' => 'dxf',
'image/vnd.fastbidsheet' => 'fbs',
'image/vnd.fpx' => 'fpx',
'image/vnd.fst' => 'fst',
'image/vnd.fujixerox.edmics-mmr' => 'mmr',
'image/vnd.fujixerox.edmics-rlc' => 'rlc',
'image/vnd.ms-modi' => 'mdi',
'image/vnd.ms-photo' => 'wdp',
'image/vnd.net-fpx' => 'npx',
'image/vnd.wap.wbmp' => 'wbmp',
'image/vnd.xiff' => 'xif',
'image/webp' => 'webp',
'image/x-3ds' => '3ds',
'image/x-cmu-raster' => 'ras',
'image/x-cmx' => 'cmx',
'image/x-freehand' => 'fh',
'image/x-icon' => 'ico',
'image/x-mrsid-image' => 'sid',
'image/x-pcx' => 'pcx',
'image/x-pict' => 'pic',
'image/x-portable-anymap' => 'pnm',
'image/x-portable-bitmap' => 'pbm',
'image/x-portable-graymap' => 'pgm',
'image/x-portable-pixmap' => 'ppm',
'image/x-rgb' => 'rgb',
'image/x-tga' => 'tga',
'image/x-xbitmap' => 'xbm',
'image/x-xpixmap' => 'xpm',
'image/x-xwindowdump' => 'xwd',
'message/rfc822' => 'eml',
'model/iges' => 'igs',
'model/mesh' => 'msh',
'model/vnd.collada+xml' => 'dae',
'model/vnd.dwf' => 'dwf',
'model/vnd.gdl' => 'gdl',
'model/vnd.gtw' => 'gtw',
'model/vnd.mts' => 'mts',
'model/vnd.vtu' => 'vtu',
'model/vrml' => 'wrl',
'model/x3d+binary' => 'x3db',
'model/x3d+vrml' => 'x3dv',
'model/x3d+xml' => 'x3d',
'text/cache-manifest' => 'appcache',
'text/calendar' => 'ics',
'text/css' => 'css',
'text/csv' => 'csv',
'text/html' => 'html',
'text/n3' => 'n3',
'text/plain' => 'txt',
'text/prs.lines.tag' => 'dsc',
'text/richtext' => 'rtx',
'text/rtf' => 'rtf',
'text/sgml' => 'sgml',
'text/tab-separated-values' => 'tsv',
'text/troff' => 't',
'text/turtle' => 'ttl',
'text/uri-list' => 'uri',
'text/vcard' => 'vcard',
'text/vnd.curl' => 'curl',
'text/vnd.curl.dcurl' => 'dcurl',
'text/vnd.curl.scurl' => 'scurl',
'text/vnd.curl.mcurl' => 'mcurl',
'text/vnd.dvb.subtitle' => 'sub',
'text/vnd.fly' => 'fly',
'text/vnd.fmi.flexstor' => 'flx',
'text/vnd.graphviz' => 'gv',
'text/vnd.in3d.3dml' => '3dml',
'text/vnd.in3d.spot' => 'spot',
'text/vnd.sun.j2me.app-descriptor' => 'jad',
'text/vnd.wap.wml' => 'wml',
'text/vnd.wap.wmlscript' => 'wmls',
'text/vtt' => 'vtt',
'text/x-asm' => 's',
'text/x-c' => 'c',
'text/x-fortran' => 'f',
'text/x-pascal' => 'p',
'text/x-java-source' => 'java',
'text/x-opml' => 'opml',
'text/x-nfo' => 'nfo',
'text/x-setext' => 'etx',
'text/x-sfv' => 'sfv',
'text/x-uuencode' => 'uu',
'text/x-vcalendar' => 'vcs',
'text/x-vcard' => 'vcf',
'video/3gpp' => '3gp',
'video/3gpp2' => '3g2',
'video/h261' => 'h261',
'video/h263' => 'h263',
'video/h264' => 'h264',
'video/jpeg' => 'jpgv',
'video/jpm' => 'jpm',
'video/mj2' => 'mj2',
'video/mp4' => 'mp4',
'video/mpeg' => 'mpeg',
'video/ogg' => 'ogv',
'video/quicktime' => 'qt',
'video/vnd.dece.hd' => 'uvh',
'video/vnd.dece.mobile' => 'uvm',
'video/vnd.dece.pd' => 'uvp',
'video/vnd.dece.sd' => 'uvs',
'video/vnd.dece.video' => 'uvv',
'video/vnd.dvb.file' => 'dvb',
'video/vnd.fvt' => 'fvt',
'video/vnd.mpegurl' => 'mxu',
'video/vnd.ms-playready.media.pyv' => 'pyv',
'video/vnd.uvvu.mp4' => 'uvu',
'video/vnd.vivo' => 'viv',
'video/webm' => 'webm',
'video/x-f4v' => 'f4v',
'video/x-fli' => 'fli',
'video/x-flv' => 'flv',
'video/x-m4v' => 'm4v',
'video/x-matroska' => 'mkv',
'video/x-mng' => 'mng',
'video/x-ms-asf' => 'asf',
'video/x-ms-vob' => 'vob',
'video/x-ms-wm' => 'wm',
'video/x-ms-wmv' => 'wmv',
'video/x-ms-wmx' => 'wmx',
'video/x-ms-wvx' => 'wvx',
'video/x-msvideo' => 'avi',
'video/x-sgi-movie' => 'movie',
'video/x-smv' => 'smv',
'x-conference/x-cooltalk' => 'ice',
);
/**
* {@inheritdoc}
*/
public function guess($mimeType)
{
return isset($this->defaultExtensions[$mimeType]) ? $this->defaultExtensions[$mimeType] : null;
}
}

View File

@@ -1,144 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\File\MimeType;
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException;
/**
* A singleton mime type guesser.
*
* By default, all mime type guessers provided by the framework are installed
* (if available on the current OS/PHP setup).
*
* You can register custom guessers by calling the register() method on the
* singleton instance. Custom guessers are always called before any default ones.
*
* $guesser = MimeTypeGuesser::getInstance();
* $guesser->register(new MyCustomMimeTypeGuesser());
*
* If you want to change the order of the default guessers, just re-register your
* preferred one as a custom one. The last registered guesser is preferred over
* previously registered ones.
*
* Re-registering a built-in guesser also allows you to configure it:
*
* $guesser = MimeTypeGuesser::getInstance();
* $guesser->register(new FileinfoMimeTypeGuesser('/path/to/magic/file'));
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class MimeTypeGuesser implements MimeTypeGuesserInterface
{
/**
* The singleton instance.
*
* @var MimeTypeGuesser
*/
private static $instance = null;
/**
* All registered MimeTypeGuesserInterface instances.
*
* @var array
*/
protected $guessers = array();
/**
* Returns the singleton instance.
*
* @return self
*/
public static function getInstance()
{
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Resets the singleton instance.
*/
public static function reset()
{
self::$instance = null;
}
/**
* Registers all natively provided mime type guessers.
*/
private function __construct()
{
if (FileBinaryMimeTypeGuesser::isSupported()) {
$this->register(new FileBinaryMimeTypeGuesser());
}
if (FileinfoMimeTypeGuesser::isSupported()) {
$this->register(new FileinfoMimeTypeGuesser());
}
}
/**
* Registers a new mime type guesser.
*
* When guessing, this guesser is preferred over previously registered ones.
*
* @param MimeTypeGuesserInterface $guesser
*/
public function register(MimeTypeGuesserInterface $guesser)
{
array_unshift($this->guessers, $guesser);
}
/**
* Tries to guess the mime type of the given file.
*
* The file is passed to each registered mime type guesser in reverse order
* of their registration (last registered is queried first). Once a guesser
* returns a value that is not NULL, this method terminates and returns the
* value.
*
* @param string $path The path to the file
*
* @return string The mime type or NULL, if none could be guessed
*
* @throws \LogicException
* @throws FileNotFoundException
* @throws AccessDeniedException
*/
public function guess($path)
{
if (!is_file($path)) {
throw new FileNotFoundException($path);
}
if (!is_readable($path)) {
throw new AccessDeniedException($path);
}
if (!$this->guessers) {
$msg = 'Unable to guess the mime type as no guessers are available';
if (!FileinfoMimeTypeGuesser::isSupported()) {
$msg .= ' (Did you enable the php_fileinfo extension?)';
}
throw new \LogicException($msg);
}
foreach ($this->guessers as $guesser) {
if (null !== $mimeType = $guesser->guess($path)) {
return $mimeType;
}
}
}
}

View File

@@ -1,35 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\File\MimeType;
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException;
/**
* Guesses the mime type of a file.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface MimeTypeGuesserInterface
{
/**
* Guesses the mime type of the file with the given path.
*
* @param string $path The path to the file
*
* @return string The mime type or NULL, if none could be guessed
*
* @throws FileNotFoundException If the file does not exist
* @throws AccessDeniedException If the file could not be read
*/
public function guess($path);
}

View File

@@ -18,10 +18,7 @@ namespace Symfony\Component\HttpFoundation\File;
*/
class Stream extends File
{
/**
* {@inheritdoc}
*/
public function getSize()
public function getSize(): int|false
{
return false;
}

View File

@@ -11,9 +11,16 @@
namespace Symfony\Component\HttpFoundation\File;
use Symfony\Component\HttpFoundation\File\Exception\CannotWriteFileException;
use Symfony\Component\HttpFoundation\File\Exception\ExtensionFileException;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
use Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser;
use Symfony\Component\HttpFoundation\File\Exception\FormSizeFileException;
use Symfony\Component\HttpFoundation\File\Exception\IniSizeFileException;
use Symfony\Component\HttpFoundation\File\Exception\NoFileException;
use Symfony\Component\HttpFoundation\File\Exception\NoTmpDirFileException;
use Symfony\Component\HttpFoundation\File\Exception\PartialFileException;
use Symfony\Component\Mime\MimeTypes;
/**
* A file uploaded through a form.
@@ -24,42 +31,10 @@ use Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser;
*/
class UploadedFile extends File
{
/**
* Whether the test mode is activated.
*
* Local files are used in test mode hence the code should not enforce HTTP uploads.
*
* @var bool
*/
private $test = false;
/**
* The original name of the uploaded file.
*
* @var string
*/
private $originalName;
/**
* The mime type provided by the uploader.
*
* @var string
*/
private $mimeType;
/**
* The file size provided by the uploader.
*
* @var int|null
*/
private $size;
/**
* The UPLOAD_ERR_XXX constant provided by the uploader.
*
* @var int
*/
private $error;
private bool $test;
private string $originalName;
private string $mimeType;
private int $error;
/**
* Accepts the information of the uploaded file as provided by the PHP global $_FILES.
@@ -76,24 +51,23 @@ class UploadedFile extends File
* Calling any other method on an non-valid instance will cause an unpredictable result.
*
* @param string $path The full temporary path to the file
* @param string $originalName The original file name
* @param string $originalName The original file name of the uploaded file
* @param string|null $mimeType The type of the file as provided by PHP; null defaults to application/octet-stream
* @param int|null $size The file size
* @param int|null $error The error constant of the upload (one of PHP's UPLOAD_ERR_XXX constants); null defaults to UPLOAD_ERR_OK
* @param bool $test Whether the test mode is active
* Local files are used in test mode hence the code should not enforce HTTP uploads
*
* @throws FileException If file_uploads is disabled
* @throws FileNotFoundException If the file does not exist
*/
public function __construct($path, $originalName, $mimeType = null, $size = null, $error = null, $test = false)
public function __construct(string $path, string $originalName, string $mimeType = null, int $error = null, bool $test = false)
{
$this->originalName = $this->getName($originalName);
$this->mimeType = $mimeType ?: 'application/octet-stream';
$this->size = $size;
$this->error = $error ?: UPLOAD_ERR_OK;
$this->test = (bool) $test;
$this->error = $error ?: \UPLOAD_ERR_OK;
$this->test = $test;
parent::__construct($path, UPLOAD_ERR_OK === $this->error);
parent::__construct($path, \UPLOAD_ERR_OK === $this->error);
}
/**
@@ -101,10 +75,8 @@ class UploadedFile extends File
*
* It is extracted from the request from which the file has been uploaded.
* Then it should not be considered as a safe value.
*
* @return string|null The original name
*/
public function getClientOriginalName()
public function getClientOriginalName(): string
{
return $this->originalName;
}
@@ -114,12 +86,10 @@ class UploadedFile extends File
*
* It is extracted from the original file name that was uploaded.
* Then it should not be considered as a safe value.
*
* @return string The extension
*/
public function getClientOriginalExtension()
public function getClientOriginalExtension(): string
{
return pathinfo($this->originalName, PATHINFO_EXTENSION);
return pathinfo($this->originalName, \PATHINFO_EXTENSION);
}
/**
@@ -131,11 +101,9 @@ class UploadedFile extends File
* For a trusted mime type, use getMimeType() instead (which guesses the mime
* type based on the file content).
*
* @return string|null The mime type
*
* @see getMimeType()
*/
public function getClientMimeType()
public function getClientMimeType(): string
{
return $this->mimeType;
}
@@ -152,30 +120,16 @@ class UploadedFile extends File
* For a trusted extension, use guessExtension() instead (which guesses
* the extension based on the guessed mime type for the file).
*
* @return string|null The guessed extension or null if it cannot be guessed
*
* @see guessExtension()
* @see getClientMimeType()
*/
public function guessClientExtension()
public function guessClientExtension(): ?string
{
$type = $this->getClientMimeType();
$guesser = ExtensionGuesser::getInstance();
if (!class_exists(MimeTypes::class)) {
throw new \LogicException('You cannot guess the extension as the Mime component is not installed. Try running "composer require symfony/mime".');
}
return $guesser->guess($type);
}
/**
* Returns the file size.
*
* It is extracted from the request from which the file has been uploaded.
* Then it should not be considered as a safe value.
*
* @return int|null The file size
*/
public function getClientSize()
{
return $this->size;
return MimeTypes::getDefault()->getExtensions($this->getClientMimeType())[0] ?? null;
}
/**
@@ -183,22 +137,18 @@ class UploadedFile extends File
*
* If the upload was successful, the constant UPLOAD_ERR_OK is returned.
* Otherwise one of the other UPLOAD_ERR_XXX constants is returned.
*
* @return int The upload error
*/
public function getError()
public function getError(): int
{
return $this->error;
}
/**
* Returns whether the file was uploaded successfully.
*
* @return bool True if the file has been uploaded with HTTP and no error occurred
* Returns whether the file has been uploaded with HTTP and no error occurred.
*/
public function isValid()
public function isValid(): bool
{
$isOk = $this->error === UPLOAD_ERR_OK;
$isOk = \UPLOAD_ERR_OK === $this->error;
return $this->test ? $isOk : $isOk && is_uploaded_file($this->getPathname());
}
@@ -206,14 +156,9 @@ class UploadedFile extends File
/**
* Moves the file to a new location.
*
* @param string $directory The destination folder
* @param string $name The new file name
*
* @return File A File object representing the new file
*
* @throws FileException if, for any reason, the file could not have been moved
*/
public function move($directory, $name = null)
public function move(string $directory, string $name = null): File
{
if ($this->isValid()) {
if ($this->test) {
@@ -222,9 +167,14 @@ class UploadedFile extends File
$target = $this->getTargetFile($directory, $name);
if (!@move_uploaded_file($this->getPathname(), $target)) {
$error = error_get_last();
throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $this->getPathname(), $target, strip_tags($error['message'])));
set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; });
try {
$moved = move_uploaded_file($this->getPathname(), $target);
} finally {
restore_error_handler();
}
if (!$moved) {
throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s).', $this->getPathname(), $target, strip_tags($error)));
}
@chmod($target, 0666 & ~umask());
@@ -232,35 +182,63 @@ class UploadedFile extends File
return $target;
}
switch ($this->error) {
case \UPLOAD_ERR_INI_SIZE:
throw new IniSizeFileException($this->getErrorMessage());
case \UPLOAD_ERR_FORM_SIZE:
throw new FormSizeFileException($this->getErrorMessage());
case \UPLOAD_ERR_PARTIAL:
throw new PartialFileException($this->getErrorMessage());
case \UPLOAD_ERR_NO_FILE:
throw new NoFileException($this->getErrorMessage());
case \UPLOAD_ERR_CANT_WRITE:
throw new CannotWriteFileException($this->getErrorMessage());
case \UPLOAD_ERR_NO_TMP_DIR:
throw new NoTmpDirFileException($this->getErrorMessage());
case \UPLOAD_ERR_EXTENSION:
throw new ExtensionFileException($this->getErrorMessage());
}
throw new FileException($this->getErrorMessage());
}
/**
* Returns the maximum size of an uploaded file as configured in php.ini.
*
* @return int The maximum size of an uploaded file in bytes
* @return int|float The maximum size of an uploaded file in bytes (returns float if size > PHP_INT_MAX)
*/
public static function getMaxFilesize()
public static function getMaxFilesize(): int|float
{
$iniMax = strtolower(ini_get('upload_max_filesize'));
$sizePostMax = self::parseFilesize(\ini_get('post_max_size'));
$sizeUploadMax = self::parseFilesize(\ini_get('upload_max_filesize'));
if ('' === $iniMax) {
return PHP_INT_MAX;
return min($sizePostMax ?: \PHP_INT_MAX, $sizeUploadMax ?: \PHP_INT_MAX);
}
private static function parseFilesize(string $size): int|float
{
if ('' === $size) {
return 0;
}
$max = ltrim($iniMax, '+');
if (0 === strpos($max, '0x')) {
$max = intval($max, 16);
} elseif (0 === strpos($max, '0')) {
$max = intval($max, 8);
$size = strtolower($size);
$max = ltrim($size, '+');
if (str_starts_with($max, '0x')) {
$max = \intval($max, 16);
} elseif (str_starts_with($max, '0')) {
$max = \intval($max, 8);
} else {
$max = (int) $max;
}
switch (substr($iniMax, -1)) {
switch (substr($size, -1)) {
case 't': $max *= 1024;
// no break
case 'g': $max *= 1024;
// no break
case 'm': $max *= 1024;
// no break
case 'k': $max *= 1024;
}
@@ -269,24 +247,22 @@ class UploadedFile extends File
/**
* Returns an informative upload error message.
*
* @return string The error message regarding the specified error code
*/
public function getErrorMessage()
public function getErrorMessage(): string
{
static $errors = array(
UPLOAD_ERR_INI_SIZE => 'The file "%s" exceeds your upload_max_filesize ini directive (limit is %d KiB).',
UPLOAD_ERR_FORM_SIZE => 'The file "%s" exceeds the upload limit defined in your form.',
UPLOAD_ERR_PARTIAL => 'The file "%s" was only partially uploaded.',
UPLOAD_ERR_NO_FILE => 'No file was uploaded.',
UPLOAD_ERR_CANT_WRITE => 'The file "%s" could not be written on disk.',
UPLOAD_ERR_NO_TMP_DIR => 'File could not be uploaded: missing temporary directory.',
UPLOAD_ERR_EXTENSION => 'File upload was stopped by a PHP extension.',
);
static $errors = [
\UPLOAD_ERR_INI_SIZE => 'The file "%s" exceeds your upload_max_filesize ini directive (limit is %d KiB).',
\UPLOAD_ERR_FORM_SIZE => 'The file "%s" exceeds the upload limit defined in your form.',
\UPLOAD_ERR_PARTIAL => 'The file "%s" was only partially uploaded.',
\UPLOAD_ERR_NO_FILE => 'No file was uploaded.',
\UPLOAD_ERR_CANT_WRITE => 'The file "%s" could not be written on disk.',
\UPLOAD_ERR_NO_TMP_DIR => 'File could not be uploaded: missing temporary directory.',
\UPLOAD_ERR_EXTENSION => 'File upload was stopped by a PHP extension.',
];
$errorCode = $this->error;
$maxFilesize = $errorCode === UPLOAD_ERR_INI_SIZE ? self::getMaxFilesize() / 1024 : 0;
$message = isset($errors[$errorCode]) ? $errors[$errorCode] : 'The file "%s" was not uploaded due to an unknown error.';
$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);
}

View File

@@ -21,14 +21,12 @@ use Symfony\Component\HttpFoundation\File\UploadedFile;
*/
class FileBag extends ParameterBag
{
private static $fileKeys = array('error', 'name', 'size', 'tmp_name', 'type');
private const FILE_KEYS = ['error', 'name', 'size', 'tmp_name', 'type'];
/**
* Constructor.
*
* @param array $parameters An array of HTTP files
* @param array|UploadedFile[] $parameters An array of HTTP files
*/
public function __construct(array $parameters = array())
public function __construct(array $parameters = [])
{
$this->replace($parameters);
}
@@ -36,18 +34,18 @@ class FileBag extends ParameterBag
/**
* {@inheritdoc}
*/
public function replace(array $files = array())
public function replace(array $files = [])
{
$this->parameters = array();
$this->parameters = [];
$this->add($files);
}
/**
* {@inheritdoc}
*/
public function set($key, $value)
public function set(string $key, mixed $value)
{
if (!is_array($value) && !$value instanceof UploadedFile) {
if (!\is_array($value) && !$value instanceof UploadedFile) {
throw new \InvalidArgumentException('An uploaded file must be an array or an instance of UploadedFile.');
}
@@ -57,7 +55,7 @@ class FileBag extends ParameterBag
/**
* {@inheritdoc}
*/
public function add(array $files = array())
public function add(array $files = [])
{
foreach ($files as $key => $file) {
$this->set($key, $file);
@@ -67,29 +65,28 @@ class FileBag extends ParameterBag
/**
* Converts uploaded files to UploadedFile instances.
*
* @param array|UploadedFile $file A (multi-dimensional) array of uploaded file information
*
* @return UploadedFile|UploadedFile[] A (multi-dimensional) array of UploadedFile instances
* @return UploadedFile[]|UploadedFile|null
*/
protected function convertFileInformation($file)
protected function convertFileInformation(array|UploadedFile $file): array|UploadedFile|null
{
if ($file instanceof UploadedFile) {
return $file;
}
$file = $this->fixPhpFilesArray($file);
if (is_array($file)) {
$keys = array_keys($file);
sort($keys);
$keys = array_keys($file);
sort($keys);
if ($keys == self::$fileKeys) {
if (UPLOAD_ERR_NO_FILE == $file['error']) {
$file = null;
} else {
$file = new UploadedFile($file['tmp_name'], $file['name'], $file['type'], $file['size'], $file['error']);
}
if (self::FILE_KEYS == $keys) {
if (\UPLOAD_ERR_NO_FILE == $file['error']) {
$file = null;
} else {
$file = array_map(array($this, 'convertFileInformation'), $file);
$file = new UploadedFile($file['tmp_name'], $file['name'], $file['type'], $file['error'], false);
}
} else {
$file = array_map(function ($v) { return $v instanceof UploadedFile || \is_array($v) ? $this->convertFileInformation($v) : $v; }, $file);
if (array_keys($keys) === $keys) {
$file = array_filter($file);
}
}
@@ -107,37 +104,31 @@ class FileBag extends ParameterBag
*
* It's safe to pass an already converted array, in which case this method
* just returns the original array unmodified.
*
* @param array $data
*
* @return array
*/
protected function fixPhpFilesArray($data)
protected function fixPhpFilesArray(array $data): array
{
if (!is_array($data)) {
return $data;
}
// Remove extra key added by PHP 8.1.
unset($data['full_path']);
$keys = array_keys($data);
sort($keys);
if (self::$fileKeys != $keys || !isset($data['name']) || !is_array($data['name'])) {
if (self::FILE_KEYS != $keys || !isset($data['name']) || !\is_array($data['name'])) {
return $data;
}
$files = $data;
foreach (self::$fileKeys as $k) {
foreach (self::FILE_KEYS as $k) {
unset($files[$k]);
}
foreach ($data['name'] as $key => $name) {
$files[$key] = $this->fixPhpFilesArray(array(
$files[$key] = $this->fixPhpFilesArray([
'error' => $data['error'][$key],
'name' => $name,
'type' => $data['type'][$key],
'tmp_name' => $data['tmp_name'][$key],
'size' => $data['size'][$key],
));
]);
}
return $files;

View File

@@ -15,18 +15,21 @@ namespace Symfony\Component\HttpFoundation;
* HeaderBag is a container for HTTP headers.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @implements \IteratorAggregate<string, list<string|null>>
*/
class HeaderBag implements \IteratorAggregate, \Countable
{
protected $headers = array();
protected $cacheControl = array();
protected const UPPER = '_ABCDEFGHIJKLMNOPQRSTUVWXYZ';
protected const LOWER = '-abcdefghijklmnopqrstuvwxyz';
/**
* Constructor.
*
* @param array $headers An array of HTTP headers
* @var array<string, list<string|null>>
*/
public function __construct(array $headers = array())
protected $headers = [];
protected $cacheControl = [];
public function __construct(array $headers = [])
{
foreach ($headers as $key => $values) {
$this->set($key, $values);
@@ -35,10 +38,8 @@ class HeaderBag implements \IteratorAggregate, \Countable
/**
* Returns the headers as a string.
*
* @return string The headers
*/
public function __toString()
public function __toString(): string
{
if (!$headers = $this->all()) {
return '';
@@ -48,7 +49,7 @@ class HeaderBag implements \IteratorAggregate, \Countable
$max = max(array_map('strlen', array_keys($headers))) + 1;
$content = '';
foreach ($headers as $name => $values) {
$name = implode('-', array_map('ucfirst', explode('-', $name)));
$name = ucwords($name, '-');
foreach ($values as $value) {
$content .= sprintf("%-{$max}s %s\r\n", $name.':', $value);
}
@@ -60,38 +61,40 @@ class HeaderBag implements \IteratorAggregate, \Countable
/**
* Returns the headers.
*
* @return array An array of headers
* @param string|null $key The name of the headers to return or null to get them all
*
* @return array<string, array<int, string|null>>|array<int, string|null>
*/
public function all()
public function all(string $key = null): array
{
if (null !== $key) {
return $this->headers[strtr($key, self::UPPER, self::LOWER)] ?? [];
}
return $this->headers;
}
/**
* Returns the parameter keys.
*
* @return array An array of parameter keys
* @return string[]
*/
public function keys()
public function keys(): array
{
return array_keys($this->all());
}
/**
* Replaces the current HTTP headers by a new set.
*
* @param array $headers An array of HTTP headers
*/
public function replace(array $headers = array())
public function replace(array $headers = [])
{
$this->headers = array();
$this->headers = [];
$this->add($headers);
}
/**
* Adds new headers the current HTTP headers set.
*
* @param array $headers An array of HTTP headers
*/
public function add(array $headers)
{
@@ -101,117 +104,97 @@ class HeaderBag implements \IteratorAggregate, \Countable
}
/**
* Returns a header value by name.
*
* @param string $key The header name
* @param mixed $default The default value
* @param bool $first Whether to return the first value or all header values
*
* @return string|array The first header value if $first is true, an array of values otherwise
* Returns the first header by name or the default one.
*/
public function get($key, $default = null, $first = true)
public function get(string $key, string $default = null): ?string
{
$key = str_replace('_', '-', strtolower($key));
$headers = $this->all();
$headers = $this->all($key);
if (!array_key_exists($key, $headers)) {
if (null === $default) {
return $first ? null : array();
}
return $first ? $default : array($default);
if (!$headers) {
return $default;
}
if ($first) {
return count($headers[$key]) ? $headers[$key][0] : $default;
if (null === $headers[0]) {
return null;
}
return $headers[$key];
return (string) $headers[0];
}
/**
* Sets a header by name.
*
* @param string $key The key
* @param string|array $values The value or an array of values
* @param bool $replace Whether to replace the actual value or not (true by default)
* @param string|string[]|null $values The value or an array of values
* @param bool $replace Whether to replace the actual value or not (true by default)
*/
public function set($key, $values, $replace = true)
public function set(string $key, string|array|null $values, bool $replace = true)
{
$key = str_replace('_', '-', strtolower($key));
$key = strtr($key, self::UPPER, self::LOWER);
$values = array_values((array) $values);
if (\is_array($values)) {
$values = array_values($values);
if (true === $replace || !isset($this->headers[$key])) {
$this->headers[$key] = $values;
if (true === $replace || !isset($this->headers[$key])) {
$this->headers[$key] = $values;
} else {
$this->headers[$key] = array_merge($this->headers[$key], $values);
}
} else {
$this->headers[$key] = array_merge($this->headers[$key], $values);
if (true === $replace || !isset($this->headers[$key])) {
$this->headers[$key] = [$values];
} else {
$this->headers[$key][] = $values;
}
}
if ('cache-control' === $key) {
$this->cacheControl = $this->parseCacheControl($values[0]);
$this->cacheControl = $this->parseCacheControl(implode(', ', $this->headers[$key]));
}
}
/**
* Returns true if the HTTP header is defined.
*
* @param string $key The HTTP header
*
* @return bool true if the parameter exists, false otherwise
*/
public function has($key)
public function has(string $key): bool
{
return array_key_exists(str_replace('_', '-', strtolower($key)), $this->all());
return \array_key_exists(strtr($key, self::UPPER, self::LOWER), $this->all());
}
/**
* Returns true if the given HTTP header contains the given value.
*
* @param string $key The HTTP header name
* @param string $value The HTTP value
*
* @return bool true if the value is contained in the header, false otherwise
*/
public function contains($key, $value)
public function contains(string $key, string $value): bool
{
return in_array($value, $this->get($key, null, false));
return \in_array($value, $this->all($key));
}
/**
* Removes a header.
*
* @param string $key The HTTP header name
*/
public function remove($key)
public function remove(string $key)
{
$key = str_replace('_', '-', strtolower($key));
$key = strtr($key, self::UPPER, self::LOWER);
unset($this->headers[$key]);
if ('cache-control' === $key) {
$this->cacheControl = array();
$this->cacheControl = [];
}
}
/**
* Returns the HTTP header value converted to a date.
*
* @param string $key The parameter key
* @param \DateTime $default The default value
*
* @return null|\DateTime The parsed DateTime or the default value if the header does not exist
*
* @throws \RuntimeException When the HTTP header is not parseable
*/
public function getDate($key, \DateTime $default = null)
public function getDate(string $key, \DateTime $default = null): ?\DateTimeInterface
{
if (null === $value = $this->get($key)) {
return $default;
}
if (false === $date = \DateTime::createFromFormat(DATE_RFC2822, $value)) {
throw new \RuntimeException(sprintf('The %s HTTP header is not parseable (%s).', $key, $value));
if (false === $date = \DateTime::createFromFormat(\DATE_RFC2822, $value)) {
throw new \RuntimeException(sprintf('The "%s" HTTP header is not parseable (%s).', $key, $value));
}
return $date;
@@ -219,11 +202,8 @@ class HeaderBag implements \IteratorAggregate, \Countable
/**
* Adds a custom Cache-Control directive.
*
* @param string $key The Cache-Control directive name
* @param mixed $value The Cache-Control directive value
*/
public function addCacheControlDirective($key, $value = true)
public function addCacheControlDirective(string $key, bool|string $value = true)
{
$this->cacheControl[$key] = $value;
@@ -232,34 +212,24 @@ class HeaderBag implements \IteratorAggregate, \Countable
/**
* Returns true if the Cache-Control directive is defined.
*
* @param string $key The Cache-Control directive
*
* @return bool true if the directive exists, false otherwise
*/
public function hasCacheControlDirective($key)
public function hasCacheControlDirective(string $key): bool
{
return array_key_exists($key, $this->cacheControl);
return \array_key_exists($key, $this->cacheControl);
}
/**
* Returns a Cache-Control directive value by name.
*
* @param string $key The directive name
*
* @return mixed|null The directive value if defined, null otherwise
*/
public function getCacheControlDirective($key)
public function getCacheControlDirective(string $key): bool|string|null
{
return array_key_exists($key, $this->cacheControl) ? $this->cacheControl[$key] : null;
return $this->cacheControl[$key] ?? null;
}
/**
* Removes a Cache-Control directive.
*
* @param string $key The Cache-Control directive
*/
public function removeCacheControlDirective($key)
public function removeCacheControlDirective(string $key)
{
unset($this->cacheControl[$key]);
@@ -269,57 +239,35 @@ class HeaderBag implements \IteratorAggregate, \Countable
/**
* Returns an iterator for headers.
*
* @return \ArrayIterator An \ArrayIterator instance
* @return \ArrayIterator<string, list<string|null>>
*/
public function getIterator()
public function getIterator(): \ArrayIterator
{
return new \ArrayIterator($this->headers);
}
/**
* Returns the number of headers.
*
* @return int The number of headers
*/
public function count()
public function count(): int
{
return count($this->headers);
return \count($this->headers);
}
protected function getCacheControlHeader()
{
$parts = array();
ksort($this->cacheControl);
foreach ($this->cacheControl as $key => $value) {
if (true === $value) {
$parts[] = $key;
} else {
if (preg_match('#[^a-zA-Z0-9._-]#', $value)) {
$value = '"'.$value.'"';
}
$parts[] = "$key=$value";
}
}
return implode(', ', $parts);
return HeaderUtils::toString($this->cacheControl, ',');
}
/**
* Parses a Cache-Control HTTP header.
*
* @param string $header The value of the Cache-Control HTTP header
*
* @return array An array representing the attribute values
*/
protected function parseCacheControl($header)
protected function parseCacheControl(string $header): array
{
$cacheControl = array();
preg_match_all('#([a-zA-Z][a-zA-Z_-]*)\s*(?:=(?:"([^"]*)"|([^ \t",;]*)))?#', $header, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$cacheControl[strtolower($match[1])] = isset($match[3]) ? $match[3] : (isset($match[2]) ? $match[2] : true);
}
$parts = HeaderUtils::split($header, ',=');
return $cacheControl;
return HeaderUtils::combine($parts);
}
}

View File

@@ -0,0 +1,293 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation;
/**
* HTTP header utility functions.
*
* @author Christian Schmidt <github@chsc.dk>
*/
class HeaderUtils
{
public const DISPOSITION_ATTACHMENT = 'attachment';
public const DISPOSITION_INLINE = 'inline';
/**
* This class should not be instantiated.
*/
private function __construct()
{
}
/**
* Splits an HTTP header by one or more separators.
*
* Example:
*
* HeaderUtils::split("da, en-gb;q=0.8", ",;")
* // => ['da'], ['en-gb', 'q=0.8']]
*
* @param string $separators List of characters to split on, ordered by
* precedence, e.g. ",", ";=", or ",;="
*
* @return array Nested array with as many levels as there are characters in
* $separators
*/
public static function split(string $header, string $separators): array
{
$quotedSeparators = preg_quote($separators, '/');
preg_match_all('
/
(?!\s)
(?:
# quoted-string
"(?:[^"\\\\]|\\\\.)*(?:"|\\\\|$)
|
# token
[^"'.$quotedSeparators.']+
)+
(?<!\s)
|
# separator
\s*
(?<separator>['.$quotedSeparators.'])
\s*
/x', trim($header), $matches, \PREG_SET_ORDER);
return self::groupParts($matches, $separators);
}
/**
* Combines an array of arrays into one associative array.
*
* Each of the nested arrays should have one or two elements. The first
* value will be used as the keys in the associative array, and the second
* will be used as the values, or true if the nested array only contains one
* element. Array keys are lowercased.
*
* Example:
*
* HeaderUtils::combine([["foo", "abc"], ["bar"]])
* // => ["foo" => "abc", "bar" => true]
*/
public static function combine(array $parts): array
{
$assoc = [];
foreach ($parts as $part) {
$name = strtolower($part[0]);
$value = $part[1] ?? true;
$assoc[$name] = $value;
}
return $assoc;
}
/**
* Joins an associative array into a string for use in an HTTP header.
*
* The key and value of each entry are joined with "=", and all entries
* are joined with the specified separator and an additional space (for
* readability). Values are quoted if necessary.
*
* Example:
*
* HeaderUtils::toString(["foo" => "abc", "bar" => true, "baz" => "a b c"], ",")
* // => 'foo=abc, bar, baz="a b c"'
*/
public static function toString(array $assoc, string $separator): string
{
$parts = [];
foreach ($assoc as $name => $value) {
if (true === $value) {
$parts[] = $name;
} else {
$parts[] = $name.'='.self::quote($value);
}
}
return implode($separator.' ', $parts);
}
/**
* Encodes a string as a quoted string, if necessary.
*
* If a string contains characters not allowed by the "token" construct in
* the HTTP specification, it is backslash-escaped and enclosed in quotes
* to match the "quoted-string" construct.
*/
public static function quote(string $s): string
{
if (preg_match('/^[a-z0-9!#$%&\'*.^_`|~-]+$/i', $s)) {
return $s;
}
return '"'.addcslashes($s, '"\\"').'"';
}
/**
* Decodes a quoted string.
*
* If passed an unquoted string that matches the "token" construct (as
* defined in the HTTP specification), it is passed through verbatimly.
*/
public static function unquote(string $s): string
{
return preg_replace('/\\\\(.)|"/', '$1', $s);
}
/**
* Generates an HTTP Content-Disposition field-value.
*
* @param string $disposition One of "inline" or "attachment"
* @param string $filename A unicode string
* @param string $filenameFallback A string containing only ASCII characters that
* is semantically equivalent to $filename. If the filename is already ASCII,
* it can be omitted, or just copied from $filename
*
* @throws \InvalidArgumentException
*
* @see RFC 6266
*/
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));
}
if ('' === $filenameFallback) {
$filenameFallback = $filename;
}
// filenameFallback is not ASCII.
if (!preg_match('/^[\x20-\x7e]*$/', $filenameFallback)) {
throw new \InvalidArgumentException('The filename fallback must only contain ASCII characters.');
}
// percent characters aren't safe in fallback.
if (str_contains($filenameFallback, '%')) {
throw new \InvalidArgumentException('The filename fallback cannot contain the "%" character.');
}
// path separators aren't allowed in either.
if (str_contains($filename, '/') || str_contains($filename, '\\') || str_contains($filenameFallback, '/') || str_contains($filenameFallback, '\\')) {
throw new \InvalidArgumentException('The filename and the fallback cannot contain the "/" and "\\" characters.');
}
$params = ['filename' => $filenameFallback];
if ($filename !== $filenameFallback) {
$params['filename*'] = "utf-8''".rawurlencode($filename);
}
return $disposition.'; '.self::toString($params, ';');
}
/**
* Like parse_str(), but preserves dots in variable names.
*/
public static function parseQuery(string $query, bool $ignoreBrackets = false, string $separator = '&'): array
{
$q = [];
foreach (explode($separator, $query) as $v) {
if (false !== $i = strpos($v, "\0")) {
$v = substr($v, 0, $i);
}
if (false === $i = strpos($v, '=')) {
$k = urldecode($v);
$v = '';
} else {
$k = urldecode(substr($v, 0, $i));
$v = substr($v, $i);
}
if (false !== $i = strpos($k, "\0")) {
$k = substr($k, 0, $i);
}
$k = ltrim($k, ' ');
if ($ignoreBrackets) {
$q[$k][] = urldecode(substr($v, 1));
continue;
}
if (false === $i = strpos($k, '[')) {
$q[] = bin2hex($k).$v;
} else {
$q[] = bin2hex(substr($k, 0, $i)).rawurlencode(substr($k, $i)).$v;
}
}
if ($ignoreBrackets) {
return $q;
}
parse_str(implode('&', $q), $q);
$query = [];
foreach ($q as $k => $v) {
if (false !== $i = strpos($k, '_')) {
$query[substr_replace($k, hex2bin(substr($k, 0, $i)).'[', 0, 1 + $i)] = $v;
} else {
$query[hex2bin($k)] = $v;
}
}
return $query;
}
private static function groupParts(array $matches, string $separators, bool $first = true): array
{
$separator = $separators[0];
$partSeparators = substr($separators, 1);
$i = 0;
$partMatches = [];
$previousMatchWasSeparator = false;
foreach ($matches as $match) {
if (!$first && $previousMatchWasSeparator && isset($match['separator']) && $match['separator'] === $separator) {
$previousMatchWasSeparator = true;
$partMatches[$i][] = $match;
} elseif (isset($match['separator']) && $match['separator'] === $separator) {
$previousMatchWasSeparator = true;
++$i;
} else {
$previousMatchWasSeparator = false;
$partMatches[$i][] = $match;
}
}
$parts = [];
if ($partSeparators) {
foreach ($partMatches as $matches) {
$parts[] = self::groupParts($matches, $partSeparators, false);
}
} else {
foreach ($partMatches as $matches) {
$parts[] = self::unquote($matches[0][0]);
}
if (!$first && 2 < \count($parts)) {
$parts = [
$parts[0],
implode($separator, \array_slice($parts, 1)),
];
}
}
return $parts;
}
}

View File

@@ -0,0 +1,98 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
/**
* InputBag is a container for user input values such as $_GET, $_POST, $_REQUEST, and $_COOKIE.
*
* @author Saif Eddin Gmati <azjezz@protonmail.com>
*/
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
*/
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)));
}
$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));
}
return $this === $value ? $default : $value;
}
/**
* Replaces the current input values by a new set.
*/
public function replace(array $inputs = [])
{
$this->parameters = [];
$this->add($inputs);
}
/**
* Adds input values.
*/
public function add(array $inputs = [])
{
foreach ($inputs as $input => $value) {
$this->set($input, $value);
}
}
/**
* Sets an input by name.
*
* @param string|int|float|bool|array|null $value
*/
public function set(string $key, mixed $value)
{
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)));
}
$this->parameters[$key] = $value;
}
/**
* {@inheritdoc}
*/
public function filter(string $key, mixed $default = null, int $filter = \FILTER_DEFAULT, mixed $options = []): mixed
{
$value = $this->has($key) ? $this->all()[$key] : $default;
// Always turn $options into an array - this allows filter_var option shortcuts.
if (!\is_array($options) && $options) {
$options = ['flags' => $options];
}
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));
}
if ((\FILTER_CALLBACK & $filter) && !(($options['options'] ?? null) instanceof \Closure)) {
throw new \InvalidArgumentException(sprintf('A Closure must be passed to "%s()" when FILTER_CALLBACK is used, "%s" given.', __METHOD__, get_debug_type($options['options'] ?? null)));
}
return filter_var($value, $filter, $options);
}
}

View File

@@ -18,7 +18,7 @@ namespace Symfony\Component\HttpFoundation;
*/
class IpUtils
{
private static $checkedIps = array();
private static array $checkedIps = [];
/**
* This class should not be instantiated.
@@ -30,15 +30,12 @@ class IpUtils
/**
* Checks if an IPv4 or IPv6 address is contained in the list of given IPs or subnets.
*
* @param string $requestIp IP to check
* @param string|array $ips List of IPs or subnets (can be a string if only a single one)
*
* @return bool Whether the IP is valid
* @param string|array $ips List of IPs or subnets (can be a string if only a single one)
*/
public static function checkIp($requestIp, $ips)
public static function checkIp(string $requestIp, string|array $ips): bool
{
if (!is_array($ips)) {
$ips = array($ips);
if (!\is_array($ips)) {
$ips = [$ips];
}
$method = substr_count($requestIp, ':') > 1 ? 'checkIp6' : 'checkIp4';
@@ -56,27 +53,26 @@ class IpUtils
* Compares two IPv4 addresses.
* In case a subnet is given, it checks if it contains the request IP.
*
* @param string $requestIp IPv4 address to check
* @param string $ip IPv4 address or subnet in CIDR notation
* @param string $ip IPv4 address or subnet in CIDR notation
*
* @return bool Whether the request IP matches the IP, or whether the request IP is within the CIDR subnet
*/
public static function checkIp4($requestIp, $ip)
public static function checkIp4(string $requestIp, string $ip): bool
{
$cacheKey = $requestIp.'-'.$ip;
if (isset(self::$checkedIps[$cacheKey])) {
return self::$checkedIps[$cacheKey];
}
if (!filter_var($requestIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
if (!filter_var($requestIp, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)) {
return self::$checkedIps[$cacheKey] = false;
}
if (false !== strpos($ip, '/')) {
list($address, $netmask) = explode('/', $ip, 2);
if (str_contains($ip, '/')) {
[$address, $netmask] = explode('/', $ip, 2);
if ($netmask === '0') {
return self::$checkedIps[$cacheKey] = filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
if ('0' === $netmask) {
return self::$checkedIps[$cacheKey] = filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4);
}
if ($netmask < 0 || $netmask > 32) {
@@ -87,6 +83,10 @@ class IpUtils
$netmask = 32;
}
if (false === ip2long($address)) {
return self::$checkedIps[$cacheKey] = false;
}
return self::$checkedIps[$cacheKey] = 0 === substr_compare(sprintf('%032b', ip2long($requestIp)), sprintf('%032b', ip2long($address)), 0, $netmask);
}
@@ -98,31 +98,45 @@ class IpUtils
*
* @see https://github.com/dsp/v6tools
*
* @param string $requestIp IPv6 address to check
* @param string $ip IPv6 address or subnet in CIDR notation
*
* @return bool Whether the IP is valid
* @param string $ip IPv6 address or subnet in CIDR notation
*
* @throws \RuntimeException When IPV6 support is not enabled
*/
public static function checkIp6($requestIp, $ip)
public static function checkIp6(string $requestIp, string $ip): bool
{
$cacheKey = $requestIp.'-'.$ip;
if (isset(self::$checkedIps[$cacheKey])) {
return self::$checkedIps[$cacheKey];
}
if (!((extension_loaded('sockets') && defined('AF_INET6')) || @inet_pton('::1'))) {
if (!((\extension_loaded('sockets') && \defined('AF_INET6')) || @inet_pton('::1'))) {
throw new \RuntimeException('Unable to check Ipv6. Check that PHP was not compiled with option "disable-ipv6".');
}
if (false !== strpos($ip, '/')) {
list($address, $netmask) = explode('/', $ip, 2);
// Check to see if we were given a IP4 $requestIp or $ip by mistake
if (!filter_var($requestIp, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) {
return self::$checkedIps[$cacheKey] = false;
}
if (str_contains($ip, '/')) {
[$address, $netmask] = explode('/', $ip, 2);
if (!filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) {
return self::$checkedIps[$cacheKey] = false;
}
if ('0' === $netmask) {
return (bool) unpack('n*', @inet_pton($address));
}
if ($netmask < 1 || $netmask > 128) {
return self::$checkedIps[$cacheKey] = false;
}
} else {
if (!filter_var($ip, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) {
return self::$checkedIps[$cacheKey] = false;
}
$address = $ip;
$netmask = 128;
}
@@ -137,7 +151,7 @@ class IpUtils
for ($i = 1, $ceil = ceil($netmask / 16); $i <= $ceil; ++$i) {
$left = $netmask - 16 * ($i - 1);
$left = ($left <= 16) ? $left : 16;
$mask = ~(0xffff >> $left) & 0xffff;
$mask = ~(0xFFFF >> $left) & 0xFFFF;
if (($bytesAddr[$i] & $mask) != ($bytesTest[$i] & $mask)) {
return self::$checkedIps[$cacheKey] = false;
}
@@ -145,4 +159,36 @@ class IpUtils
return self::$checkedIps[$cacheKey] = true;
}
/**
* Anonymizes an IP/IPv6.
*
* Removes the last byte for v4 and the last 8 bytes for v6 IPs
*/
public static function anonymize(string $ip): string
{
$wrappedIPv6 = false;
if ('[' === substr($ip, 0, 1) && ']' === substr($ip, -1, 1)) {
$wrappedIPv6 = true;
$ip = substr($ip, 1, -1);
}
$packedAddress = inet_pton($ip);
if (4 === \strlen($packedAddress)) {
$mask = '255.255.255.0';
} elseif ($ip === inet_ntop($packedAddress & inet_pton('::ffff:ffff:ffff'))) {
$mask = '::ffff:ffff:ff00';
} elseif ($ip === inet_ntop($packedAddress & inet_pton('::ffff:ffff'))) {
$mask = '::ffff:ff00';
} else {
$mask = 'ffff:ffff:ffff:ffff:0000:0000:0000:0000';
}
$ip = inet_ntop($packedAddress & inet_pton($mask));
if ($wrappedIPv6) {
$ip = '['.$ip.']';
}
return $ip;
}
}

View File

@@ -18,7 +18,7 @@ namespace Symfony\Component\HttpFoundation;
* object. It is however recommended that you do return an object as it
* protects yourself against XSSI and JSON-JavaScript Hijacking.
*
* @see https://www.owasp.org/index.php/OWASP_AJAX_Security_Guidelines#Always_return_JSON_with_an_Object_on_the_outside
* @see https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/AJAX_Security_Cheat_Sheet.md#always-return-json-with-an-object-on-the-outside
*
* @author Igor Wiedler <igor@wiedler.ch>
*/
@@ -29,20 +29,21 @@ class JsonResponse extends Response
// Encode <, >, ', &, and " characters in the JSON, making it also safe to be embedded into HTML.
// 15 === JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT
const DEFAULT_ENCODING_OPTIONS = 15;
public const DEFAULT_ENCODING_OPTIONS = 15;
protected $encodingOptions = self::DEFAULT_ENCODING_OPTIONS;
/**
* @param mixed $data The response data
* @param int $status The response status code
* @param array $headers An array of response headers
* @param bool $json If the data is already a JSON string
* @param bool $json If the data is already a JSON string
*/
public function __construct($data = null, $status = 200, $headers = array(), $json = false)
public function __construct(mixed $data = null, int $status = 200, array $headers = [], bool $json = false)
{
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)));
}
if (null === $data) {
$data = new \ArrayObject();
}
@@ -55,24 +56,14 @@ class JsonResponse extends Response
*
* Example:
*
* return JsonResponse::create($data, 200)
* return JsonResponse::fromJsonString('{"key": "value"}')
* ->setSharedMaxAge(300);
*
* @param mixed $data The json response data
* @param int $status The response status code
* @param array $headers An array of response headers
*
* @return static
* @param string $data The JSON response string
* @param int $status The response status code
* @param array $headers An array of response headers
*/
public static function create($data = null, $status = 200, $headers = array())
{
return new static($data, $status, $headers);
}
/**
* Make easier the creation of JsonResponse from raw json.
*/
public static function fromJsonString($data = null, $status = 200, $headers = array())
public static function fromJsonString(string $data, int $status = 200, array $headers = []): static
{
return new static($data, $status, $headers, true);
}
@@ -86,22 +77,22 @@ class JsonResponse extends Response
*
* @throws \InvalidArgumentException When the callback name is not valid
*/
public function setCallback($callback = null)
public function setCallback(string $callback = null): static
{
if (null !== $callback) {
// partially taken from http://www.geekality.net/2011/08/03/valid-javascript-identifier/
// partially taken from https://geekality.net/2011/08/03/valid-javascript-identifier/
// partially taken from https://github.com/willdurand/JsonpCallbackValidator
// JsonpCallbackValidator is released under the MIT License. See https://github.com/willdurand/JsonpCallbackValidator/blob/v1.1.0/LICENSE for details.
// (c) William Durand <william.durand1@gmail.com>
$pattern = '/^[$_\p{L}][$_\p{L}\p{Mn}\p{Mc}\p{Nd}\p{Pc}\x{200C}\x{200D}]*(?:\[(?:"(?:\\\.|[^"\\\])*"|\'(?:\\\.|[^\'\\\])*\'|\d+)\])*?$/u';
$reserved = array(
$reserved = [
'break', 'do', 'instanceof', 'typeof', 'case', 'else', 'new', 'var', 'catch', 'finally', 'return', 'void', 'continue', 'for', 'switch', 'while',
'debugger', 'function', 'this', 'with', 'default', 'if', 'throw', 'delete', 'in', 'try', 'class', 'enum', 'extends', 'super', 'const', 'export',
'import', 'implements', 'let', 'private', 'public', 'yield', 'interface', 'package', 'protected', 'static', 'null', 'true', 'false',
);
];
$parts = explode('.', $callback);
foreach ($parts as $part) {
if (!preg_match($pattern, $part) || in_array($part, $reserved, true)) {
if (!preg_match($pattern, $part) || \in_array($part, $reserved, true)) {
throw new \InvalidArgumentException('The callback name is not valid.');
}
}
@@ -115,13 +106,9 @@ class JsonResponse extends Response
/**
* Sets a raw string containing a JSON document to be sent.
*
* @param string $json
*
* @return $this
*
* @throws \InvalidArgumentException
*/
public function setJson($json)
public function setJson(string $json): static
{
$this->data = $json;
@@ -131,34 +118,26 @@ class JsonResponse extends Response
/**
* Sets the data to be sent as JSON.
*
* @param mixed $data
*
* @return $this
*
* @throws \InvalidArgumentException
*/
public function setData($data = array())
public function setData(mixed $data = []): static
{
if (defined('HHVM_VERSION')) {
// HHVM does not trigger any warnings and let exceptions
// thrown from a JsonSerializable object pass through.
// If only PHP did the same...
try {
$data = json_encode($data, $this->encodingOptions);
} else {
try {
// PHP 5.4 and up wrap exceptions thrown by JsonSerializable
// objects in a new exception that needs to be removed.
// Fortunately, PHP 5.5 and up do not trigger any warning anymore.
$data = json_encode($data, $this->encodingOptions);
} catch (\Exception $e) {
if ('Exception' === get_class($e) && 0 === strpos($e->getMessage(), 'Failed calling ')) {
throw $e->getPrevious() ?: $e;
}
throw $e;
} catch (\Exception $e) {
if ('Exception' === \get_class($e) && str_starts_with($e->getMessage(), 'Failed calling ')) {
throw $e->getPrevious() ?: $e;
}
throw $e;
}
if (JSON_ERROR_NONE !== json_last_error()) {
if (\JSON_THROW_ON_ERROR & $this->encodingOptions) {
return $this->setJson($data);
}
if (\JSON_ERROR_NONE !== json_last_error()) {
throw new \InvalidArgumentException(json_last_error_msg());
}
@@ -167,10 +146,8 @@ class JsonResponse extends Response
/**
* Returns options used while encoding data to JSON.
*
* @return int
*/
public function getEncodingOptions()
public function getEncodingOptions(): int
{
return $this->encodingOptions;
}
@@ -178,13 +155,11 @@ class JsonResponse extends Response
/**
* Sets options used while encoding data to JSON.
*
* @param int $encodingOptions
*
* @return $this
*/
public function setEncodingOptions($encodingOptions)
public function setEncodingOptions(int $encodingOptions): static
{
$this->encodingOptions = (int) $encodingOptions;
$this->encodingOptions = $encodingOptions;
return $this->setData(json_decode($this->data));
}
@@ -194,7 +169,7 @@ class JsonResponse extends Response
*
* @return $this
*/
protected function update()
protected function update(): static
{
if (null !== $this->callback) {
// Not using application/javascript for compatibility reasons with older browsers.

View File

@@ -1,4 +1,4 @@
Copyright (c) 2004-2017 Fabien Potencier
Copyright (c) 2004-2022 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -11,26 +11,23 @@
namespace Symfony\Component\HttpFoundation;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
/**
* ParameterBag is a container for key/value pairs.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @implements \IteratorAggregate<string, mixed>
*/
class ParameterBag implements \IteratorAggregate, \Countable
{
/**
* Parameter storage.
*
* @var array
*/
protected $parameters;
/**
* Constructor.
*
* @param array $parameters An array of parameters
*/
public function __construct(array $parameters = array())
public function __construct(array $parameters = [])
{
$this->parameters = $parameters;
}
@@ -38,179 +35,135 @@ class ParameterBag implements \IteratorAggregate, \Countable
/**
* Returns the parameters.
*
* @return array An array of parameters
* @param string|null $key The name of the parameter to return or null to get them all
*/
public function all()
public function all(string $key = null): array
{
return $this->parameters;
if (null === $key) {
return $this->parameters;
}
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)));
}
return $value;
}
/**
* Returns the parameter keys.
*
* @return array An array of parameter keys
*/
public function keys()
public function keys(): array
{
return array_keys($this->parameters);
}
/**
* Replaces the current parameters by a new set.
*
* @param array $parameters An array of parameters
*/
public function replace(array $parameters = array())
public function replace(array $parameters = [])
{
$this->parameters = $parameters;
}
/**
* Adds parameters.
*
* @param array $parameters An array of parameters
*/
public function add(array $parameters = array())
public function add(array $parameters = [])
{
$this->parameters = array_replace($this->parameters, $parameters);
}
/**
* Returns a parameter by name.
*
* @param string $key The key
* @param mixed $default The default value if the parameter key does not exist
*
* @return mixed
*/
public function get($key, $default = null)
public function get(string $key, mixed $default = null): mixed
{
return array_key_exists($key, $this->parameters) ? $this->parameters[$key] : $default;
return \array_key_exists($key, $this->parameters) ? $this->parameters[$key] : $default;
}
/**
* Sets a parameter by name.
*
* @param string $key The key
* @param mixed $value The value
*/
public function set($key, $value)
public function set(string $key, mixed $value)
{
$this->parameters[$key] = $value;
}
/**
* Returns true if the parameter is defined.
*
* @param string $key The key
*
* @return bool true if the parameter exists, false otherwise
*/
public function has($key)
public function has(string $key): bool
{
return array_key_exists($key, $this->parameters);
return \array_key_exists($key, $this->parameters);
}
/**
* Removes a parameter.
*
* @param string $key The key
*/
public function remove($key)
public function remove(string $key)
{
unset($this->parameters[$key]);
}
/**
* Returns the alphabetic characters of the parameter value.
*
* @param string $key The parameter key
* @param string $default The default value if the parameter key does not exist
*
* @return string The filtered value
*/
public function getAlpha($key, $default = '')
public function getAlpha(string $key, string $default = ''): string
{
return preg_replace('/[^[:alpha:]]/', '', $this->get($key, $default));
}
/**
* Returns the alphabetic characters and digits of the parameter value.
*
* @param string $key The parameter key
* @param string $default The default value if the parameter key does not exist
*
* @return string The filtered value
*/
public function getAlnum($key, $default = '')
public function getAlnum(string $key, string $default = ''): string
{
return preg_replace('/[^[:alnum:]]/', '', $this->get($key, $default));
}
/**
* Returns the digits of the parameter value.
*
* @param string $key The parameter key
* @param string $default The default value if the parameter key does not exist
*
* @return string The filtered value
*/
public function getDigits($key, $default = '')
public function getDigits(string $key, string $default = ''): string
{
// we need to remove - and + because they're allowed in the filter
return str_replace(array('-', '+'), '', $this->filter($key, $default, FILTER_SANITIZE_NUMBER_INT));
return str_replace(['-', '+'], '', $this->filter($key, $default, \FILTER_SANITIZE_NUMBER_INT));
}
/**
* Returns the parameter value converted to integer.
*
* @param string $key The parameter key
* @param int $default The default value if the parameter key does not exist
*
* @return int The filtered value
*/
public function getInt($key, $default = 0)
public function getInt(string $key, int $default = 0): int
{
return (int) $this->get($key, $default);
}
/**
* Returns the parameter value converted to boolean.
*
* @param string $key The parameter key
* @param mixed $default The default value if the parameter key does not exist
*
* @return bool The filtered value
*/
public function getBoolean($key, $default = false)
public function getBoolean(string $key, bool $default = false): bool
{
return $this->filter($key, $default, FILTER_VALIDATE_BOOLEAN);
return $this->filter($key, $default, \FILTER_VALIDATE_BOOLEAN);
}
/**
* Filter key.
*
* @param string $key Key
* @param mixed $default Default = null
* @param int $filter FILTER_* constant
* @param mixed $options Filter options
* @param int $filter FILTER_* constant
*
* @see http://php.net/manual/en/function.filter-var.php
*
* @return mixed
* @see https://php.net/filter-var
*/
public function filter($key, $default = null, $filter = FILTER_DEFAULT, $options = array())
public function filter(string $key, mixed $default = null, int $filter = \FILTER_DEFAULT, mixed $options = []): mixed
{
$value = $this->get($key, $default);
// Always turn $options into an array - this allows filter_var option shortcuts.
if (!is_array($options) && $options) {
$options = array('flags' => $options);
if (!\is_array($options) && $options) {
$options = ['flags' => $options];
}
// Add a convenience check for arrays.
if (is_array($value) && !isset($options['flags'])) {
$options['flags'] = FILTER_REQUIRE_ARRAY;
if (\is_array($value) && !isset($options['flags'])) {
$options['flags'] = \FILTER_REQUIRE_ARRAY;
}
if ((\FILTER_CALLBACK & $filter) && !(($options['options'] ?? null) instanceof \Closure)) {
throw new \InvalidArgumentException(sprintf('A Closure must be passed to "%s()" when FILTER_CALLBACK is used, "%s" given.', __METHOD__, get_debug_type($options['options'] ?? null)));
}
return filter_var($value, $filter, $options);
@@ -219,20 +172,18 @@ class ParameterBag implements \IteratorAggregate, \Countable
/**
* Returns an iterator for parameters.
*
* @return \ArrayIterator An \ArrayIterator instance
* @return \ArrayIterator<string, mixed>
*/
public function getIterator()
public function getIterator(): \ArrayIterator
{
return new \ArrayIterator($this->parameters);
}
/**
* Returns the number of parameters.
*
* @return int The number of parameters
*/
public function count()
public function count(): int
{
return count($this->parameters);
return \count($this->parameters);
}
}

View File

@@ -4,11 +4,25 @@ HttpFoundation Component
The HttpFoundation component defines an object-oriented layer for the HTTP
specification.
Sponsor
-------
The HttpFoundation component for Symfony 5.4/6.0 is [backed][1] by [Laravel][2].
Laravel is a PHP web development framework that is passionate about maximum developer
happiness. Laravel is built using a variety of bespoke and Symfony based components.
Help Symfony by [sponsoring][3] its development!
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/http_foundation/index.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)
* [Documentation](https://symfony.com/doc/current/components/http_foundation.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)
[1]: https://symfony.com/backers
[2]: https://laravel.com/
[3]: https://symfony.com/sponsor

View File

@@ -0,0 +1,71 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\RateLimiter;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\RateLimiter\LimiterInterface;
use Symfony\Component\RateLimiter\Policy\NoLimiter;
use Symfony\Component\RateLimiter\RateLimit;
/**
* An implementation of RequestRateLimiterInterface that
* fits most use-cases.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
abstract class AbstractRequestRateLimiter implements RequestRateLimiterInterface
{
public function consume(Request $request): RateLimit
{
$limiters = $this->getLimiters($request);
if (0 === \count($limiters)) {
$limiters = [new NoLimiter()];
}
$minimalRateLimit = null;
foreach ($limiters as $limiter) {
$rateLimit = $limiter->consume(1);
$minimalRateLimit = $minimalRateLimit ? self::getMinimalRateLimit($minimalRateLimit, $rateLimit) : $rateLimit;
}
return $minimalRateLimit;
}
public function reset(Request $request): void
{
foreach ($this->getLimiters($request) as $limiter) {
$limiter->reset();
}
}
/**
* @return LimiterInterface[] a set of limiters using keys extracted from the request
*/
abstract protected function getLimiters(Request $request): array;
private static function getMinimalRateLimit(RateLimit $first, RateLimit $second): RateLimit
{
if ($first->isAccepted() !== $second->isAccepted()) {
return $first->isAccepted() ? $second : $first;
}
$firstRemainingTokens = $first->getRemainingTokens();
$secondRemainingTokens = $second->getRemainingTokens();
if ($firstRemainingTokens === $secondRemainingTokens) {
return $first->getRetryAfter() < $second->getRetryAfter() ? $second : $first;
}
return $firstRemainingTokens > $secondRemainingTokens ? $second : $first;
}
}

View File

@@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\RateLimiter;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\RateLimiter\RateLimit;
/**
* A special type of limiter that deals with requests.
*
* This allows to limit on different types of information
* from the requests.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
interface RequestRateLimiterInterface
{
public function consume(Request $request): RateLimit;
public function reset(Request $request): void;
}

View File

@@ -30,9 +30,9 @@ class RedirectResponse extends Response
*
* @throws \InvalidArgumentException
*
* @see http://tools.ietf.org/html/rfc2616#section-10.3
* @see https://tools.ietf.org/html/rfc2616#section-10.3
*/
public function __construct($url, $status = 302, $headers = array())
public function __construct(string $url, int $status = 302, array $headers = [])
{
parent::__construct('', $status, $headers);
@@ -42,25 +42,15 @@ class RedirectResponse extends Response
throw new \InvalidArgumentException(sprintf('The HTTP status code is not a redirect ("%s" given).', $status));
}
if (301 == $status && !array_key_exists('cache-control', $headers)) {
if (301 == $status && !\array_key_exists('cache-control', array_change_key_case($headers, \CASE_LOWER))) {
$this->headers->remove('cache-control');
}
}
/**
* {@inheritdoc}
*/
public static function create($url = '', $status = 302, $headers = array())
{
return new static($url, $status, $headers);
}
/**
* Returns the target URL.
*
* @return string target URL
*/
public function getTargetUrl()
public function getTargetUrl(): string
{
return $this->targetUrl;
}
@@ -68,15 +58,13 @@ class RedirectResponse extends Response
/**
* Sets the redirect target of this response.
*
* @param string $url The URL to redirect to
*
* @return $this
*
* @throws \InvalidArgumentException
*/
public function setTargetUrl($url)
public function setTargetUrl(string $url): static
{
if (empty($url)) {
if ('' === $url) {
throw new \InvalidArgumentException('Cannot redirect to an empty URL.');
}
@@ -87,14 +75,14 @@ class RedirectResponse extends Response
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="refresh" content="0;url=%1$s" />
<meta http-equiv="refresh" content="0;url=\'%1$s\'" />
<title>Redirecting to %1$s</title>
</head>
<body>
Redirecting to <a href="%1$s">%1$s</a>.
</body>
</html>', htmlspecialchars($url, ENT_QUOTES, 'UTF-8')));
</html>', htmlspecialchars($url, \ENT_QUOTES, 'UTF-8')));
$this->headers->set('Location', $url);

File diff suppressed because it is too large Load Diff

View File

@@ -18,51 +18,43 @@ namespace Symfony\Component\HttpFoundation;
*/
class RequestMatcher implements RequestMatcherInterface
{
/**
* @var string|null
*/
private $path;
/**
* @var string|null
*/
private $host;
private ?string $path = null;
private ?string $host = null;
private ?int $port = null;
/**
* @var string[]
*/
private $methods = array();
private array $methods = [];
/**
* @var string[]
*/
private $ips = array();
/**
* @var array
*/
private $attributes = array();
private array $ips = [];
/**
* @var string[]
*/
private $schemes = array();
private array $attributes = [];
/**
* @var string[]
*/
private array $schemes = [];
/**
* @param string|null $path
* @param string|null $host
* @param string|string[]|null $methods
* @param string|string[]|null $ips
* @param array $attributes
* @param string|string[]|null $schemes
*/
public function __construct($path = null, $host = null, $methods = null, $ips = null, array $attributes = array(), $schemes = null)
public function __construct(string $path = null, string $host = null, string|array $methods = null, string|array $ips = null, array $attributes = [], string|array $schemes = null, int $port = null)
{
$this->matchPath($path);
$this->matchHost($host);
$this->matchMethod($methods);
$this->matchIps($ips);
$this->matchScheme($schemes);
$this->matchPort($port);
foreach ($attributes as $k => $v) {
$this->matchAttribute($k, $v);
@@ -74,27 +66,33 @@ class RequestMatcher implements RequestMatcherInterface
*
* @param string|string[]|null $scheme An HTTP scheme or an array of HTTP schemes
*/
public function matchScheme($scheme)
public function matchScheme(string|array|null $scheme)
{
$this->schemes = null !== $scheme ? array_map('strtolower', (array) $scheme) : array();
$this->schemes = null !== $scheme ? array_map('strtolower', (array) $scheme) : [];
}
/**
* Adds a check for the URL host name.
*
* @param string|null $regexp A Regexp
*/
public function matchHost($regexp)
public function matchHost(?string $regexp)
{
$this->host = $regexp;
}
/**
* Adds a check for the URL path info.
* Adds a check for the the URL port.
*
* @param string|null $regexp A Regexp
* @param int|null $port The port number to connect to
*/
public function matchPath($regexp)
public function matchPort(?int $port)
{
$this->port = $port;
}
/**
* Adds a check for the URL path info.
*/
public function matchPath(?string $regexp)
{
$this->path = $regexp;
}
@@ -104,7 +102,7 @@ class RequestMatcher implements RequestMatcherInterface
*
* @param string $ip A specific IP address or a range specified using IP/netmask like 192.168.1.0/24
*/
public function matchIp($ip)
public function matchIp(string $ip)
{
$this->matchIps($ip);
}
@@ -114,9 +112,13 @@ class RequestMatcher implements RequestMatcherInterface
*
* @param string|string[]|null $ips A specific IP address or a range specified using IP/netmask like 192.168.1.0/24
*/
public function matchIps($ips)
public function matchIps(string|array|null $ips)
{
$this->ips = null !== $ips ? (array) $ips : array();
$ips = null !== $ips ? (array) $ips : [];
$this->ips = array_reduce($ips, static function (array $ips, string $ip) {
return array_merge($ips, preg_split('/\s*,\s*/', $ip));
}, []);
}
/**
@@ -124,18 +126,15 @@ class RequestMatcher implements RequestMatcherInterface
*
* @param string|string[]|null $method An HTTP method or an array of HTTP methods
*/
public function matchMethod($method)
public function matchMethod(string|array|null $method)
{
$this->methods = null !== $method ? array_map('strtoupper', (array) $method) : array();
$this->methods = null !== $method ? array_map('strtoupper', (array) $method) : [];
}
/**
* Adds a check for request attribute.
*
* @param string $key The request attribute name
* @param string $regexp A Regexp
*/
public function matchAttribute($key, $regexp)
public function matchAttribute(string $key, string $regexp)
{
$this->attributes[$key] = $regexp;
}
@@ -143,18 +142,22 @@ class RequestMatcher implements RequestMatcherInterface
/**
* {@inheritdoc}
*/
public function matches(Request $request)
public function matches(Request $request): bool
{
if ($this->schemes && !in_array($request->getScheme(), $this->schemes, true)) {
if ($this->schemes && !\in_array($request->getScheme(), $this->schemes, true)) {
return false;
}
if ($this->methods && !in_array($request->getMethod(), $this->methods, true)) {
if ($this->methods && !\in_array($request->getMethod(), $this->methods, true)) {
return false;
}
foreach ($this->attributes as $key => $pattern) {
if (!preg_match('{'.$pattern.'}', $request->attributes->get($key))) {
$requestAttribute = $request->attributes->get($key);
if (!\is_string($requestAttribute)) {
return false;
}
if (!preg_match('{'.$pattern.'}', $requestAttribute)) {
return false;
}
}
@@ -167,12 +170,16 @@ class RequestMatcher implements RequestMatcherInterface
return false;
}
if (IpUtils::checkIp($request->getClientIp(), $this->ips)) {
if (null !== $this->port && 0 < $this->port && $request->getPort() !== $this->port) {
return false;
}
if (IpUtils::checkIp($request->getClientIp() ?? '', $this->ips)) {
return true;
}
// Note to future implementors: add additional checks above the
// foreach above or else your check might not be run!
return count($this->ips) === 0;
return 0 === \count($this->ips);
}
}

View File

@@ -20,10 +20,6 @@ interface RequestMatcherInterface
{
/**
* Decides whether the rule(s) implemented by the strategy matches the supplied request.
*
* @param Request $request The request to check for a match
*
* @return bool true if the request matches, false otherwise
*/
public function matches(Request $request);
public function matches(Request $request): bool;
}

View File

@@ -11,6 +11,9 @@
namespace Symfony\Component\HttpFoundation;
use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
/**
* Request stack that controls the lifecycle of requests.
*
@@ -21,7 +24,7 @@ class RequestStack
/**
* @var Request[]
*/
private $requests = array();
private array $requests = [];
/**
* Pushes a Request on the stack.
@@ -41,39 +44,32 @@ class RequestStack
*
* This method should generally not be called directly as the stack
* management should be taken care of by the application itself.
*
* @return Request|null
*/
public function pop()
public function pop(): ?Request
{
if (!$this->requests) {
return;
return null;
}
return array_pop($this->requests);
}
/**
* @return Request|null
*/
public function getCurrentRequest()
public function getCurrentRequest(): ?Request
{
return end($this->requests) ?: null;
}
/**
* Gets the master Request.
* Gets the main request.
*
* Be warned that making your code aware of the master request
* Be warned that making your code aware of the main request
* might make it un-compatible with other features of your framework
* like ESI support.
*
* @return Request|null
*/
public function getMasterRequest()
public function getMainRequest(): ?Request
{
if (!$this->requests) {
return;
return null;
}
return $this->requests[0];
@@ -86,18 +82,26 @@ class RequestStack
* might make it un-compatible with other features of your framework
* like ESI support.
*
* If current Request is the master request, it returns null.
*
* @return Request|null
* If current Request is the main request, it returns null.
*/
public function getParentRequest()
public function getParentRequest(): ?Request
{
$pos = count($this->requests) - 2;
$pos = \count($this->requests) - 2;
if (!isset($this->requests[$pos])) {
return;
return $this->requests[$pos] ?? null;
}
/**
* Gets the current session.
*
* @throws SessionNotFoundException
*/
public function getSession(): SessionInterface
{
if ((null !== $request = end($this->requests) ?: null) && $request->hasSession()) {
return $request->getSession();
}
return $this->requests[$pos];
throw new SessionNotFoundException();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -18,51 +18,38 @@ namespace Symfony\Component\HttpFoundation;
*/
class ResponseHeaderBag extends HeaderBag
{
const COOKIES_FLAT = 'flat';
const COOKIES_ARRAY = 'array';
public const COOKIES_FLAT = 'flat';
public const COOKIES_ARRAY = 'array';
const DISPOSITION_ATTACHMENT = 'attachment';
const DISPOSITION_INLINE = 'inline';
public const DISPOSITION_ATTACHMENT = 'attachment';
public const DISPOSITION_INLINE = 'inline';
/**
* @var array
*/
protected $computedCacheControl = array();
protected $computedCacheControl = [];
protected $cookies = [];
protected $headerNames = [];
/**
* @var array
*/
protected $cookies = array();
/**
* @var array
*/
protected $headerNames = array();
/**
* Constructor.
*
* @param array $headers An array of HTTP headers
*/
public function __construct(array $headers = array())
public function __construct(array $headers = [])
{
parent::__construct($headers);
if (!isset($this->headers['cache-control'])) {
$this->set('Cache-Control', '');
}
/* RFC2616 - 14.18 says all Responses need to have a Date */
if (!isset($this->headers['date'])) {
$this->initDate();
}
}
/**
* Returns the headers, with original capitalizations.
*
* @return array An array of headers
*/
public function allPreserveCase()
public function allPreserveCase(): array
{
$headers = array();
$headers = [];
foreach ($this->all() as $name => $value) {
$headers[isset($this->headerNames[$name]) ? $this->headerNames[$name] : $name] = $value;
$headers[$this->headerNames[$name] ?? $name] = $value;
}
return $headers;
@@ -81,23 +68,34 @@ class ResponseHeaderBag extends HeaderBag
/**
* {@inheritdoc}
*/
public function replace(array $headers = array())
public function replace(array $headers = [])
{
$this->headerNames = array();
$this->headerNames = [];
parent::replace($headers);
if (!isset($this->headers['cache-control'])) {
$this->set('Cache-Control', '');
}
if (!isset($this->headers['date'])) {
$this->initDate();
}
}
/**
* {@inheritdoc}
*/
public function all()
public function all(string $key = null): array
{
$headers = parent::all();
if (null !== $key) {
$key = strtr($key, self::UPPER, self::LOWER);
return 'set-cookie' !== $key ? $headers[$key] ?? [] : array_map('strval', $this->getCookies());
}
foreach ($this->getCookies() as $cookie) {
$headers['set-cookie'][] = (string) $cookie;
}
@@ -108,13 +106,13 @@ class ResponseHeaderBag extends HeaderBag
/**
* {@inheritdoc}
*/
public function set($key, $values, $replace = true)
public function set(string $key, string|array|null $values, bool $replace = true)
{
$uniqueKey = str_replace('_', '-', strtolower($key));
$uniqueKey = strtr($key, self::UPPER, self::LOWER);
if ('set-cookie' === $uniqueKey) {
if ($replace) {
$this->cookies = array();
$this->cookies = [];
}
foreach ((array) $values as $cookie) {
$this->setCookie(Cookie::fromString($cookie));
@@ -129,9 +127,8 @@ class ResponseHeaderBag extends HeaderBag
parent::set($key, $values, $replace);
// ensure the cache-control header has sensible defaults
if (in_array($uniqueKey, array('cache-control', 'etag', 'last-modified', 'expires'))) {
$computed = $this->computeCacheControlValue();
$this->headers['cache-control'] = array($computed);
if (\in_array($uniqueKey, ['cache-control', 'etag', 'last-modified', 'expires'], true) && '' !== $computed = $this->computeCacheControlValue()) {
$this->headers['cache-control'] = [$computed];
$this->headerNames['cache-control'] = 'Cache-Control';
$this->computedCacheControl = $this->parseCacheControl($computed);
}
@@ -140,13 +137,13 @@ class ResponseHeaderBag extends HeaderBag
/**
* {@inheritdoc}
*/
public function remove($key)
public function remove(string $key)
{
$uniqueKey = str_replace('_', '-', strtolower($key));
$uniqueKey = strtr($key, self::UPPER, self::LOWER);
unset($this->headerNames[$uniqueKey]);
if ('set-cookie' === $uniqueKey) {
$this->cookies = array();
$this->cookies = [];
return;
}
@@ -154,31 +151,30 @@ class ResponseHeaderBag extends HeaderBag
parent::remove($key);
if ('cache-control' === $uniqueKey) {
$this->computedCacheControl = array();
$this->computedCacheControl = [];
}
if ('date' === $uniqueKey) {
$this->initDate();
}
}
/**
* {@inheritdoc}
*/
public function hasCacheControlDirective($key)
public function hasCacheControlDirective(string $key): bool
{
return array_key_exists($key, $this->computedCacheControl);
return \array_key_exists($key, $this->computedCacheControl);
}
/**
* {@inheritdoc}
*/
public function getCacheControlDirective($key)
public function getCacheControlDirective(string $key): bool|string|null
{
return array_key_exists($key, $this->computedCacheControl) ? $this->computedCacheControl[$key] : null;
return $this->computedCacheControl[$key] ?? null;
}
/**
* Sets a cookie.
*
* @param Cookie $cookie
*/
public function setCookie(Cookie $cookie)
{
$this->cookies[$cookie->getDomain()][$cookie->getPath()][$cookie->getName()] = $cookie;
@@ -187,12 +183,8 @@ class ResponseHeaderBag extends HeaderBag
/**
* Removes a cookie from the array, but does not unset it in the browser.
*
* @param string $name
* @param string $path
* @param string $domain
*/
public function removeCookie($name, $path = '/', $domain = null)
public function removeCookie(string $name, ?string $path = '/', string $domain = null)
{
if (null === $path) {
$path = '/';
@@ -216,23 +208,21 @@ class ResponseHeaderBag extends HeaderBag
/**
* Returns an array with all cookies.
*
* @param string $format
*
* @return array
* @return Cookie[]
*
* @throws \InvalidArgumentException When the $format is invalid
*/
public function getCookies($format = self::COOKIES_FLAT)
public function getCookies(string $format = self::COOKIES_FLAT): array
{
if (!in_array($format, array(self::COOKIES_FLAT, self::COOKIES_ARRAY))) {
throw new \InvalidArgumentException(sprintf('Format "%s" invalid (%s).', $format, implode(', ', array(self::COOKIES_FLAT, self::COOKIES_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])));
}
if (self::COOKIES_ARRAY === $format) {
return $this->cookies;
}
$flattenedCookies = array();
$flattenedCookies = [];
foreach ($this->cookies as $path) {
foreach ($path as $cookies) {
foreach ($cookies as $cookie) {
@@ -246,65 +236,18 @@ class ResponseHeaderBag extends HeaderBag
/**
* Clears a cookie in the browser.
*
* @param string $name
* @param string $path
* @param string $domain
* @param bool $secure
* @param bool $httpOnly
*/
public function clearCookie($name, $path = '/', $domain = null, $secure = false, $httpOnly = true)
public function clearCookie(string $name, ?string $path = '/', string $domain = null, bool $secure = false, bool $httpOnly = true, string $sameSite = null)
{
$this->setCookie(new Cookie($name, null, 1, $path, $domain, $secure, $httpOnly));
$this->setCookie(new Cookie($name, null, 1, $path, $domain, $secure, $httpOnly, false, $sameSite));
}
/**
* Generates a HTTP Content-Disposition field-value.
*
* @param string $disposition One of "inline" or "attachment"
* @param string $filename A unicode string
* @param string $filenameFallback A string containing only ASCII characters that
* is semantically equivalent to $filename. If the filename is already ASCII,
* it can be omitted, or just copied from $filename
*
* @return string A string suitable for use as a Content-Disposition field-value
*
* @throws \InvalidArgumentException
*
* @see RFC 6266
* @see HeaderUtils::makeDisposition()
*/
public function makeDisposition($disposition, $filename, $filenameFallback = '')
public function makeDisposition(string $disposition, string $filename, string $filenameFallback = '')
{
if (!in_array($disposition, array(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) {
$filenameFallback = $filename;
}
// filenameFallback is not ASCII.
if (!preg_match('/^[\x20-\x7e]*$/', $filenameFallback)) {
throw new \InvalidArgumentException('The filename fallback must only contain ASCII characters.');
}
// percent characters aren't safe in fallback.
if (false !== strpos($filenameFallback, '%')) {
throw new \InvalidArgumentException('The filename fallback cannot contain the "%" character.');
}
// path separators aren't allowed in either.
if (false !== strpos($filename, '/') || false !== strpos($filename, '\\') || false !== strpos($filenameFallback, '/') || false !== strpos($filenameFallback, '\\')) {
throw new \InvalidArgumentException('The filename and the fallback cannot contain the "/" and "\\" characters.');
}
$output = sprintf('%s; filename="%s"', $disposition, str_replace('"', '\\"', $filenameFallback));
if ($filename !== $filenameFallback) {
$output .= sprintf("; filename*=utf-8''%s", rawurlencode($filename));
}
return $output;
return HeaderUtils::makeDisposition($disposition, $filename, $filenameFallback);
}
/**
@@ -312,18 +255,16 @@ class ResponseHeaderBag extends HeaderBag
*
* This considers several other headers and calculates or modifies the
* cache-control header to a sensible, conservative value.
*
* @return string
*/
protected function computeCacheControlValue()
protected function computeCacheControlValue(): string
{
if (!$this->cacheControl && !$this->has('ETag') && !$this->has('Last-Modified') && !$this->has('Expires')) {
return 'no-cache, private';
}
if (!$this->cacheControl) {
if ($this->has('Last-Modified') || $this->has('Expires')) {
return 'private, must-revalidate'; // allows for heuristic expiration (RFC 7234 Section 4.2.2) in the case of "Last-Modified"
}
// conservative by default
return 'private, must-revalidate';
return 'no-cache, private';
}
$header = $this->getCacheControlHeader();
@@ -338,4 +279,9 @@ class ResponseHeaderBag extends HeaderBag
return $header;
}
private function initDate(): void
{
$this->set('Date', gmdate('D, d M Y H:i:s').' GMT');
}
}

View File

@@ -22,37 +22,32 @@ class ServerBag extends ParameterBag
{
/**
* Gets the HTTP headers.
*
* @return array
*/
public function getHeaders()
public function getHeaders(): array
{
$headers = array();
$contentHeaders = array('CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true);
$headers = [];
foreach ($this->parameters as $key => $value) {
if (0 === strpos($key, 'HTTP_')) {
if (str_starts_with($key, 'HTTP_')) {
$headers[substr($key, 5)] = $value;
}
// CONTENT_* are not prefixed with HTTP_
elseif (isset($contentHeaders[$key])) {
} elseif (\in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH', 'CONTENT_MD5'], true)) {
$headers[$key] = $value;
}
}
if (isset($this->parameters['PHP_AUTH_USER'])) {
$headers['PHP_AUTH_USER'] = $this->parameters['PHP_AUTH_USER'];
$headers['PHP_AUTH_PW'] = isset($this->parameters['PHP_AUTH_PW']) ? $this->parameters['PHP_AUTH_PW'] : '';
$headers['PHP_AUTH_PW'] = $this->parameters['PHP_AUTH_PW'] ?? '';
} else {
/*
* php-cgi under Apache does not pass HTTP Basic user/pass to PHP by default
* For this workaround to work, add these lines to your .htaccess file:
* RewriteCond %{HTTP:Authorization} ^(.+)$
* RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
* RewriteCond %{HTTP:Authorization} .+
* RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0]
*
* A sample .htaccess file:
* RewriteEngine On
* RewriteCond %{HTTP:Authorization} ^(.+)$
* RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
* RewriteCond %{HTTP:Authorization} .+
* RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0]
* RewriteCond %{REQUEST_FILENAME} !-f
* RewriteRule ^(.*)$ app.php [QSA,L]
*/
@@ -68,8 +63,8 @@ class ServerBag extends ParameterBag
if (0 === stripos($authorizationHeader, 'basic ')) {
// Decode AUTHORIZATION header into PHP_AUTH_USER and PHP_AUTH_PW when authorization header is basic
$exploded = explode(':', base64_decode(substr($authorizationHeader, 6)), 2);
if (count($exploded) == 2) {
list($headers['PHP_AUTH_USER'], $headers['PHP_AUTH_PW']) = $exploded;
if (2 == \count($exploded)) {
[$headers['PHP_AUTH_USER'], $headers['PHP_AUTH_PW']] = $exploded;
}
} elseif (empty($this->parameters['PHP_AUTH_DIGEST']) && (0 === stripos($authorizationHeader, 'digest '))) {
// In some circumstances PHP_AUTH_DIGEST needs to be set
@@ -79,7 +74,7 @@ class ServerBag extends ParameterBag
/*
* XXX: Since there is no PHP_AUTH_BEARER in PHP predefined variables,
* I'll just set $headers['AUTHORIZATION'] here.
* http://php.net/manual/en/reserved.variables.server.php
* https://php.net/reserved.variables.server
*/
$headers['AUTHORIZATION'] = $authorizationHeader;
}
@@ -92,7 +87,7 @@ class ServerBag extends ParameterBag
// PHP_AUTH_USER/PHP_AUTH_PW
if (isset($headers['PHP_AUTH_USER'])) {
$headers['AUTHORIZATION'] = 'Basic '.base64_encode($headers['PHP_AUTH_USER'].':'.$headers['PHP_AUTH_PW']);
$headers['AUTHORIZATION'] = 'Basic '.base64_encode($headers['PHP_AUTH_USER'].':'.($headers['PHP_AUTH_PW'] ?? ''));
} elseif (isset($headers['PHP_AUTH_DIGEST'])) {
$headers['AUTHORIZATION'] = $headers['PHP_AUTH_DIGEST'];
}

View File

@@ -13,27 +13,20 @@ namespace Symfony\Component\HttpFoundation\Session\Attribute;
/**
* This class relates to session attribute storage.
*
* @implements \IteratorAggregate<string, mixed>
*/
class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Countable
{
private $name = 'attributes';
private string $name = 'attributes';
private string $storageKey;
protected $attributes = [];
/**
* @var string
*/
private $storageKey;
/**
* @var array
*/
protected $attributes = array();
/**
* Constructor.
*
* @param string $storageKey The key used to store attributes in the session
*/
public function __construct($storageKey = '_sf2_attributes')
public function __construct(string $storageKey = '_sf2_attributes')
{
$this->storageKey = $storageKey;
}
@@ -41,12 +34,12 @@ class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Counta
/**
* {@inheritdoc}
*/
public function getName()
public function getName(): string
{
return $this->name;
}
public function setName($name)
public function setName(string $name)
{
$this->name = $name;
}
@@ -62,7 +55,7 @@ class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Counta
/**
* {@inheritdoc}
*/
public function getStorageKey()
public function getStorageKey(): string
{
return $this->storageKey;
}
@@ -70,23 +63,23 @@ class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Counta
/**
* {@inheritdoc}
*/
public function has($name)
public function has(string $name): bool
{
return array_key_exists($name, $this->attributes);
return \array_key_exists($name, $this->attributes);
}
/**
* {@inheritdoc}
*/
public function get($name, $default = null)
public function get(string $name, mixed $default = null): mixed
{
return array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default;
return \array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default;
}
/**
* {@inheritdoc}
*/
public function set($name, $value)
public function set(string $name, mixed $value)
{
$this->attributes[$name] = $value;
}
@@ -94,7 +87,7 @@ class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Counta
/**
* {@inheritdoc}
*/
public function all()
public function all(): array
{
return $this->attributes;
}
@@ -104,7 +97,7 @@ class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Counta
*/
public function replace(array $attributes)
{
$this->attributes = array();
$this->attributes = [];
foreach ($attributes as $key => $value) {
$this->set($key, $value);
}
@@ -113,10 +106,10 @@ class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Counta
/**
* {@inheritdoc}
*/
public function remove($name)
public function remove(string $name): mixed
{
$retval = null;
if (array_key_exists($name, $this->attributes)) {
if (\array_key_exists($name, $this->attributes)) {
$retval = $this->attributes[$name];
unset($this->attributes[$name]);
}
@@ -127,10 +120,10 @@ class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Counta
/**
* {@inheritdoc}
*/
public function clear()
public function clear(): mixed
{
$return = $this->attributes;
$this->attributes = array();
$this->attributes = [];
return $return;
}
@@ -138,20 +131,18 @@ class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Counta
/**
* Returns an iterator for attributes.
*
* @return \ArrayIterator An \ArrayIterator instance
* @return \ArrayIterator<string, mixed>
*/
public function getIterator()
public function getIterator(): \ArrayIterator
{
return new \ArrayIterator($this->attributes);
}
/**
* Returns the number of attributes.
*
* @return int The number of attributes
*/
public function count()
public function count(): int
{
return count($this->attributes);
return \count($this->attributes);
}
}

View File

@@ -22,51 +22,32 @@ interface AttributeBagInterface extends SessionBagInterface
{
/**
* Checks if an attribute is defined.
*
* @param string $name The attribute name
*
* @return bool true if the attribute is defined, false otherwise
*/
public function has($name);
public function has(string $name): bool;
/**
* Returns an attribute.
*
* @param string $name The attribute name
* @param mixed $default The default value if not found
*
* @return mixed
*/
public function get($name, $default = null);
public function get(string $name, mixed $default = null): mixed;
/**
* Sets an attribute.
*
* @param string $name
* @param mixed $value
*/
public function set($name, $value);
public function set(string $name, mixed $value);
/**
* Returns attributes.
*
* @return array Attributes
* @return array<string, mixed>
*/
public function all();
public function all(): array;
/**
* Sets attributes.
*
* @param array $attributes Attributes
*/
public function replace(array $attributes);
/**
* Removes an attribute.
*
* @param string $name
*
* @return mixed The removed value or null when it does not exist
*/
public function remove($name);
public function remove(string $name): mixed;
}

View File

@@ -1,160 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session\Attribute;
/**
* This class provides structured storage of session attributes using
* a name spacing character in the key.
*
* @author Drak <drak@zikula.org>
*/
class NamespacedAttributeBag extends AttributeBag
{
/**
* Namespace character.
*
* @var string
*/
private $namespaceCharacter;
/**
* Constructor.
*
* @param string $storageKey Session storage key
* @param string $namespaceCharacter Namespace character to use in keys
*/
public function __construct($storageKey = '_sf2_attributes', $namespaceCharacter = '/')
{
$this->namespaceCharacter = $namespaceCharacter;
parent::__construct($storageKey);
}
/**
* {@inheritdoc}
*/
public function has($name)
{
// reference mismatch: if fixed, re-introduced in array_key_exists; keep as it is
$attributes = $this->resolveAttributePath($name);
$name = $this->resolveKey($name);
if (null === $attributes) {
return false;
}
return array_key_exists($name, $attributes);
}
/**
* {@inheritdoc}
*/
public function get($name, $default = null)
{
// reference mismatch: if fixed, re-introduced in array_key_exists; keep as it is
$attributes = $this->resolveAttributePath($name);
$name = $this->resolveKey($name);
if (null === $attributes) {
return $default;
}
return array_key_exists($name, $attributes) ? $attributes[$name] : $default;
}
/**
* {@inheritdoc}
*/
public function set($name, $value)
{
$attributes = &$this->resolveAttributePath($name, true);
$name = $this->resolveKey($name);
$attributes[$name] = $value;
}
/**
* {@inheritdoc}
*/
public function remove($name)
{
$retval = null;
$attributes = &$this->resolveAttributePath($name);
$name = $this->resolveKey($name);
if (null !== $attributes && array_key_exists($name, $attributes)) {
$retval = $attributes[$name];
unset($attributes[$name]);
}
return $retval;
}
/**
* Resolves a path in attributes property and returns it as a reference.
*
* This method allows structured namespacing of session attributes.
*
* @param string $name Key name
* @param bool $writeContext Write context, default false
*
* @return array
*/
protected function &resolveAttributePath($name, $writeContext = false)
{
$array = &$this->attributes;
$name = (strpos($name, $this->namespaceCharacter) === 0) ? substr($name, 1) : $name;
// Check if there is anything to do, else return
if (!$name) {
return $array;
}
$parts = explode($this->namespaceCharacter, $name);
if (count($parts) < 2) {
if (!$writeContext) {
return $array;
}
$array[$parts[0]] = array();
return $array;
}
unset($parts[count($parts) - 1]);
foreach ($parts as $part) {
if (null !== $array && !array_key_exists($part, $array)) {
$array[$part] = $writeContext ? array() : null;
}
$array = &$array[$part];
}
return $array;
}
/**
* Resolves the key from the name.
*
* This is the last part in a dot separated string.
*
* @param string $name
*
* @return string
*/
protected function resolveKey($name)
{
if (false !== $pos = strrpos($name, $this->namespaceCharacter)) {
$name = substr($name, $pos + 1);
}
return $name;
}
}

View File

@@ -18,28 +18,14 @@ namespace Symfony\Component\HttpFoundation\Session\Flash;
*/
class AutoExpireFlashBag implements FlashBagInterface
{
private $name = 'flashes';
private string $name = 'flashes';
private array $flashes = ['display' => [], 'new' => []];
private string $storageKey;
/**
* Flash messages.
*
* @var array
*/
private $flashes = array('display' => array(), 'new' => array());
/**
* The storage key for flashes in the session.
*
* @var string
*/
private $storageKey;
/**
* Constructor.
*
* @param string $storageKey The key used to store flashes in the session
*/
public function __construct($storageKey = '_sf2_flashes')
public function __construct(string $storageKey = '_symfony_flashes')
{
$this->storageKey = $storageKey;
}
@@ -47,12 +33,12 @@ class AutoExpireFlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function getName()
public function getName(): string
{
return $this->name;
}
public function setName($name)
public function setName(string $name)
{
$this->name = $name;
}
@@ -67,14 +53,14 @@ class AutoExpireFlashBag implements FlashBagInterface
// The logic: messages from the last request will be stored in new, so we move them to previous
// This request we will show what is in 'display'. What is placed into 'new' this time round will
// be moved to display next time round.
$this->flashes['display'] = array_key_exists('new', $this->flashes) ? $this->flashes['new'] : array();
$this->flashes['new'] = array();
$this->flashes['display'] = \array_key_exists('new', $this->flashes) ? $this->flashes['new'] : [];
$this->flashes['new'] = [];
}
/**
* {@inheritdoc}
*/
public function add($type, $message)
public function add(string $type, mixed $message)
{
$this->flashes['new'][$type][] = $message;
}
@@ -82,7 +68,7 @@ class AutoExpireFlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function peek($type, array $default = array())
public function peek(string $type, array $default = []): array
{
return $this->has($type) ? $this->flashes['display'][$type] : $default;
}
@@ -90,15 +76,15 @@ class AutoExpireFlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function peekAll()
public function peekAll(): array
{
return array_key_exists('display', $this->flashes) ? (array) $this->flashes['display'] : array();
return \array_key_exists('display', $this->flashes) ? $this->flashes['display'] : [];
}
/**
* {@inheritdoc}
*/
public function get($type, array $default = array())
public function get(string $type, array $default = []): array
{
$return = $default;
@@ -117,10 +103,10 @@ class AutoExpireFlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function all()
public function all(): array
{
$return = $this->flashes['display'];
$this->flashes = array('new' => array(), 'display' => array());
$this->flashes['display'] = [];
return $return;
}
@@ -136,7 +122,7 @@ class AutoExpireFlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function set($type, $messages)
public function set(string $type, string|array $messages)
{
$this->flashes['new'][$type] = (array) $messages;
}
@@ -144,15 +130,15 @@ class AutoExpireFlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function has($type)
public function has(string $type): bool
{
return array_key_exists($type, $this->flashes['display']) && $this->flashes['display'][$type];
return \array_key_exists($type, $this->flashes['display']) && $this->flashes['display'][$type];
}
/**
* {@inheritdoc}
*/
public function keys()
public function keys(): array
{
return array_keys($this->flashes['display']);
}
@@ -160,7 +146,7 @@ class AutoExpireFlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function getStorageKey()
public function getStorageKey(): string
{
return $this->storageKey;
}
@@ -168,7 +154,7 @@ class AutoExpireFlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function clear()
public function clear(): mixed
{
return $this->all();
}

View File

@@ -18,28 +18,14 @@ namespace Symfony\Component\HttpFoundation\Session\Flash;
*/
class FlashBag implements FlashBagInterface
{
private $name = 'flashes';
private string $name = 'flashes';
private array $flashes = [];
private string $storageKey;
/**
* Flash messages.
*
* @var array
*/
private $flashes = array();
/**
* The storage key for flashes in the session.
*
* @var string
*/
private $storageKey;
/**
* Constructor.
*
* @param string $storageKey The key used to store flashes in the session
*/
public function __construct($storageKey = '_sf2_flashes')
public function __construct(string $storageKey = '_symfony_flashes')
{
$this->storageKey = $storageKey;
}
@@ -47,12 +33,12 @@ class FlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function getName()
public function getName(): string
{
return $this->name;
}
public function setName($name)
public function setName(string $name)
{
$this->name = $name;
}
@@ -68,7 +54,7 @@ class FlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function add($type, $message)
public function add(string $type, mixed $message)
{
$this->flashes[$type][] = $message;
}
@@ -76,7 +62,7 @@ class FlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function peek($type, array $default = array())
public function peek(string $type, array $default = []): array
{
return $this->has($type) ? $this->flashes[$type] : $default;
}
@@ -84,7 +70,7 @@ class FlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function peekAll()
public function peekAll(): array
{
return $this->flashes;
}
@@ -92,7 +78,7 @@ class FlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function get($type, array $default = array())
public function get(string $type, array $default = []): array
{
if (!$this->has($type)) {
return $default;
@@ -108,10 +94,10 @@ class FlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function all()
public function all(): array
{
$return = $this->peekAll();
$this->flashes = array();
$this->flashes = [];
return $return;
}
@@ -119,7 +105,7 @@ class FlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function set($type, $messages)
public function set(string $type, string|array $messages)
{
$this->flashes[$type] = (array) $messages;
}
@@ -135,15 +121,15 @@ class FlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function has($type)
public function has(string $type): bool
{
return array_key_exists($type, $this->flashes) && $this->flashes[$type];
return \array_key_exists($type, $this->flashes) && $this->flashes[$type];
}
/**
* {@inheritdoc}
*/
public function keys()
public function keys(): array
{
return array_keys($this->flashes);
}
@@ -151,7 +137,7 @@ class FlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function getStorageKey()
public function getStorageKey(): string
{
return $this->storageKey;
}
@@ -159,7 +145,7 @@ class FlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function clear()
public function clear(): mixed
{
return $this->all();
}

View File

@@ -21,75 +21,52 @@ use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
interface FlashBagInterface extends SessionBagInterface
{
/**
* Adds a flash message for type.
*
* @param string $type
* @param string $message
* Adds a flash message for the given type.
*/
public function add($type, $message);
public function add(string $type, mixed $message);
/**
* Registers a message for a given type.
*
* @param string $type
* @param string|array $message
* Registers one or more messages for a given type.
*/
public function set($type, $message);
public function set(string $type, string|array $messages);
/**
* Gets flash messages for a given type.
*
* @param string $type Message category type
* @param array $default Default value if $type does not exist
*
* @return array
*/
public function peek($type, array $default = array());
public function peek(string $type, array $default = []): array;
/**
* Gets all flash messages.
*
* @return array
*/
public function peekAll();
public function peekAll(): array;
/**
* Gets and clears flash from the stack.
*
* @param string $type
* @param array $default Default value if $type does not exist
*
* @return array
* @param array $default Default value if $type does not exist
*/
public function get($type, array $default = array());
public function get(string $type, array $default = []): array;
/**
* Gets and clears flashes from the stack.
*
* @return array
*/
public function all();
public function all(): array;
/**
* Sets all flash messages.
*
* @param array $messages
*/
public function setAll(array $messages);
/**
* Has flash messages for a given type?
*
* @param string $type
*
* @return bool
*/
public function has($type);
public function has(string $type): bool;
/**
* Returns a list of all defined types.
*
* @return array
*/
public function keys();
public function keys(): array;
}

View File

@@ -11,54 +11,45 @@
namespace Symfony\Component\HttpFoundation\Session;
use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface;
use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag;
use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBag;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag;
use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface;
// Help opcache.preload discover always-needed symbols
class_exists(AttributeBag::class);
class_exists(FlashBag::class);
class_exists(SessionBagProxy::class);
/**
* Session.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Drak <drak@zikula.org>
*
* @implements \IteratorAggregate<string, mixed>
*/
class Session implements SessionInterface, \IteratorAggregate, \Countable
{
/**
* Storage driver.
*
* @var SessionStorageInterface
*/
protected $storage;
/**
* @var string
*/
private $flashName;
private string $flashName;
private string $attributeName;
private array $data = [];
private int $usageIndex = 0;
private ?\Closure $usageReporter;
/**
* @var string
*/
private $attributeName;
/**
* Constructor.
*
* @param SessionStorageInterface $storage A SessionStorageInterface instance
* @param AttributeBagInterface $attributes An AttributeBagInterface instance, (defaults null for default AttributeBag)
* @param FlashBagInterface $flashes A FlashBagInterface instance (defaults null for default FlashBag)
*/
public function __construct(SessionStorageInterface $storage = null, AttributeBagInterface $attributes = null, FlashBagInterface $flashes = null)
public function __construct(SessionStorageInterface $storage = null, AttributeBagInterface $attributes = null, FlashBagInterface $flashes = null, callable $usageReporter = null)
{
$this->storage = $storage ?: new NativeSessionStorage();
$this->storage = $storage ?? new NativeSessionStorage();
$this->usageReporter = $usageReporter instanceof \Closure || !\is_callable($usageReporter) ? $usageReporter : \Closure::fromCallable($usageReporter);
$attributes = $attributes ?: new AttributeBag();
$attributes = $attributes ?? new AttributeBag();
$this->attributeName = $attributes->getName();
$this->registerBag($attributes);
$flashes = $flashes ?: new FlashBag();
$flashes = $flashes ?? new FlashBag();
$this->flashName = $flashes->getName();
$this->registerBag($flashes);
}
@@ -66,7 +57,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
/**
* {@inheritdoc}
*/
public function start()
public function start(): bool
{
return $this->storage->start();
}
@@ -74,7 +65,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
/**
* {@inheritdoc}
*/
public function has($name)
public function has(string $name): bool
{
return $this->getAttributeBag()->has($name);
}
@@ -82,7 +73,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
/**
* {@inheritdoc}
*/
public function get($name, $default = null)
public function get(string $name, mixed $default = null): mixed
{
return $this->getAttributeBag()->get($name, $default);
}
@@ -90,7 +81,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
/**
* {@inheritdoc}
*/
public function set($name, $value)
public function set(string $name, mixed $value)
{
$this->getAttributeBag()->set($name, $value);
}
@@ -98,7 +89,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
/**
* {@inheritdoc}
*/
public function all()
public function all(): array
{
return $this->getAttributeBag()->all();
}
@@ -114,7 +105,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
/**
* {@inheritdoc}
*/
public function remove($name)
public function remove(string $name): mixed
{
return $this->getAttributeBag()->remove($name);
}
@@ -124,13 +115,13 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
*/
public function clear()
{
$this->storage->getBag($this->attributeName)->clear();
$this->getAttributeBag()->clear();
}
/**
* {@inheritdoc}
*/
public function isStarted()
public function isStarted(): bool
{
return $this->storage->isStarted();
}
@@ -138,27 +129,50 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
/**
* Returns an iterator for attributes.
*
* @return \ArrayIterator An \ArrayIterator instance
* @return \ArrayIterator<string, mixed>
*/
public function getIterator()
public function getIterator(): \ArrayIterator
{
return new \ArrayIterator($this->getAttributeBag()->all());
}
/**
* Returns the number of attributes.
*
* @return int The number of attributes
*/
public function count()
public function count(): int
{
return count($this->getAttributeBag()->all());
return \count($this->getAttributeBag()->all());
}
public function &getUsageIndex(): int
{
return $this->usageIndex;
}
/**
* @internal
*/
public function isEmpty(): bool
{
if ($this->isStarted()) {
++$this->usageIndex;
if ($this->usageReporter && 0 <= $this->usageIndex) {
($this->usageReporter)();
}
}
foreach ($this->data as &$data) {
if (!empty($data)) {
return false;
}
}
return true;
}
/**
* {@inheritdoc}
*/
public function invalidate($lifetime = null)
public function invalidate(int $lifetime = null): bool
{
$this->storage->clear();
@@ -168,7 +182,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
/**
* {@inheritdoc}
*/
public function migrate($destroy = false, $lifetime = null)
public function migrate(bool $destroy = false, int $lifetime = null): bool
{
return $this->storage->regenerate($destroy, $lifetime);
}
@@ -184,7 +198,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
/**
* {@inheritdoc}
*/
public function getId()
public function getId(): string
{
return $this->storage->getId();
}
@@ -192,15 +206,17 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
/**
* {@inheritdoc}
*/
public function setId($id)
public function setId(string $id)
{
$this->storage->setId($id);
if ($this->storage->getId() !== $id) {
$this->storage->setId($id);
}
}
/**
* {@inheritdoc}
*/
public function getName()
public function getName(): string
{
return $this->storage->getName();
}
@@ -208,7 +224,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
/**
* {@inheritdoc}
*/
public function setName($name)
public function setName(string $name)
{
$this->storage->setName($name);
}
@@ -216,8 +232,13 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
/**
* {@inheritdoc}
*/
public function getMetadataBag()
public function getMetadataBag(): MetadataBag
{
++$this->usageIndex;
if ($this->usageReporter && 0 <= $this->usageIndex) {
($this->usageReporter)();
}
return $this->storage->getMetadataBag();
}
@@ -226,23 +247,23 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
*/
public function registerBag(SessionBagInterface $bag)
{
$this->storage->registerBag($bag);
$this->storage->registerBag(new SessionBagProxy($bag, $this->data, $this->usageIndex, $this->usageReporter));
}
/**
* {@inheritdoc}
*/
public function getBag($name)
public function getBag(string $name): SessionBagInterface
{
return $this->storage->getBag($name);
$bag = $this->storage->getBag($name);
return method_exists($bag, 'getBag') ? $bag->getBag() : $bag;
}
/**
* Gets the flashbag interface.
*
* @return FlashBagInterface
*/
public function getFlashBag()
public function getFlashBag(): FlashBagInterface
{
return $this->getBag($this->flashName);
}
@@ -251,11 +272,9 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
* Gets the attributebag interface.
*
* Note that this method was added to help with IDE autocompletion.
*
* @return AttributeBagInterface
*/
private function getAttributeBag()
private function getAttributeBag(): AttributeBagInterface
{
return $this->storage->getBag($this->attributeName);
return $this->getBag($this->attributeName);
}
}

View File

@@ -20,29 +20,23 @@ interface SessionBagInterface
{
/**
* Gets this bag's name.
*
* @return string
*/
public function getName();
public function getName(): string;
/**
* Initializes the Bag.
*
* @param array $array
*/
public function initialize(array &$array);
/**
* Gets the storage key for this bag.
*
* @return string
*/
public function getStorageKey();
public function getStorageKey(): string;
/**
* Clears out data from bag.
*
* @return mixed Whatever data was contained
*/
public function clear();
public function clear(): mixed;
}

View File

@@ -0,0 +1,95 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
final class SessionBagProxy implements SessionBagInterface
{
private $bag;
private array $data;
private ?int $usageIndex;
private ?\Closure $usageReporter;
public function __construct(SessionBagInterface $bag, array &$data, ?int &$usageIndex, ?callable $usageReporter)
{
$this->bag = $bag;
$this->data = &$data;
$this->usageIndex = &$usageIndex;
$this->usageReporter = $usageReporter instanceof \Closure || !\is_callable($usageReporter) ? $usageReporter : \Closure::fromCallable($usageReporter);
}
public function getBag(): SessionBagInterface
{
++$this->usageIndex;
if ($this->usageReporter && 0 <= $this->usageIndex) {
($this->usageReporter)();
}
return $this->bag;
}
public function isEmpty(): bool
{
if (!isset($this->data[$this->bag->getStorageKey()])) {
return true;
}
++$this->usageIndex;
if ($this->usageReporter && 0 <= $this->usageIndex) {
($this->usageReporter)();
}
return empty($this->data[$this->bag->getStorageKey()]);
}
/**
* {@inheritdoc}
*/
public function getName(): string
{
return $this->bag->getName();
}
/**
* {@inheritdoc}
*/
public function initialize(array &$array): void
{
++$this->usageIndex;
if ($this->usageReporter && 0 <= $this->usageIndex) {
($this->usageReporter)();
}
$this->data[$this->bag->getStorageKey()] = &$array;
$this->bag->initialize($array);
}
/**
* {@inheritdoc}
*/
public function getStorageKey(): string
{
return $this->bag->getStorageKey();
}
/**
* {@inheritdoc}
*/
public function clear(): mixed
{
return $this->bag->clear();
}
}

View File

@@ -0,0 +1,40 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageFactoryInterface;
// Help opcache.preload discover always-needed symbols
class_exists(Session::class);
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class SessionFactory implements SessionFactoryInterface
{
private $requestStack;
private $storageFactory;
private ?\Closure $usageReporter;
public function __construct(RequestStack $requestStack, SessionStorageFactoryInterface $storageFactory, callable $usageReporter = null)
{
$this->requestStack = $requestStack;
$this->storageFactory = $storageFactory;
$this->usageReporter = $usageReporter instanceof \Closure || !\is_callable($usageReporter) ? $usageReporter : \Closure::fromCallable($usageReporter);
}
public function createSession(): SessionInterface
{
return new Session($this->storageFactory->createStorage($this->requestStack->getMainRequest()), null, null, $this->usageReporter);
}
}

View File

@@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
interface SessionFactoryInterface
{
public function createSession(): SessionInterface;
}

View File

@@ -23,39 +23,29 @@ interface SessionInterface
/**
* Starts the session storage.
*
* @return bool True if session started
*
* @throws \RuntimeException If session fails to start.
* @throws \RuntimeException if session fails to start
*/
public function start();
public function start(): bool;
/**
* Returns the session ID.
*
* @return string The session ID
*/
public function getId();
public function getId(): string;
/**
* Sets the session ID.
*
* @param string $id
*/
public function setId($id);
public function setId(string $id);
/**
* Returns the session name.
*
* @return mixed The session name
*/
public function getName();
public function getName(): string;
/**
* Sets the session name.
*
* @param string $name
*/
public function setName($name);
public function setName(string $name);
/**
* Invalidates the current session.
@@ -67,10 +57,8 @@ interface SessionInterface
* will leave the system settings unchanged, 0 sets the cookie
* to expire with browser session. Time is in seconds, and is
* not a Unix timestamp.
*
* @return bool True if session invalidated, false if error
*/
public function invalidate($lifetime = null);
public function invalidate(int $lifetime = null): bool;
/**
* Migrates the current session to a new session id while maintaining all
@@ -81,10 +69,8 @@ interface SessionInterface
* will leave the system settings unchanged, 0 sets the cookie
* to expire with browser session. Time is in seconds, and is
* not a Unix timestamp.
*
* @return bool True if session migrated, false if error
*/
public function migrate($destroy = false, $lifetime = null);
public function migrate(bool $destroy = false, int $lifetime = null): bool;
/**
* Force the session to be saved and closed.
@@ -97,53 +83,35 @@ interface SessionInterface
/**
* Checks if an attribute is defined.
*
* @param string $name The attribute name
*
* @return bool true if the attribute is defined, false otherwise
*/
public function has($name);
public function has(string $name): bool;
/**
* Returns an attribute.
*
* @param string $name The attribute name
* @param mixed $default The default value if not found
*
* @return mixed
*/
public function get($name, $default = null);
public function get(string $name, mixed $default = null): mixed;
/**
* Sets an attribute.
*
* @param string $name
* @param mixed $value
*/
public function set($name, $value);
public function set(string $name, mixed $value);
/**
* Returns attributes.
*
* @return array Attributes
*/
public function all();
public function all(): array;
/**
* Sets attributes.
*
* @param array $attributes Attributes
*/
public function replace(array $attributes);
/**
* Removes an attribute.
*
* @param string $name
*
* @return mixed The removed value or null when it does not exist
*/
public function remove($name);
public function remove(string $name): mixed;
/**
* Clears all attributes.
@@ -152,31 +120,21 @@ interface SessionInterface
/**
* Checks if the session was started.
*
* @return bool
*/
public function isStarted();
public function isStarted(): bool;
/**
* Registers a SessionBagInterface with the session.
*
* @param SessionBagInterface $bag
*/
public function registerBag(SessionBagInterface $bag);
/**
* Gets a bag instance by name.
*
* @param string $name
*
* @return SessionBagInterface
*/
public function getBag($name);
public function getBag(string $name): SessionBagInterface;
/**
* Gets session meta.
*
* @return MetadataBag
*/
public function getMetadataBag();
public function getMetadataBag(): MetadataBag;
}

View File

@@ -0,0 +1,59 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session;
/**
* Session utility functions.
*
* @author Nicolas Grekas <p@tchwork.com>
* @author Rémon van de Kamp <rpkamp@gmail.com>
*
* @internal
*/
final class SessionUtils
{
/**
* Finds the session header amongst the headers that are to be sent, removes it, and returns
* it so the caller can process it further.
*/
public static function popSessionCookie(string $sessionName, string $sessionId): ?string
{
$sessionCookie = null;
$sessionCookiePrefix = sprintf(' %s=', urlencode($sessionName));
$sessionCookieWithId = sprintf('%s%s;', $sessionCookiePrefix, urlencode($sessionId));
$otherCookies = [];
foreach (headers_list() as $h) {
if (0 !== stripos($h, 'Set-Cookie:')) {
continue;
}
if (11 === strpos($h, $sessionCookiePrefix, 11)) {
$sessionCookie = $h;
if (11 !== strpos($h, $sessionCookieWithId, 11)) {
$otherCookies[] = $h;
}
} else {
$otherCookies[] = $h;
}
}
if (null === $sessionCookie) {
return null;
}
header_remove('Set-Cookie');
foreach ($otherCookies as $h) {
header($h, false);
}
return $sessionCookie;
}
}

View File

@@ -0,0 +1,111 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
use Symfony\Component\HttpFoundation\Session\SessionUtils;
/**
* This abstract session handler provides a generic implementation
* of the PHP 7.0 SessionUpdateTimestampHandlerInterface,
* enabling strict and lazy session handling.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
abstract class AbstractSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface
{
private string $sessionName;
private string $prefetchId;
private string $prefetchData;
private ?string $newSessionId = null;
private string $igbinaryEmptyData;
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')));
}
return true;
}
abstract protected function doRead(string $sessionId): string;
abstract protected function doWrite(string $sessionId, string $data): bool;
abstract protected function doDestroy(string $sessionId): bool;
public function validateId(string $sessionId): bool
{
$this->prefetchData = $this->read($sessionId);
$this->prefetchId = $sessionId;
return '' !== $this->prefetchData;
}
public function read(string $sessionId): string
{
if (isset($this->prefetchId)) {
$prefetchId = $this->prefetchId;
$prefetchData = $this->prefetchData;
unset($this->prefetchId, $this->prefetchData);
if ($prefetchId === $sessionId || '' === $prefetchData) {
$this->newSessionId = '' === $prefetchData ? $sessionId : null;
return $prefetchData;
}
}
$data = $this->doRead($sessionId);
$this->newSessionId = '' === $data ? $sessionId : null;
return $data;
}
public function write(string $sessionId, string $data): bool
{
// see https://github.com/igbinary/igbinary/issues/146
$this->igbinaryEmptyData ??= \function_exists('igbinary_serialize') ? igbinary_serialize([]) : '';
if ('' === $data || $this->igbinaryEmptyData === $data) {
return $this->destroy($sessionId);
}
$this->newSessionId = null;
return $this->doWrite($sessionId, $data);
}
public function destroy(string $sessionId): bool
{
if (!headers_sent() && filter_var(\ini_get('session.use_cookies'), \FILTER_VALIDATE_BOOLEAN)) {
if (!isset($this->sessionName)) {
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);
/*
* We send an invalidation Set-Cookie header (zero lifetime)
* when either the session was started or a cookie with
* the session name was sent by the client (in which case
* we know it's invalid as a valid session cookie would've
* started the session).
*/
if (null === $cookie || isset($_COOKIE[$this->sessionName])) {
$params = session_get_cookie_params();
unset($params['lifetime']);
setcookie($this->sessionName, '', $params);
}
}
return $this->newSessionId === $sessionId || $this->doDestroy($sessionId);
}
}

View File

@@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
/**
* @author Ahmed TAILOULOUTE <ahmed.tailouloute@gmail.com>
*/
class IdentityMarshaller implements MarshallerInterface
{
/**
* {@inheritdoc}
*/
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__));
}
}
return $values;
}
/**
* {@inheritdoc}
*/
public function unmarshall(string $value): string
{
return $value;
}
}

View File

@@ -0,0 +1,76 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
/**
* @author Ahmed TAILOULOUTE <ahmed.tailouloute@gmail.com>
*/
class MarshallingSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface
{
private $handler;
private $marshaller;
public function __construct(AbstractSessionHandler $handler, MarshallerInterface $marshaller)
{
$this->handler = $handler;
$this->marshaller = $marshaller;
}
public function open(string $savePath, string $name): bool
{
return $this->handler->open($savePath, $name);
}
public function close(): bool
{
return $this->handler->close();
}
public function destroy(string $sessionId): bool
{
return $this->handler->destroy($sessionId);
}
public function gc(int $maxlifetime): int|false
{
return $this->handler->gc($maxlifetime);
}
public function read(string $sessionId): string
{
return $this->marshaller->unmarshall($this->handler->read($sessionId));
}
public function write(string $sessionId, string $data): bool
{
$failed = [];
$marshalledData = $this->marshaller->marshall(['data' => $data], $failed);
if (isset($failed['data'])) {
return false;
}
return $this->handler->write($sessionId, $marshalledData['data']);
}
public function validateId(string $sessionId): bool
{
return $this->handler->validateId($sessionId);
}
public function updateTimestamp(string $sessionId, string $data): bool
{
return $this->handler->updateTimestamp($sessionId, $data);
}
}

View File

@@ -1,119 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
/**
* MemcacheSessionHandler.
*
* @author Drak <drak@zikula.org>
*/
class MemcacheSessionHandler implements \SessionHandlerInterface
{
/**
* @var \Memcache Memcache driver
*/
private $memcache;
/**
* @var int Time to live in seconds
*/
private $ttl;
/**
* @var string Key prefix for shared environments
*/
private $prefix;
/**
* Constructor.
*
* List of available options:
* * prefix: The prefix to use for the memcache keys in order to avoid collision
* * expiretime: The time to live in seconds
*
* @param \Memcache $memcache A \Memcache instance
* @param array $options An associative array of Memcache options
*
* @throws \InvalidArgumentException When unsupported options are passed
*/
public function __construct(\Memcache $memcache, array $options = array())
{
if ($diff = array_diff(array_keys($options), array('prefix', 'expiretime'))) {
throw new \InvalidArgumentException(sprintf(
'The following options are not supported "%s"', implode(', ', $diff)
));
}
$this->memcache = $memcache;
$this->ttl = isset($options['expiretime']) ? (int) $options['expiretime'] : 86400;
$this->prefix = isset($options['prefix']) ? $options['prefix'] : 'sf2s';
}
/**
* {@inheritdoc}
*/
public function open($savePath, $sessionName)
{
return true;
}
/**
* {@inheritdoc}
*/
public function close()
{
return true;
}
/**
* {@inheritdoc}
*/
public function read($sessionId)
{
return $this->memcache->get($this->prefix.$sessionId) ?: '';
}
/**
* {@inheritdoc}
*/
public function write($sessionId, $data)
{
return $this->memcache->set($this->prefix.$sessionId, $data, 0, time() + $this->ttl);
}
/**
* {@inheritdoc}
*/
public function destroy($sessionId)
{
return $this->memcache->delete($this->prefix.$sessionId);
}
/**
* {@inheritdoc}
*/
public function gc($maxlifetime)
{
// not required here because memcache will auto expire the records anyhow.
return true;
}
/**
* Return a Memcache instance.
*
* @return \Memcache
*/
protected function getMemcache()
{
return $this->memcache;
}
}

View File

@@ -12,113 +12,109 @@
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
/**
* MemcachedSessionHandler.
*
* Memcached based session storage handler based on the Memcached class
* provided by the PHP memcached extension.
*
* @see http://php.net/memcached
* @see https://php.net/memcached
*
* @author Drak <drak@zikula.org>
*/
class MemcachedSessionHandler implements \SessionHandlerInterface
class MemcachedSessionHandler extends AbstractSessionHandler
{
/**
* @var \Memcached Memcached driver
*/
private $memcached;
/**
* @var int Time to live in seconds
* Time to live in seconds.
*/
private $ttl;
private ?int $ttl;
/**
* @var string Key prefix for shared environments
* Key prefix for shared environments.
*/
private $prefix;
private string $prefix;
/**
* Constructor.
*
* List of available options:
* * prefix: The prefix to use for the memcached keys in order to avoid collision
* * expiretime: The time to live in seconds
*
* @param \Memcached $memcached A \Memcached instance
* @param array $options An associative array of Memcached options
* * ttl: The time to live in seconds.
*
* @throws \InvalidArgumentException When unsupported options are passed
*/
public function __construct(\Memcached $memcached, array $options = array())
public function __construct(\Memcached $memcached, array $options = [])
{
$this->memcached = $memcached;
if ($diff = array_diff(array_keys($options), array('prefix', 'expiretime'))) {
throw new \InvalidArgumentException(sprintf(
'The following options are not supported "%s"', implode(', ', $diff)
));
if ($diff = array_diff(array_keys($options), ['prefix', 'expiretime', 'ttl'])) {
throw new \InvalidArgumentException(sprintf('The following options are not supported "%s".', implode(', ', $diff)));
}
$this->ttl = isset($options['expiretime']) ? (int) $options['expiretime'] : 86400;
$this->prefix = isset($options['prefix']) ? $options['prefix'] : 'sf2s';
$this->ttl = $options['expiretime'] ?? $options['ttl'] ?? null;
$this->prefix = $options['prefix'] ?? 'sf2s';
}
/**
* {@inheritdoc}
*/
public function open($savePath, $sessionName)
public function close(): bool
{
return true;
return $this->memcached->quit();
}
/**
* {@inheritdoc}
*/
public function close()
{
return true;
}
/**
* {@inheritdoc}
*/
public function read($sessionId)
protected function doRead(string $sessionId): string
{
return $this->memcached->get($this->prefix.$sessionId) ?: '';
}
/**
* {@inheritdoc}
*/
public function write($sessionId, $data)
public function updateTimestamp(string $sessionId, string $data): bool
{
return $this->memcached->set($this->prefix.$sessionId, $data, time() + $this->ttl);
}
$this->memcached->touch($this->prefix.$sessionId, $this->getCompatibleTtl());
/**
* {@inheritdoc}
*/
public function destroy($sessionId)
{
return $this->memcached->delete($this->prefix.$sessionId);
}
/**
* {@inheritdoc}
*/
public function gc($maxlifetime)
{
// not required here because memcached will auto expire the records anyhow.
return true;
}
/**
* Return a Memcached instance.
*
* @return \Memcached
* {@inheritdoc}
*/
protected function getMemcached()
protected function doWrite(string $sessionId, string $data): bool
{
return $this->memcached->set($this->prefix.$sessionId, $data, $this->getCompatibleTtl());
}
private function getCompatibleTtl(): int
{
$ttl = (int) ($this->ttl ?? \ini_get('session.gc_maxlifetime'));
// If the relative TTL that is used exceeds 30 days, memcached will treat the value as Unix time.
// We have to convert it to an absolute Unix time at this point, to make sure the TTL is correct.
if ($ttl > 60 * 60 * 24 * 30) {
$ttl += time();
}
return $ttl;
}
/**
* {@inheritdoc}
*/
protected function doDestroy(string $sessionId): bool
{
$result = $this->memcached->delete($this->prefix.$sessionId);
return $result || \Memcached::RES_NOTFOUND == $this->memcached->getResultCode();
}
public function gc(int $maxlifetime): int|false
{
// not required here because memcached will auto expire the records anyhow.
return 0;
}
/**
* Return a Memcached instance.
*/
protected function getMemcached(): \Memcached
{
return $this->memcached;
}

View File

@@ -0,0 +1,107 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
/**
* Migrating session handler for migrating from one handler to another. It reads
* from the current handler and writes both the current and new ones.
*
* It ignores errors from the new handler.
*
* @author Ross Motley <ross.motley@amara.com>
* @author Oliver Radwell <oliver.radwell@amara.com>
*/
class MigratingSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface
{
/**
* @var \SessionHandlerInterface&\SessionUpdateTimestampHandlerInterface
*/
private \SessionHandlerInterface $currentHandler;
/**
* @var \SessionHandlerInterface&\SessionUpdateTimestampHandlerInterface
*/
private \SessionHandlerInterface $writeOnlyHandler;
public function __construct(\SessionHandlerInterface $currentHandler, \SessionHandlerInterface $writeOnlyHandler)
{
if (!$currentHandler instanceof \SessionUpdateTimestampHandlerInterface) {
$currentHandler = new StrictSessionHandler($currentHandler);
}
if (!$writeOnlyHandler instanceof \SessionUpdateTimestampHandlerInterface) {
$writeOnlyHandler = new StrictSessionHandler($writeOnlyHandler);
}
$this->currentHandler = $currentHandler;
$this->writeOnlyHandler = $writeOnlyHandler;
}
public function close(): bool
{
$result = $this->currentHandler->close();
$this->writeOnlyHandler->close();
return $result;
}
public function destroy(string $sessionId): bool
{
$result = $this->currentHandler->destroy($sessionId);
$this->writeOnlyHandler->destroy($sessionId);
return $result;
}
public function gc(int $maxlifetime): int|false
{
$result = $this->currentHandler->gc($maxlifetime);
$this->writeOnlyHandler->gc($maxlifetime);
return $result;
}
public function open(string $savePath, string $sessionName): bool
{
$result = $this->currentHandler->open($savePath, $sessionName);
$this->writeOnlyHandler->open($savePath, $sessionName);
return $result;
}
public function read(string $sessionId): string
{
// No reading from new handler until switch-over
return $this->currentHandler->read($sessionId);
}
public function write(string $sessionId, string $sessionData): bool
{
$result = $this->currentHandler->write($sessionId, $sessionData);
$this->writeOnlyHandler->write($sessionId, $sessionData);
return $result;
}
public function validateId(string $sessionId): bool
{
// No reading from new handler until switch-over
return $this->currentHandler->validateId($sessionId);
}
public function updateTimestamp(string $sessionId, string $sessionData): bool
{
$result = $this->currentHandler->updateTimestamp($sessionId, $sessionData);
$this->writeOnlyHandler->updateTimestamp($sessionId, $sessionData);
return $result;
}
}

View File

@@ -11,27 +11,24 @@
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
use MongoDB\BSON\Binary;
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
use MongoDB\Collection;
/**
* MongoDB session handler.
* Session handler using the mongodb/mongodb package and MongoDB driver extension.
*
* @author Markus Bachmann <markus.bachmann@bachi.biz>
*
* @see https://packagist.org/packages/mongodb/mongodb
* @see https://php.net/mongodb
*/
class MongoDbSessionHandler implements \SessionHandlerInterface
class MongoDbSessionHandler extends AbstractSessionHandler
{
/**
* @var \Mongo|\MongoClient|\MongoDB\Client
*/
private $mongo;
/**
* @var \MongoCollection
*/
private $collection;
/**
* @var array
*/
private $options;
private array $options;
/**
* Constructor.
@@ -42,7 +39,7 @@ class MongoDbSessionHandler implements \SessionHandlerInterface
* * id_field: The field name for storing the session id [default: _id]
* * data_field: The field name for storing the session data [default: data]
* * time_field: The field name for storing the timestamp [default: time]
* * expiry_field: The field name for storing the expiry-timestamp [default: expires_at]
* * expiry_field: The field name for storing the expiry-timestamp [default: expires_at].
*
* It is strongly recommended to put an index on the `expiry_field` for
* garbage-collection. Alternatively it's possible to automatically expire
@@ -51,46 +48,35 @@ class MongoDbSessionHandler implements \SessionHandlerInterface
* A TTL collections can be used on MongoDB 2.2+ to cleanup expired sessions
* automatically. Such an index can for example look like this:
*
* db.<session-collection>.ensureIndex(
* db.<session-collection>.createIndex(
* { "<expiry-field>": 1 },
* { "expireAfterSeconds": 0 }
* )
*
* More details on: http://docs.mongodb.org/manual/tutorial/expire-data/
* More details on: https://docs.mongodb.org/manual/tutorial/expire-data/
*
* If you use such an index, you can drop `gc_probability` to 0 since
* no garbage-collection is required.
*
* @param \Mongo|\MongoClient|\MongoDB\Client $mongo A MongoDB\Client, MongoClient or Mongo instance
* @param array $options An associative array of field options
*
* @throws \InvalidArgumentException When MongoClient or Mongo instance not provided
* @throws \InvalidArgumentException When "database" or "collection" not provided
*/
public function __construct($mongo, array $options)
public function __construct(Client $mongo, array $options)
{
if (!($mongo instanceof \MongoDB\Client || $mongo instanceof \MongoClient || $mongo instanceof \Mongo)) {
throw new \InvalidArgumentException('MongoClient or Mongo instance required');
}
if (!isset($options['database']) || !isset($options['collection'])) {
throw new \InvalidArgumentException('You must provide the "database" and "collection" option for MongoDBSessionHandler');
throw new \InvalidArgumentException('You must provide the "database" and "collection" option for MongoDBSessionHandler.');
}
$this->mongo = $mongo;
$this->options = array_merge(array(
$this->options = array_merge([
'id_field' => '_id',
'data_field' => 'data',
'time_field' => 'time',
'expiry_field' => 'expires_at',
), $options);
], $options);
}
/**
* {@inheritdoc}
*/
public function open($savePath, $sessionName)
public function close(): bool
{
return true;
}
@@ -98,66 +84,54 @@ class MongoDbSessionHandler implements \SessionHandlerInterface
/**
* {@inheritdoc}
*/
public function close()
protected function doDestroy(string $sessionId): bool
{
return true;
}
/**
* {@inheritdoc}
*/
public function destroy($sessionId)
{
$methodName = $this->mongo instanceof \MongoDB\Client ? 'deleteOne' : 'remove';
$this->getCollection()->$methodName(array(
$this->getCollection()->deleteOne([
$this->options['id_field'] => $sessionId,
));
]);
return true;
}
public function gc(int $maxlifetime): int|false
{
return $this->getCollection()->deleteMany([
$this->options['expiry_field'] => ['$lt' => new UTCDateTime()],
])->getDeletedCount();
}
/**
* {@inheritdoc}
*/
public function gc($maxlifetime)
protected function doWrite(string $sessionId, string $data): bool
{
$methodName = $this->mongo instanceof \MongoDB\Client ? 'deleteOne' : 'remove';
$expiry = new UTCDateTime((time() + (int) \ini_get('session.gc_maxlifetime')) * 1000);
$this->getCollection()->$methodName(array(
$this->options['expiry_field'] => array('$lt' => $this->createDateTime()),
));
return true;
}
/**
* {@inheritdoc}
*/
public function write($sessionId, $data)
{
$expiry = $this->createDateTime(time() + (int) ini_get('session.gc_maxlifetime'));
$fields = array(
$this->options['time_field'] => $this->createDateTime(),
$fields = [
$this->options['time_field'] => new UTCDateTime(),
$this->options['expiry_field'] => $expiry,
$this->options['data_field'] => new Binary($data, Binary::TYPE_OLD_BINARY),
];
$this->getCollection()->updateOne(
[$this->options['id_field'] => $sessionId],
['$set' => $fields],
['upsert' => true]
);
$options = array('upsert' => true);
return true;
}
if ($this->mongo instanceof \MongoDB\Client) {
$fields[$this->options['data_field']] = new \MongoDB\BSON\Binary($data, \MongoDB\BSON\Binary::TYPE_OLD_BINARY);
} else {
$fields[$this->options['data_field']] = new \MongoBinData($data, \MongoBinData::BYTE_ARRAY);
$options['multiple'] = false;
}
public function updateTimestamp(string $sessionId, string $data): bool
{
$expiry = new UTCDateTime((time() + (int) \ini_get('session.gc_maxlifetime')) * 1000);
$methodName = $this->mongo instanceof \MongoDB\Client ? 'updateOne' : 'update';
$this->getCollection()->$methodName(
array($this->options['id_field'] => $sessionId),
array('$set' => $fields),
$options
$this->getCollection()->updateOne(
[$this->options['id_field'] => $sessionId],
['$set' => [
$this->options['time_field'] => new UTCDateTime(),
$this->options['expiry_field'] => $expiry,
]]
);
return true;
@@ -166,67 +140,27 @@ class MongoDbSessionHandler implements \SessionHandlerInterface
/**
* {@inheritdoc}
*/
public function read($sessionId)
protected function doRead(string $sessionId): string
{
$dbData = $this->getCollection()->findOne(array(
$dbData = $this->getCollection()->findOne([
$this->options['id_field'] => $sessionId,
$this->options['expiry_field'] => array('$gte' => $this->createDateTime()),
));
$this->options['expiry_field'] => ['$gte' => new UTCDateTime()],
]);
if (null === $dbData) {
return '';
}
if ($dbData[$this->options['data_field']] instanceof \MongoDB\BSON\Binary) {
return $dbData[$this->options['data_field']]->getData();
}
return $dbData[$this->options['data_field']]->bin;
return $dbData[$this->options['data_field']]->getData();
}
/**
* Return a "MongoCollection" instance.
*
* @return \MongoCollection
*/
private function getCollection()
private function getCollection(): Collection
{
if (null === $this->collection) {
$this->collection = $this->mongo->selectCollection($this->options['database'], $this->options['collection']);
}
return $this->collection;
return $this->collection ??= $this->mongo->selectCollection($this->options['database'], $this->options['collection']);
}
/**
* Return a Mongo instance.
*
* @return \Mongo|\MongoClient|\MongoDB\Client
*/
protected function getMongo()
protected function getMongo(): Client
{
return $this->mongo;
}
/**
* Create a date object using the class appropriate for the current mongo connection.
*
* Return an instance of a MongoDate or \MongoDB\BSON\UTCDateTime
*
* @param int $seconds An integer representing UTC seconds since Jan 1 1970. Defaults to now.
*
* @return \MongoDate|\MongoDB\BSON\UTCDateTime
*/
private function createDateTime($seconds = null)
{
if (null === $seconds) {
$seconds = time();
}
if ($this->mongo instanceof \MongoDB\Client) {
return new \MongoDB\BSON\UTCDateTime($seconds * 1000);
}
return new \MongoDate($seconds);
}
}

View File

@@ -12,36 +12,33 @@
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
/**
* NativeFileSessionHandler.
*
* Native session handler using PHP's built in file storage.
*
* @author Drak <drak@zikula.org>
*/
class NativeFileSessionHandler extends NativeSessionHandler
class NativeFileSessionHandler extends \SessionHandler
{
/**
* Constructor.
*
* @param string $savePath Path of directory to save session files
* Default null will leave setting as defined by PHP.
* '/path', 'N;/path', or 'N;octal-mode;/path
*
* @see http://php.net/session.configuration.php#ini.session.save-path for further details.
* @see https://php.net/session.configuration#ini.session.save-path for further details.
*
* @throws \InvalidArgumentException On invalid $savePath
* @throws \RuntimeException When failing to create the save directory
*/
public function __construct($savePath = null)
public function __construct(string $savePath = null)
{
if (null === $savePath) {
$savePath = ini_get('session.save_path');
$savePath = \ini_get('session.save_path');
}
$baseDir = $savePath;
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
@@ -49,7 +46,7 @@ class NativeFileSessionHandler extends NativeSessionHandler
}
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));
}
ini_set('session.save_path', $savePath);

View File

@@ -12,18 +12,18 @@
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
/**
* NullSessionHandler.
*
* Can be used in unit testing or in a situations where persisted sessions are not desired.
*
* @author Drak <drak@zikula.org>
*/
class NullSessionHandler implements \SessionHandlerInterface
class NullSessionHandler extends AbstractSessionHandler
{
/**
* {@inheritdoc}
*/
public function open($savePath, $sessionName)
public function close(): bool
{
return true;
}
public function validateId(string $sessionId): bool
{
return true;
}
@@ -31,23 +31,12 @@ class NullSessionHandler implements \SessionHandlerInterface
/**
* {@inheritdoc}
*/
public function close()
{
return true;
}
/**
* {@inheritdoc}
*/
public function read($sessionId)
protected function doRead(string $sessionId): string
{
return '';
}
/**
* {@inheritdoc}
*/
public function write($sessionId, $data)
public function updateTimestamp(string $sessionId, string $data): bool
{
return true;
}
@@ -55,7 +44,7 @@ class NullSessionHandler implements \SessionHandlerInterface
/**
* {@inheritdoc}
*/
public function destroy($sessionId)
protected function doWrite(string $sessionId, string $data): bool
{
return true;
}
@@ -63,8 +52,13 @@ class NullSessionHandler implements \SessionHandlerInterface
/**
* {@inheritdoc}
*/
public function gc($maxlifetime)
protected function doDestroy(string $sessionId): bool
{
return true;
}
public function gc(int $maxlifetime): int|false
{
return 0;
}
}

View File

@@ -32,13 +32,13 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
* Saving it in a character column could corrupt the data. You can use createTable()
* to initialize a correctly defined table.
*
* @see http://php.net/sessionhandlerinterface
* @see https://php.net/sessionhandlerinterface
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Michael Williams <michael.williams@funsational.com>
* @author Tobias Schultze <http://tobion.de>
*/
class PdoSessionHandler implements \SessionHandlerInterface
class PdoSessionHandler extends AbstractSessionHandler
{
/**
* No locking is done. This means sessions are prone to loss of data due to
@@ -46,7 +46,7 @@ class PdoSessionHandler implements \SessionHandlerInterface
* write will win in this case. It might be useful when you implement your own
* logic to deal with this like an optimistic approach.
*/
const LOCK_NONE = 0;
public const LOCK_NONE = 0;
/**
* Creates an application-level lock on a session. The disadvantage is that the
@@ -55,7 +55,7 @@ class PdoSessionHandler implements \SessionHandlerInterface
* does not require a transaction.
* This mode is not available for SQLite and not yet implemented for oci and sqlsrv.
*/
const LOCK_ADVISORY = 1;
public const LOCK_ADVISORY = 1;
/**
* Issues a real row lock. Since it uses a transaction between opening and
@@ -63,93 +63,65 @@ class PdoSessionHandler implements \SessionHandlerInterface
* that you also use for your application logic. This mode is the default because
* it's the only reliable solution across DBMSs.
*/
const LOCK_TRANSACTIONAL = 2;
public const LOCK_TRANSACTIONAL = 2;
/**
* @var \PDO|null PDO instance or null when not connected yet
*/
private $pdo;
/**
* @var string|null|false DSN string or null for session.save_path or false when lazy connection disabled
* DSN string or null for session.save_path or false when lazy connection disabled.
*/
private $dsn = false;
private string|false|null $dsn = false;
private string $driver;
private string $table = 'sessions';
private string $idCol = 'sess_id';
private string $dataCol = 'sess_data';
private string $lifetimeCol = 'sess_lifetime';
private string $timeCol = 'sess_time';
/**
* @var string Database driver
* Username when lazy-connect.
*/
private $driver;
private string $username = '';
/**
* @var string Table name
* Password when lazy-connect.
*/
private $table = 'sessions';
private string $password = '';
/**
* @var string Column for session id
* Connection options when lazy-connect.
*/
private $idCol = 'sess_id';
private array $connectionOptions = [];
/**
* @var string Column for session data
* The strategy for locking, see constants.
*/
private $dataCol = 'sess_data';
/**
* @var string Column for lifetime
*/
private $lifetimeCol = 'sess_lifetime';
/**
* @var string Column for timestamp
*/
private $timeCol = 'sess_time';
/**
* @var string Username when lazy-connect
*/
private $username = '';
/**
* @var string Password when lazy-connect
*/
private $password = '';
/**
* @var array Connection options when lazy-connect
*/
private $connectionOptions = array();
/**
* @var int The strategy for locking, see constants
*/
private $lockMode = self::LOCK_TRANSACTIONAL;
private int $lockMode = self::LOCK_TRANSACTIONAL;
/**
* It's an array to support multiple reads before closing which is manual, non-standard usage.
*
* @var \PDOStatement[] An array of statements to release advisory locks
*/
private $unlockStatements = array();
private array $unlockStatements = [];
/**
* @var bool True when the current session exists but expired according to session.gc_maxlifetime
* True when the current session exists but expired according to session.gc_maxlifetime.
*/
private $sessionExpired = false;
private bool $sessionExpired = false;
/**
* @var bool Whether a transaction is active
* Whether a transaction is active.
*/
private $inTransaction = false;
private bool $inTransaction = false;
/**
* @var bool Whether gc() has been called
* Whether gc() has been called.
*/
private $gcCalled = false;
private bool $gcCalled = false;
/**
* Constructor.
*
* You can either pass an existing database connection as PDO instance or
* pass a DSN string that will be used to lazy-connect to the database
* when the session is actually used. Furthermore it's possible to pass null
@@ -163,36 +135,37 @@ class PdoSessionHandler implements \SessionHandlerInterface
* * db_time_col: The column where to store the timestamp [default: sess_time]
* * db_username: The username when lazy-connect [default: '']
* * db_password: The password when lazy-connect [default: '']
* * db_connection_options: An array of driver-specific connection options [default: array()]
* * db_connection_options: An array of driver-specific connection options [default: []]
* * lock_mode: The strategy for locking, see constants [default: LOCK_TRANSACTIONAL]
*
* @param \PDO|string|null $pdoOrDsn A \PDO instance or DSN string or null
* @param array $options An associative array of options
* @param \PDO|string|null $pdoOrDsn A \PDO instance or DSN string or URL string or null
*
* @throws \InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION
*/
public function __construct($pdoOrDsn = null, array $options = array())
public function __construct(\PDO|string $pdoOrDsn = null, array $options = [])
{
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;
$this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
} elseif (\is_string($pdoOrDsn) && str_contains($pdoOrDsn, '://')) {
$this->dsn = $this->buildDsnFromUrl($pdoOrDsn);
} else {
$this->dsn = $pdoOrDsn;
}
$this->table = isset($options['db_table']) ? $options['db_table'] : $this->table;
$this->idCol = isset($options['db_id_col']) ? $options['db_id_col'] : $this->idCol;
$this->dataCol = isset($options['db_data_col']) ? $options['db_data_col'] : $this->dataCol;
$this->lifetimeCol = isset($options['db_lifetime_col']) ? $options['db_lifetime_col'] : $this->lifetimeCol;
$this->timeCol = isset($options['db_time_col']) ? $options['db_time_col'] : $this->timeCol;
$this->username = isset($options['db_username']) ? $options['db_username'] : $this->username;
$this->password = isset($options['db_password']) ? $options['db_password'] : $this->password;
$this->connectionOptions = isset($options['db_connection_options']) ? $options['db_connection_options'] : $this->connectionOptions;
$this->lockMode = isset($options['lock_mode']) ? $options['lock_mode'] : $this->lockMode;
$this->table = $options['db_table'] ?? $this->table;
$this->idCol = $options['db_id_col'] ?? $this->idCol;
$this->dataCol = $options['db_data_col'] ?? $this->dataCol;
$this->lifetimeCol = $options['db_lifetime_col'] ?? $this->lifetimeCol;
$this->timeCol = $options['db_time_col'] ?? $this->timeCol;
$this->username = $options['db_username'] ?? $this->username;
$this->password = $options['db_password'] ?? $this->password;
$this->connectionOptions = $options['db_connection_options'] ?? $this->connectionOptions;
$this->lockMode = $options['lock_mode'] ?? $this->lockMode;
}
/**
@@ -218,7 +191,7 @@ class PdoSessionHandler implements \SessionHandlerInterface
// - trailing space removal
// - case-insensitivity
// - language processing like é == e
$sql = "CREATE TABLE $this->table ($this->idCol VARBINARY(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol MEDIUMINT NOT NULL, $this->timeCol INTEGER UNSIGNED NOT NULL) COLLATE utf8_bin, ENGINE = InnoDB";
$sql = "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";
break;
case 'sqlite':
$sql = "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)";
@@ -238,6 +211,7 @@ class PdoSessionHandler implements \SessionHandlerInterface
try {
$this->pdo->exec($sql);
$this->pdo->exec("CREATE INDEX EXPIRY ON $this->table ($this->lifetimeCol)");
} catch (\PDOException $e) {
$this->rollback();
@@ -249,33 +223,27 @@ class PdoSessionHandler implements \SessionHandlerInterface
* Returns true when the current session exists but expired according to session.gc_maxlifetime.
*
* Can be used to distinguish between a new session and one that expired due to inactivity.
*
* @return bool Whether current session expired
*/
public function isSessionExpired()
public function isSessionExpired(): bool
{
return $this->sessionExpired;
}
/**
* {@inheritdoc}
*/
public function open($savePath, $sessionName)
public function open(string $savePath, string $sessionName): bool
{
if (null === $this->pdo) {
$this->sessionExpired = false;
if (!isset($this->pdo)) {
$this->connect($this->dsn ?: $savePath);
}
return true;
return parent::open($savePath, $sessionName);
}
/**
* {@inheritdoc}
*/
public function read($sessionId)
public function read(string $sessionId): string
{
try {
return $this->doRead($sessionId);
return parent::read($sessionId);
} catch (\PDOException $e) {
$this->rollback();
@@ -283,22 +251,19 @@ class PdoSessionHandler implements \SessionHandlerInterface
}
}
/**
* {@inheritdoc}
*/
public function gc($maxlifetime)
public function gc(int $maxlifetime): int|false
{
// We delay gc() to close() so that it is executed outside the transactional and blocking read-write process.
// This way, pruning expired sessions does not block them from being started while the current session is used.
$this->gcCalled = true;
return true;
return 0;
}
/**
* {@inheritdoc}
*/
public function destroy($sessionId)
protected function doDestroy(string $sessionId): bool
{
// delete the record associated with this id
$sql = "DELETE FROM $this->table WHERE $this->idCol = :id";
@@ -319,9 +284,9 @@ class PdoSessionHandler implements \SessionHandlerInterface
/**
* {@inheritdoc}
*/
public function write($sessionId, $data)
protected function doWrite(string $sessionId, string $data): bool
{
$maxlifetime = (int) ini_get('session.gc_maxlifetime');
$maxlifetime = (int) \ini_get('session.gc_maxlifetime');
try {
// We use a single MERGE SQL query when supported by the database.
@@ -332,13 +297,7 @@ class PdoSessionHandler implements \SessionHandlerInterface
return true;
}
$updateStmt = $this->pdo->prepare(
"UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time WHERE $this->idCol = :id"
);
$updateStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
$updateStmt->bindParam(':data', $data, \PDO::PARAM_LOB);
$updateStmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT);
$updateStmt->bindValue(':time', time(), \PDO::PARAM_INT);
$updateStmt = $this->getUpdateStatement($sessionId, $data, $maxlifetime);
$updateStmt->execute();
// When MERGE is not supported, like in Postgres < 9.5, we have to use this approach that can result in
@@ -348,17 +307,11 @@ class PdoSessionHandler implements \SessionHandlerInterface
// false positives due to longer gap locking.
if (!$updateStmt->rowCount()) {
try {
$insertStmt = $this->pdo->prepare(
"INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)"
);
$insertStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
$insertStmt->bindParam(':data', $data, \PDO::PARAM_LOB);
$insertStmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT);
$insertStmt->bindValue(':time', time(), \PDO::PARAM_INT);
$insertStmt = $this->getInsertStatement($sessionId, $data, $maxlifetime);
$insertStmt->execute();
} catch (\PDOException $e) {
// Handle integrity violation SQLSTATE 23000 (or a subclass like 23505 in Postgres) for duplicate keys
if (0 === strpos($e->getCode(), '23')) {
if (str_starts_with($e->getCode(), '23')) {
$updateStmt->execute();
} else {
throw $e;
@@ -374,10 +327,28 @@ class PdoSessionHandler implements \SessionHandlerInterface
return true;
}
/**
* {@inheritdoc}
*/
public function close()
public function updateTimestamp(string $sessionId, string $data): bool
{
$expiry = time() + (int) \ini_get('session.gc_maxlifetime');
try {
$updateStmt = $this->pdo->prepare(
"UPDATE $this->table SET $this->lifetimeCol = :expiry, $this->timeCol = :time WHERE $this->idCol = :id"
);
$updateStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
$updateStmt->bindParam(':expiry', $expiry, \PDO::PARAM_INT);
$updateStmt->bindValue(':time', time(), \PDO::PARAM_INT);
$updateStmt->execute();
} catch (\PDOException $e) {
$this->rollback();
throw $e;
}
return true;
}
public function close(): bool
{
$this->commit();
@@ -389,15 +360,14 @@ class PdoSessionHandler implements \SessionHandlerInterface
$this->gcCalled = false;
// delete the session records that have expired
$sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol < :time";
$sql = "DELETE FROM $this->table WHERE $this->lifetimeCol < :time";
$stmt = $this->pdo->prepare($sql);
$stmt->bindValue(':time', time(), \PDO::PARAM_INT);
$stmt->execute();
}
if (false !== $this->dsn) {
$this->pdo = null; // only close lazy-connection
unset($this->pdo, $this->driver); // only close lazy-connection
}
return true;
@@ -405,16 +375,128 @@ class PdoSessionHandler implements \SessionHandlerInterface
/**
* Lazy-connects to the database.
*
* @param string $dsn DSN string
*/
private function connect($dsn)
private function connect(string $dsn): void
{
$this->pdo = new \PDO($dsn, $this->username, $this->password, $this->connectionOptions);
$this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
}
/**
* Builds a PDO DSN from a URL-like connection string.
*
* @todo implement missing support for oci DSN (which look totally different from other PDO ones)
*/
private function buildDsnFromUrl(string $dsnOrUrl): string
{
// (pdo_)?sqlite3?:///... => (pdo_)?sqlite3?://localhost/... or else the URL will be invalid
$url = preg_replace('#^((?:pdo_)?sqlite3?):///#', '$1://localhost/', $dsnOrUrl);
$params = parse_url($url);
if (false === $params) {
return $dsnOrUrl; // If the URL is not valid, let's assume it might be a DSN already.
}
$params = array_map('rawurldecode', $params);
// Override the default username and password. Values passed through options will still win over these in the constructor.
if (isset($params['user'])) {
$this->username = $params['user'];
}
if (isset($params['pass'])) {
$this->password = $params['pass'];
}
if (!isset($params['scheme'])) {
throw new \InvalidArgumentException('URLs without scheme are not supported to configure the PdoSessionHandler.');
}
$driverAliasMap = [
'mssql' => 'sqlsrv',
'mysql2' => 'mysql', // Amazon RDS, for some weird reason
'postgres' => 'pgsql',
'postgresql' => 'pgsql',
'sqlite3' => 'sqlite',
];
$driver = $driverAliasMap[$params['scheme']] ?? $params['scheme'];
// Doctrine DBAL supports passing its internal pdo_* driver names directly too (allowing both dashes and underscores). This allows supporting the same here.
if (str_starts_with($driver, 'pdo_') || str_starts_with($driver, 'pdo-')) {
$driver = substr($driver, 4);
}
$dsn = null;
switch ($driver) {
case 'mysql':
$dsn = 'mysql:';
if ('' !== ($params['query'] ?? '')) {
$queryParams = [];
parse_str($params['query'], $queryParams);
if ('' !== ($queryParams['charset'] ?? '')) {
$dsn .= 'charset='.$queryParams['charset'].';';
}
if ('' !== ($queryParams['unix_socket'] ?? '')) {
$dsn .= 'unix_socket='.$queryParams['unix_socket'].';';
if (isset($params['path'])) {
$dbName = substr($params['path'], 1); // Remove the leading slash
$dsn .= 'dbname='.$dbName.';';
}
return $dsn;
}
}
// If "unix_socket" is not in the query, we continue with the same process as pgsql
// no break
case 'pgsql':
$dsn ?? $dsn = 'pgsql:';
if (isset($params['host']) && '' !== $params['host']) {
$dsn .= 'host='.$params['host'].';';
}
if (isset($params['port']) && '' !== $params['port']) {
$dsn .= 'port='.$params['port'].';';
}
if (isset($params['path'])) {
$dbName = substr($params['path'], 1); // Remove the leading slash
$dsn .= 'dbname='.$dbName.';';
}
return $dsn;
case 'sqlite':
return 'sqlite:'.substr($params['path'], 1);
case 'sqlsrv':
$dsn = 'sqlsrv:server=';
if (isset($params['host'])) {
$dsn .= $params['host'];
}
if (isset($params['port']) && '' !== $params['port']) {
$dsn .= ','.$params['port'];
}
if (isset($params['path'])) {
$dbName = substr($params['path'], 1); // Remove the leading slash
$dsn .= ';Database='.$dbName;
}
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']));
}
}
/**
* Helper method to begin a transaction.
*
@@ -424,10 +506,10 @@ class PdoSessionHandler implements \SessionHandlerInterface
* PDO::rollback or PDO::inTransaction for SQLite.
*
* Also MySQLs default isolation, REPEATABLE READ, causes deadlock for different sessions
* due to http://www.mysqlperformanceblog.com/2013/12/12/one-more-innodb-gap-lock-to-avoid/ .
* due to https://percona.com/blog/2013/12/12/one-more-innodb-gap-lock-to-avoid/ .
* So we change it to READ COMMITTED.
*/
private function beginTransaction()
private function beginTransaction(): void
{
if (!$this->inTransaction) {
if ('sqlite' === $this->driver) {
@@ -445,7 +527,7 @@ class PdoSessionHandler implements \SessionHandlerInterface
/**
* Helper method to commit a transaction.
*/
private function commit()
private function commit(): void
{
if ($this->inTransaction) {
try {
@@ -467,7 +549,7 @@ class PdoSessionHandler implements \SessionHandlerInterface
/**
* Helper method to rollback a transaction.
*/
private function rollback()
private function rollback(): void
{
// We only need to rollback if we are in a transaction. Otherwise the resulting
// error would hide the real problem why rollback was called. We might not be
@@ -488,15 +570,9 @@ class PdoSessionHandler implements \SessionHandlerInterface
*
* We need to make sure we do not return session data that is already considered garbage according
* to the session.gc_maxlifetime setting because gc() is called after read() and only sometimes.
*
* @param string $sessionId Session ID
*
* @return string The session data
*/
private function doRead($sessionId)
protected function doRead(string $sessionId): string
{
$this->sessionExpired = false;
if (self::LOCK_ADVISORY === $this->lockMode) {
$this->unlockStatements[] = $this->doAdvisoryLock($sessionId);
}
@@ -504,37 +580,41 @@ class PdoSessionHandler implements \SessionHandlerInterface
$selectSql = $this->getSelectSql();
$selectStmt = $this->pdo->prepare($selectSql);
$selectStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
$insertStmt = null;
do {
while (true) {
$selectStmt->execute();
$sessionRows = $selectStmt->fetchAll(\PDO::FETCH_NUM);
if ($sessionRows) {
if ($sessionRows[0][1] + $sessionRows[0][2] < time()) {
$expiry = (int) $sessionRows[0][1];
if ($expiry < time()) {
$this->sessionExpired = true;
return '';
}
return is_resource($sessionRows[0][0]) ? stream_get_contents($sessionRows[0][0]) : $sessionRows[0][0];
return \is_resource($sessionRows[0][0]) ? stream_get_contents($sessionRows[0][0]) : $sessionRows[0][0];
}
if (self::LOCK_TRANSACTIONAL === $this->lockMode && 'sqlite' !== $this->driver) {
if (null !== $insertStmt) {
$this->rollback();
throw new \RuntimeException('Failed to read session: INSERT reported a duplicate id but next SELECT did not return any data.');
}
if (!filter_var(\ini_get('session.use_strict_mode'), \FILTER_VALIDATE_BOOLEAN) && self::LOCK_TRANSACTIONAL === $this->lockMode && 'sqlite' !== $this->driver) {
// In strict mode, session fixation is not possible: new sessions always start with a unique
// random id, so that concurrency is not possible and this code path can be skipped.
// Exclusive-reading of non-existent rows does not block, so we need to do an insert to block
// until other connections to the session are committed.
try {
$insertStmt = $this->pdo->prepare(
"INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)"
);
$insertStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
$insertStmt->bindValue(':data', '', \PDO::PARAM_LOB);
$insertStmt->bindValue(':lifetime', 0, \PDO::PARAM_INT);
$insertStmt->bindValue(':time', time(), \PDO::PARAM_INT);
$insertStmt = $this->getInsertStatement($sessionId, '', 0);
$insertStmt->execute();
} catch (\PDOException $e) {
// Catch duplicate key error because other connection created the session already.
// It would only not be the case when the other connection destroyed the session.
if (0 === strpos($e->getCode(), '23')) {
if (str_starts_with($e->getCode(), '23')) {
// Retrieve finished session data written by concurrent connection by restarting the loop.
// We have to start a new transaction as a failed query will mark the current transaction as
// aborted in PostgreSQL and disallow further queries within it.
@@ -548,14 +628,12 @@ class PdoSessionHandler implements \SessionHandlerInterface
}
return '';
} while (true);
}
}
/**
* Executes an application-level lock on the database.
*
* @param string $sessionId Session ID
*
* @return \PDOStatement The statement that needs to be executed later to release the lock
*
* @throws \DomainException When an unsupported PDO driver is used
@@ -564,27 +642,29 @@ class PdoSessionHandler implements \SessionHandlerInterface
* - for oci using DBMS_LOCK.REQUEST
* - for sqlsrv using sp_getapplock with LockOwner = Session
*/
private function doAdvisoryLock($sessionId)
private function doAdvisoryLock(string $sessionId): \PDOStatement
{
switch ($this->driver) {
case 'mysql':
// MySQL 5.7.5 and later enforces a maximum length on lock names of 64 characters. Previously, no limit was enforced.
$lockId = substr($sessionId, 0, 64);
// should we handle the return value? 0 on timeout, null on error
// we use a timeout of 50 seconds which is also the default for innodb_lock_wait_timeout
$stmt = $this->pdo->prepare('SELECT GET_LOCK(:key, 50)');
$stmt->bindValue(':key', $sessionId, \PDO::PARAM_STR);
$stmt->bindValue(':key', $lockId, \PDO::PARAM_STR);
$stmt->execute();
$releaseStmt = $this->pdo->prepare('DO RELEASE_LOCK(:key)');
$releaseStmt->bindValue(':key', $sessionId, \PDO::PARAM_STR);
$releaseStmt->bindValue(':key', $lockId, \PDO::PARAM_STR);
return $releaseStmt;
case 'pgsql':
// Obtaining an exclusive session level advisory lock requires an integer key.
// So we convert the HEX representation of the session id to an integer.
// Since integers are signed, we have to skip one hex char to fit in the range.
if (4 === PHP_INT_SIZE) {
$sessionInt1 = hexdec(substr($sessionId, 0, 7));
$sessionInt2 = hexdec(substr($sessionId, 7, 7));
// When session.sid_bits_per_character > 4, the session id can contain non-hex-characters.
// So we cannot just use hexdec().
if (4 === \PHP_INT_SIZE) {
$sessionInt1 = $this->convertStringToInt($sessionId);
$sessionInt2 = $this->convertStringToInt(substr($sessionId, 4, 4));
$stmt = $this->pdo->prepare('SELECT pg_advisory_lock(:key1, :key2)');
$stmt->bindValue(':key1', $sessionInt1, \PDO::PARAM_INT);
@@ -595,7 +675,7 @@ class PdoSessionHandler implements \SessionHandlerInterface
$releaseStmt->bindValue(':key1', $sessionInt1, \PDO::PARAM_INT);
$releaseStmt->bindValue(':key2', $sessionInt2, \PDO::PARAM_INT);
} else {
$sessionBigInt = hexdec(substr($sessionId, 0, 15));
$sessionBigInt = $this->convertStringToInt($sessionId);
$stmt = $this->pdo->prepare('SELECT pg_advisory_lock(:key)');
$stmt->bindValue(':key', $sessionBigInt, \PDO::PARAM_INT);
@@ -614,13 +694,28 @@ class PdoSessionHandler implements \SessionHandlerInterface
}
/**
* Return a locking or nonlocking SQL query to read session information.
* Encodes the first 4 (when PHP_INT_SIZE == 4) or 8 characters of the string as an integer.
*
* @return string The SQL string
* Keep in mind, PHP integers are signed.
*/
private function convertStringToInt(string $string): int
{
if (4 === \PHP_INT_SIZE) {
return (\ord($string[3]) << 24) + (\ord($string[2]) << 16) + (\ord($string[1]) << 8) + \ord($string[0]);
}
$int1 = (\ord($string[7]) << 24) + (\ord($string[6]) << 16) + (\ord($string[5]) << 8) + \ord($string[4]);
$int2 = (\ord($string[3]) << 24) + (\ord($string[2]) << 16) + (\ord($string[1]) << 8) + \ord($string[0]);
return $int2 + ($int1 << 32);
}
/**
* Return a locking or nonlocking SQL query to read session information.
*
* @throws \DomainException When an unsupported PDO driver is used
*/
private function getSelectSql()
private function getSelectSql(): string
{
if (self::LOCK_TRANSACTIONAL === $this->lockMode) {
$this->beginTransaction();
@@ -629,9 +724,9 @@ class PdoSessionHandler implements \SessionHandlerInterface
case 'mysql':
case 'oci':
case 'pgsql':
return "SELECT $this->dataCol, $this->lifetimeCol, $this->timeCol FROM $this->table WHERE $this->idCol = :id FOR UPDATE";
return "SELECT $this->dataCol, $this->lifetimeCol FROM $this->table WHERE $this->idCol = :id FOR UPDATE";
case 'sqlsrv':
return "SELECT $this->dataCol, $this->lifetimeCol, $this->timeCol FROM $this->table WITH (UPDLOCK, ROWLOCK) WHERE $this->idCol = :id";
return "SELECT $this->dataCol, $this->lifetimeCol FROM $this->table WITH (UPDLOCK, ROWLOCK) WHERE $this->idCol = :id";
case 'sqlite':
// we already locked when starting transaction
break;
@@ -640,80 +735,120 @@ class PdoSessionHandler implements \SessionHandlerInterface
}
}
return "SELECT $this->dataCol, $this->lifetimeCol, $this->timeCol FROM $this->table WHERE $this->idCol = :id";
return "SELECT $this->dataCol, $this->lifetimeCol FROM $this->table WHERE $this->idCol = :id";
}
/**
* Returns an insert statement supported by the database for writing session data.
*/
private function getInsertStatement(string $sessionId, string $sessionData, int $maxlifetime): \PDOStatement
{
switch ($this->driver) {
case 'oci':
$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, EMPTY_BLOB(), :expiry, :time) RETURNING $this->dataCol into :data";
break;
default:
$data = $sessionData;
$sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :expiry, :time)";
break;
}
$stmt = $this->pdo->prepare($sql);
$stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
$stmt->bindParam(':data', $data, \PDO::PARAM_LOB);
$stmt->bindValue(':expiry', time() + $maxlifetime, \PDO::PARAM_INT);
$stmt->bindValue(':time', time(), \PDO::PARAM_INT);
return $stmt;
}
/**
* Returns an update statement supported by the database for writing session data.
*/
private function getUpdateStatement(string $sessionId, string $sessionData, int $maxlifetime): \PDOStatement
{
switch ($this->driver) {
case 'oci':
$data = fopen('php://memory', 'r+');
fwrite($data, $sessionData);
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;
default:
$data = $sessionData;
$sql = "UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :expiry, $this->timeCol = :time WHERE $this->idCol = :id";
break;
}
$stmt = $this->pdo->prepare($sql);
$stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
$stmt->bindParam(':data', $data, \PDO::PARAM_LOB);
$stmt->bindValue(':expiry', time() + $maxlifetime, \PDO::PARAM_INT);
$stmt->bindValue(':time', time(), \PDO::PARAM_INT);
return $stmt;
}
/**
* Returns a merge/upsert (i.e. insert or update) statement when supported by the database for writing session data.
*
* @param string $sessionId Session ID
* @param string $data Encoded session data
* @param int $maxlifetime session.gc_maxlifetime
*
* @return \PDOStatement|null The merge statement or null when not supported
*/
private function getMergeStatement($sessionId, $data, $maxlifetime)
private function getMergeStatement(string $sessionId, string $data, int $maxlifetime): ?\PDOStatement
{
$mergeSql = null;
switch (true) {
case 'mysql' === $this->driver:
$mergeSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ".
$mergeSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :expiry, :time) ".
"ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)";
break;
case 'oci' === $this->driver:
// DUAL is Oracle specific dummy table
$mergeSql = "MERGE INTO $this->table USING DUAL ON ($this->idCol = ?) ".
"WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ".
"WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?";
break;
case 'sqlsrv' === $this->driver && version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '10', '>='):
// MERGE is only available since SQL Server 2008 and must be terminated by semicolon
// It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
// It also requires HOLDLOCK according to https://weblogs.sqlteam.com/dang/2009/01/31/upsert-race-condition-with-merge/
$mergeSql = "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = ?) ".
"WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ".
"WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?;";
break;
case 'sqlite' === $this->driver:
$mergeSql = "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)";
$mergeSql = "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :expiry, :time)";
break;
case 'pgsql' === $this->driver && version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '9.5', '>='):
$mergeSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ".
$mergeSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :expiry, :time) ".
"ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->lifetimeCol, $this->timeCol) = (EXCLUDED.$this->dataCol, EXCLUDED.$this->lifetimeCol, EXCLUDED.$this->timeCol)";
break;
default:
// MERGE is not supported with LOBs: https://oracle.com/technetwork/articles/fuecks-lobs-095315.html
return null;
}
if (null !== $mergeSql) {
$mergeStmt = $this->pdo->prepare($mergeSql);
$mergeStmt = $this->pdo->prepare($mergeSql);
if ('sqlsrv' === $this->driver || 'oci' === $this->driver) {
$mergeStmt->bindParam(1, $sessionId, \PDO::PARAM_STR);
$mergeStmt->bindParam(2, $sessionId, \PDO::PARAM_STR);
$mergeStmt->bindParam(3, $data, \PDO::PARAM_LOB);
$mergeStmt->bindParam(4, $maxlifetime, \PDO::PARAM_INT);
$mergeStmt->bindValue(5, time(), \PDO::PARAM_INT);
$mergeStmt->bindParam(6, $data, \PDO::PARAM_LOB);
$mergeStmt->bindParam(7, $maxlifetime, \PDO::PARAM_INT);
$mergeStmt->bindValue(8, time(), \PDO::PARAM_INT);
} else {
$mergeStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
$mergeStmt->bindParam(':data', $data, \PDO::PARAM_LOB);
$mergeStmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT);
$mergeStmt->bindValue(':time', time(), \PDO::PARAM_INT);
}
return $mergeStmt;
if ('sqlsrv' === $this->driver) {
$mergeStmt->bindParam(1, $sessionId, \PDO::PARAM_STR);
$mergeStmt->bindParam(2, $sessionId, \PDO::PARAM_STR);
$mergeStmt->bindParam(3, $data, \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->bindValue(7, time() + $maxlifetime, \PDO::PARAM_INT);
$mergeStmt->bindValue(8, time(), \PDO::PARAM_INT);
} else {
$mergeStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
$mergeStmt->bindParam(':data', $data, \PDO::PARAM_LOB);
$mergeStmt->bindValue(':expiry', time() + $maxlifetime, \PDO::PARAM_INT);
$mergeStmt->bindValue(':time', time(), \PDO::PARAM_INT);
}
return $mergeStmt;
}
/**
* Return a PDO instance.
*
* @return \PDO
*/
protected function getConnection()
protected function getConnection(): \PDO
{
if (null === $this->pdo) {
$this->connect($this->dsn ?: ini_get('session.save_path'));
if (!isset($this->pdo)) {
$this->connect($this->dsn ?: \ini_get('session.save_path'));
}
return $this->pdo;

View File

@@ -0,0 +1,114 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
use Predis\Response\ErrorInterface;
use Symfony\Component\Cache\Traits\RedisClusterProxy;
use Symfony\Component\Cache\Traits\RedisProxy;
/**
* Redis based session storage handler based on the Redis class
* provided by the PHP redis extension.
*
* @author Dalibor Karlović <dalibor@flexolabs.io>
*/
class RedisSessionHandler extends AbstractSessionHandler
{
private $redis;
/**
* Key prefix for shared environments.
*/
private string $prefix;
/**
* Time to live in seconds.
*/
private ?int $ttl;
/**
* List of available options:
* * prefix: The prefix to use for the keys in order to avoid collision on the Redis server
* * ttl: The time to live in seconds.
*
* @throws \InvalidArgumentException When unsupported client or options are passed
*/
public function __construct(\Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy $redis, array $options = [])
{
if ($diff = array_diff(array_keys($options), ['prefix', 'ttl'])) {
throw new \InvalidArgumentException(sprintf('The following options are not supported "%s".', implode(', ', $diff)));
}
$this->redis = $redis;
$this->prefix = $options['prefix'] ?? 'sf_s';
$this->ttl = $options['ttl'] ?? null;
}
/**
* {@inheritdoc}
*/
protected function doRead(string $sessionId): string
{
return $this->redis->get($this->prefix.$sessionId) ?: '';
}
/**
* {@inheritdoc}
*/
protected function doWrite(string $sessionId, string $data): bool
{
$result = $this->redis->setEx($this->prefix.$sessionId, (int) ($this->ttl ?? \ini_get('session.gc_maxlifetime')), $data);
return $result && !$result instanceof ErrorInterface;
}
/**
* {@inheritdoc}
*/
protected function doDestroy(string $sessionId): bool
{
static $unlink = true;
if ($unlink) {
try {
$unlink = false !== $this->redis->unlink($this->prefix.$sessionId);
} catch (\Throwable $e) {
$unlink = false;
}
}
if (!$unlink) {
$this->redis->del($this->prefix.$sessionId);
}
return true;
}
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function close(): bool
{
return true;
}
public function gc(int $maxlifetime): int|false
{
return 0;
}
public function updateTimestamp(string $sessionId, string $data): bool
{
return $this->redis->expire($this->prefix.$sessionId, (int) ($this->ttl ?? \ini_get('session.gc_maxlifetime')));
}
}

View File

@@ -0,0 +1,84 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
use Doctrine\DBAL\DriverManager;
use Symfony\Component\Cache\Adapter\AbstractAdapter;
use Symfony\Component\Cache\Traits\RedisClusterProxy;
use Symfony\Component\Cache\Traits\RedisProxy;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class SessionHandlerFactory
{
public static function createHandler(object|string $connection): AbstractSessionHandler
{
if ($options = \is_string($connection) ? parse_url($connection) : false) {
parse_str($options['query'] ?? '', $options);
}
switch (true) {
case $connection instanceof \Redis:
case $connection instanceof \RedisArray:
case $connection instanceof \RedisCluster:
case $connection instanceof \Predis\ClientInterface:
case $connection instanceof RedisProxy:
case $connection instanceof RedisClusterProxy:
return new RedisSessionHandler($connection);
case $connection instanceof \Memcached:
return new MemcachedSessionHandler($connection);
case $connection instanceof \PDO:
return new PdoSessionHandler($connection);
case !\is_string($connection):
throw new \InvalidArgumentException(sprintf('Unsupported Connection: "%s".', get_debug_type($connection)));
case str_starts_with($connection, 'file://'):
$savePath = substr($connection, 7);
return new StrictSessionHandler(new NativeFileSessionHandler('' === $savePath ? null : $savePath));
case str_starts_with($connection, 'redis:'):
case str_starts_with($connection, 'rediss:'):
case str_starts_with($connection, 'memcached:'):
if (!class_exists(AbstractAdapter::class)) {
throw new \InvalidArgumentException(sprintf('Unsupported DSN "%s". Try running "composer require symfony/cache".', $connection));
}
$handlerClass = str_starts_with($connection, 'memcached:') ? MemcachedSessionHandler::class : RedisSessionHandler::class;
$connection = AbstractAdapter::createConnection($connection, ['lazy' => true]);
return new $handlerClass($connection, array_intersect_key($options ?: [], ['prefix' => 1, 'ttl' => 1]));
case str_starts_with($connection, 'pdo_oci://'):
if (!class_exists(DriverManager::class)) {
throw new \InvalidArgumentException(sprintf('Unsupported DSN "%s". Try running "composer require doctrine/dbal".', $connection));
}
$connection = DriverManager::getConnection(['url' => $connection])->getWrappedConnection();
// no break;
case str_starts_with($connection, 'mssql://'):
case str_starts_with($connection, 'mysql://'):
case str_starts_with($connection, 'mysql2://'):
case str_starts_with($connection, 'pgsql://'):
case str_starts_with($connection, 'postgres://'):
case str_starts_with($connection, 'postgresql://'):
case str_starts_with($connection, 'sqlsrv://'):
case str_starts_with($connection, 'sqlite://'):
case str_starts_with($connection, 'sqlite3://'):
return new PdoSessionHandler($connection, $options ?: []);
}
throw new \InvalidArgumentException(sprintf('Unsupported Connection: "%s".', $connection));
}
}

View File

@@ -0,0 +1,98 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
/**
* Adds basic `SessionUpdateTimestampHandlerInterface` behaviors to another `SessionHandlerInterface`.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class StrictSessionHandler extends AbstractSessionHandler
{
private \SessionHandlerInterface $handler;
private bool $doDestroy;
public function __construct(\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;
}
/**
* Returns true if this handler wraps an internal PHP session save handler using \SessionHandler.
*
* @internal
*/
public function isWrapper(): bool
{
return $this->handler instanceof \SessionHandler;
}
public function open(string $savePath, string $sessionName): bool
{
parent::open($savePath, $sessionName);
return $this->handler->open($savePath, $sessionName);
}
/**
* {@inheritdoc}
*/
protected function doRead(string $sessionId): string
{
return $this->handler->read($sessionId);
}
public function updateTimestamp(string $sessionId, string $data): bool
{
return $this->write($sessionId, $data);
}
/**
* {@inheritdoc}
*/
protected function doWrite(string $sessionId, string $data): bool
{
return $this->handler->write($sessionId, $data);
}
public function destroy(string $sessionId): bool
{
$this->doDestroy = true;
$destroyed = parent::destroy($sessionId);
return $this->doDestroy ? $this->doDestroy($sessionId) : $destroyed;
}
/**
* {@inheritdoc}
*/
protected function doDestroy(string $sessionId): bool
{
$this->doDestroy = false;
return $this->handler->destroy($sessionId);
}
public function close(): bool
{
return $this->handler->close();
}
public function gc(int $maxlifetime): int|false
{
return $this->handler->gc($maxlifetime);
}
}

View File

@@ -1,91 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
/**
* Wraps another SessionHandlerInterface to only write the session when it has been modified.
*
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class WriteCheckSessionHandler implements \SessionHandlerInterface
{
/**
* @var \SessionHandlerInterface
*/
private $wrappedSessionHandler;
/**
* @var array sessionId => session
*/
private $readSessions;
public function __construct(\SessionHandlerInterface $wrappedSessionHandler)
{
$this->wrappedSessionHandler = $wrappedSessionHandler;
}
/**
* {@inheritdoc}
*/
public function close()
{
return $this->wrappedSessionHandler->close();
}
/**
* {@inheritdoc}
*/
public function destroy($sessionId)
{
return $this->wrappedSessionHandler->destroy($sessionId);
}
/**
* {@inheritdoc}
*/
public function gc($maxlifetime)
{
return $this->wrappedSessionHandler->gc($maxlifetime);
}
/**
* {@inheritdoc}
*/
public function open($savePath, $sessionName)
{
return $this->wrappedSessionHandler->open($savePath, $sessionName);
}
/**
* {@inheritdoc}
*/
public function read($sessionId)
{
$session = $this->wrappedSessionHandler->read($sessionId);
$this->readSessions[$sessionId] = $session;
return $session;
}
/**
* {@inheritdoc}
*/
public function write($sessionId, $data)
{
if (isset($this->readSessions[$sessionId]) && $data === $this->readSessions[$sessionId]) {
return true;
}
return $this->wrappedSessionHandler->write($sessionId, $data);
}
}

View File

@@ -22,44 +22,30 @@ use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
*/
class MetadataBag implements SessionBagInterface
{
const CREATED = 'c';
const UPDATED = 'u';
const LIFETIME = 'l';
public const CREATED = 'c';
public const UPDATED = 'u';
public const LIFETIME = 'l';
/**
* @var string
*/
private $name = '__metadata';
/**
* @var string
*/
private $storageKey;
private string $name = '__metadata';
private string $storageKey;
/**
* @var array
*/
protected $meta = array(self::CREATED => 0, self::UPDATED => 0, self::LIFETIME => 0);
protected $meta = [self::CREATED => 0, self::UPDATED => 0, self::LIFETIME => 0];
/**
* Unix timestamp.
*
* @var int
*/
private $lastUsed;
private int $lastUsed;
private int $updateThreshold;
/**
* @var int
*/
private $updateThreshold;
/**
* Constructor.
*
* @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($storageKey = '_sf2_meta', $updateThreshold = 0)
public function __construct(string $storageKey = '_sf2_meta', int $updateThreshold = 0)
{
$this->storageKey = $storageKey;
$this->updateThreshold = $updateThreshold;
@@ -86,10 +72,8 @@ class MetadataBag implements SessionBagInterface
/**
* Gets the lifetime that the session cookie was set with.
*
* @return int
*/
public function getLifetime()
public function getLifetime(): int
{
return $this->meta[self::LIFETIME];
}
@@ -102,7 +86,7 @@ class MetadataBag implements SessionBagInterface
* to expire with browser session. Time is in seconds, and is
* not a Unix timestamp.
*/
public function stampNew($lifetime = null)
public function stampNew(int $lifetime = null)
{
$this->stampCreated($lifetime);
}
@@ -110,7 +94,7 @@ class MetadataBag implements SessionBagInterface
/**
* {@inheritdoc}
*/
public function getStorageKey()
public function getStorageKey(): string
{
return $this->storageKey;
}
@@ -120,7 +104,7 @@ class MetadataBag implements SessionBagInterface
*
* @return int Unix timestamp
*/
public function getCreated()
public function getCreated(): int
{
return $this->meta[self::CREATED];
}
@@ -130,7 +114,7 @@ class MetadataBag implements SessionBagInterface
*
* @return int Unix timestamp
*/
public function getLastUsed()
public function getLastUsed(): int
{
return $this->lastUsed;
}
@@ -138,33 +122,32 @@ class MetadataBag implements SessionBagInterface
/**
* {@inheritdoc}
*/
public function clear()
public function clear(): mixed
{
// nothing to do
return null;
}
/**
* {@inheritdoc}
*/
public function getName()
public function getName(): string
{
return $this->name;
}
/**
* Sets name.
*
* @param string $name
*/
public function setName($name)
public function setName(string $name)
{
$this->name = $name;
}
private function stampCreated($lifetime = null)
private function stampCreated(int $lifetime = null): void
{
$timeStamp = time();
$this->meta[self::CREATED] = $this->meta[self::UPDATED] = $this->lastUsed = $timeStamp;
$this->meta[self::LIFETIME] = (null === $lifetime) ? ini_get('session.cookie_lifetime') : $lifetime;
$this->meta[self::LIFETIME] = $lifetime ?? (int) \ini_get('session.cookie_lifetime');
}
}

View File

@@ -50,7 +50,7 @@ class MockArraySessionStorage implements SessionStorageInterface
/**
* @var array
*/
protected $data = array();
protected $data = [];
/**
* @var MetadataBag
@@ -60,25 +60,14 @@ class MockArraySessionStorage implements SessionStorageInterface
/**
* @var array|SessionBagInterface[]
*/
protected $bags = array();
protected $bags = [];
/**
* Constructor.
*
* @param string $name Session name
* @param MetadataBag $metaBag MetadataBag instance
*/
public function __construct($name = 'MOCKSESSID', MetadataBag $metaBag = null)
public function __construct(string $name = 'MOCKSESSID', MetadataBag $metaBag = null)
{
$this->name = $name;
$this->setMetadataBag($metaBag);
}
/**
* Sets the session data.
*
* @param array $array
*/
public function setSessionData(array $array)
{
$this->data = $array;
@@ -87,7 +76,7 @@ class MockArraySessionStorage implements SessionStorageInterface
/**
* {@inheritdoc}
*/
public function start()
public function start(): bool
{
if ($this->started) {
return true;
@@ -105,7 +94,7 @@ class MockArraySessionStorage implements SessionStorageInterface
/**
* {@inheritdoc}
*/
public function regenerate($destroy = false, $lifetime = null)
public function regenerate(bool $destroy = false, int $lifetime = null): bool
{
if (!$this->started) {
$this->start();
@@ -120,7 +109,7 @@ class MockArraySessionStorage implements SessionStorageInterface
/**
* {@inheritdoc}
*/
public function getId()
public function getId(): string
{
return $this->id;
}
@@ -128,7 +117,7 @@ class MockArraySessionStorage implements SessionStorageInterface
/**
* {@inheritdoc}
*/
public function setId($id)
public function setId(string $id)
{
if ($this->started) {
throw new \LogicException('Cannot set session ID after the session has started.');
@@ -140,7 +129,7 @@ class MockArraySessionStorage implements SessionStorageInterface
/**
* {@inheritdoc}
*/
public function getName()
public function getName(): string
{
return $this->name;
}
@@ -148,7 +137,7 @@ class MockArraySessionStorage implements SessionStorageInterface
/**
* {@inheritdoc}
*/
public function setName($name)
public function setName(string $name)
{
$this->name = $name;
}
@@ -159,7 +148,7 @@ class MockArraySessionStorage implements SessionStorageInterface
public function save()
{
if (!$this->started || $this->closed) {
throw new \RuntimeException('Trying to save a session that was not started yet or was already closed');
throw new \RuntimeException('Trying to save a session that was not started yet or was already closed.');
}
// nothing to do since we don't persist the session data
$this->closed = false;
@@ -177,7 +166,7 @@ class MockArraySessionStorage implements SessionStorageInterface
}
// clear out the session
$this->data = array();
$this->data = [];
// reconnect the bags to the session
$this->loadSession();
@@ -194,10 +183,10 @@ class MockArraySessionStorage implements SessionStorageInterface
/**
* {@inheritdoc}
*/
public function getBag($name)
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) {
@@ -210,16 +199,11 @@ class MockArraySessionStorage implements SessionStorageInterface
/**
* {@inheritdoc}
*/
public function isStarted()
public function isStarted(): bool
{
return $this->started;
}
/**
* Sets the MetadataBag.
*
* @param MetadataBag $bag
*/
public function setMetadataBag(MetadataBag $bag = null)
{
if (null === $bag) {
@@ -231,10 +215,8 @@ class MockArraySessionStorage implements SessionStorageInterface
/**
* Gets the MetadataBag.
*
* @return MetadataBag
*/
public function getMetadataBag()
public function getMetadataBag(): MetadataBag
{
return $this->metadataBag;
}
@@ -244,21 +226,19 @@ class MockArraySessionStorage implements SessionStorageInterface
*
* This doesn't need to be particularly cryptographically secure since this is just
* a mock.
*
* @return string
*/
protected function generateId()
protected function generateId(): string
{
return hash('sha256', uniqid('ss_mock_', true));
}
protected function loadSession()
{
$bags = array_merge($this->bags, array($this->metadataBag));
$bags = array_merge($this->bags, [$this->metadataBag]);
foreach ($bags as $bag) {
$key = $bag->getStorageKey();
$this->data[$key] = isset($this->data[$key]) ? $this->data[$key] : array();
$this->data[$key] = $this->data[$key] ?? [];
$bag->initialize($this->data[$key]);
}

View File

@@ -13,7 +13,8 @@ namespace Symfony\Component\HttpFoundation\Session\Storage;
/**
* MockFileSessionStorage is used to mock sessions for
* functional testing when done in a single PHP process.
* functional testing where you may need to persist session data
* across separate PHP processes.
*
* No PHP session is actually started since a session can be initialized
* and shutdown only once per PHP execution cycle and this class does
@@ -24,26 +25,19 @@ namespace Symfony\Component\HttpFoundation\Session\Storage;
*/
class MockFileSessionStorage extends MockArraySessionStorage
{
/**
* @var string
*/
private $savePath;
private string $savePath;
/**
* Constructor.
*
* @param string $savePath Path of directory to save session files
* @param string $name Session name
* @param MetadataBag $metaBag MetadataBag instance
* @param string|null $savePath Path of directory to save session files
*/
public function __construct($savePath = null, $name = 'MOCKSESSID', MetadataBag $metaBag = null)
public function __construct(string $savePath = null, string $name = 'MOCKSESSID', MetadataBag $metaBag = null)
{
if (null === $savePath) {
$savePath = sys_get_temp_dir();
}
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;
@@ -54,7 +48,7 @@ class MockFileSessionStorage extends MockArraySessionStorage
/**
* {@inheritdoc}
*/
public function start()
public function start(): bool
{
if ($this->started) {
return true;
@@ -74,7 +68,7 @@ class MockFileSessionStorage extends MockArraySessionStorage
/**
* {@inheritdoc}
*/
public function regenerate($destroy = false, $lifetime = null)
public function regenerate(bool $destroy = false, int $lifetime = null): bool
{
if (!$this->started) {
$this->start();
@@ -93,14 +87,35 @@ class MockFileSessionStorage extends MockArraySessionStorage
public function save()
{
if (!$this->started) {
throw new \RuntimeException('Trying to save a session that was not started yet or was already closed');
throw new \RuntimeException('Trying to save a session that was not started yet or was already closed.');
}
file_put_contents($this->getFilePath(), serialize($this->data));
$data = $this->data;
// this is needed for Silex, where the session object is re-used across requests
// in functional tests. In Symfony, the container is rebooted, so we don't have
// this issue
foreach ($this->bags as $bag) {
if (empty($data[$key = $bag->getStorageKey()])) {
unset($data[$key]);
}
}
if ([$key = $this->metadataBag->getStorageKey()] === array_keys($data)) {
unset($data[$key]);
}
try {
if ($data) {
$path = $this->getFilePath();
$tmp = $path.bin2hex(random_bytes(6));
file_put_contents($tmp, serialize($data));
rename($tmp, $path);
} else {
$this->destroy();
}
} finally {
$this->data = $data;
}
// this is needed when the session object is re-used across multiple requests
// in functional tests.
$this->started = false;
}
@@ -108,19 +123,20 @@ class MockFileSessionStorage extends MockArraySessionStorage
* Deletes a session from persistent storage.
* Deliberately leaves session data in memory intact.
*/
private function destroy()
private function destroy(): void
{
if (is_file($this->getFilePath())) {
set_error_handler(static function () {});
try {
unlink($this->getFilePath());
} finally {
restore_error_handler();
}
}
/**
* Calculate path to file.
*
* @return string File path
*/
private function getFilePath()
private function getFilePath(): string
{
return $this->savePath.'/'.$this->id.'.mocksess';
}
@@ -128,10 +144,16 @@ class MockFileSessionStorage extends MockArraySessionStorage
/**
* Reads session from storage and loads session.
*/
private function read()
private function read(): void
{
$filePath = $this->getFilePath();
$this->data = is_readable($filePath) && is_file($filePath) ? unserialize(file_get_contents($filePath)) : array();
set_error_handler(static function () {});
try {
$data = file_get_contents($this->getFilePath());
} finally {
restore_error_handler();
}
$this->data = $data ? unserialize($data) : [];
$this->loadSession();
}

View File

@@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session\Storage;
use Symfony\Component\HttpFoundation\Request;
// Help opcache.preload discover always-needed symbols
class_exists(MockFileSessionStorage::class);
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class MockFileSessionStorageFactory implements SessionStorageFactoryInterface
{
private ?string $savePath;
private string $name;
private $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 createStorage(?Request $request): SessionStorageInterface
{
return new MockFileSessionStorage($this->savePath, $this->name, $this->metaBag);
}
}

View File

@@ -11,12 +11,16 @@
namespace Symfony\Component\HttpFoundation\Session\Storage;
use Symfony\Component\Debug\Exception\ContextErrorException;
use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeSessionHandler;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler;
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy;
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy;
// Help opcache.preload discover always-needed symbols
class_exists(MetadataBag::class);
class_exists(StrictSessionHandler::class);
class_exists(SessionHandlerProxy::class);
/**
* This provides a base class for session attribute storage.
*
@@ -25,11 +29,9 @@ use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy;
class NativeSessionStorage implements SessionStorageInterface
{
/**
* Array of SessionBagInterface.
*
* @var SessionBagInterface[]
*/
protected $bags;
protected $bags = [];
/**
* @var bool
@@ -42,7 +44,7 @@ class NativeSessionStorage implements SessionStorageInterface
protected $closed = false;
/**
* @var AbstractProxy
* @var AbstractProxy|\SessionHandlerInterface
*/
protected $saveHandler;
@@ -52,59 +54,54 @@ class NativeSessionStorage implements SessionStorageInterface
protected $metadataBag;
/**
* Constructor.
*
* Depending on how you want the storage driver to behave you probably
* want to override this constructor entirely.
*
* List of options for $options array with their defaults.
*
* @see http://php.net/session.configuration for options
* @see https://php.net/session.configuration for options
* but we omit 'session.' from the beginning of the keys for convenience.
*
* ("auto_start", is not supported as it tells PHP to start a session before
* PHP starts to execute user-land code. Setting during runtime has no effect).
*
* cache_limiter, "" (use "0" to prevent headers from being sent entirely).
* cache_expire, "0"
* cookie_domain, ""
* cookie_httponly, ""
* cookie_lifetime, "0"
* cookie_path, "/"
* cookie_secure, ""
* entropy_file, ""
* entropy_length, "0"
* cookie_samesite, null
* gc_divisor, "100"
* gc_maxlifetime, "1440"
* gc_probability, "1"
* hash_bits_per_character, "4"
* hash_function, "0"
* lazy_write, "1"
* name, "PHPSESSID"
* referer_check, ""
* serialize_handler, "php"
* use_strict_mode, "0"
* use_strict_mode, "1"
* use_cookies, "1"
* use_only_cookies, "1"
* use_trans_sid, "0"
* upload_progress.enabled, "1"
* upload_progress.cleanup, "1"
* upload_progress.prefix, "upload_progress_"
* upload_progress.name, "PHP_SESSION_UPLOAD_PROGRESS"
* upload_progress.freq, "1%"
* upload_progress.min-freq, "1"
* url_rewriter.tags, "a=href,area=href,frame=src,form=,fieldset="
* sid_length, "32"
* sid_bits_per_character, "5"
* trans_sid_hosts, $_SERVER['HTTP_HOST']
* trans_sid_tags, "a=href,area=href,frame=src,form="
*
* @param array $options Session configuration options
* @param AbstractProxy|NativeSessionHandler|\SessionHandlerInterface|null $handler
* @param MetadataBag $metaBag MetadataBag
*/
public function __construct(array $options = array(), $handler = null, MetadataBag $metaBag = null)
public function __construct(array $options = [], AbstractProxy|\SessionHandlerInterface $handler = null, MetadataBag $metaBag = null)
{
session_cache_limiter(''); // disable by default because it's managed by HeaderBag (if used)
ini_set('session.use_cookies', 1);
if (!\extension_loaded('session')) {
throw new \LogicException('PHP extension "session" is required.');
}
$options += [
'cache_limiter' => '',
'cache_expire' => 0,
'use_cookies' => 1,
'lazy_write' => 1,
'use_strict_mode' => 1,
];
session_register_shutdown();
@@ -115,10 +112,8 @@ class NativeSessionStorage implements SessionStorageInterface
/**
* Gets the save handler instance.
*
* @return AbstractProxy
*/
public function getSaveHandler()
public function getSaveHandler(): AbstractProxy|\SessionHandlerInterface
{
return $this->saveHandler;
}
@@ -126,7 +121,7 @@ class NativeSessionStorage implements SessionStorageInterface
/**
* {@inheritdoc}
*/
public function start()
public function start(): bool
{
if ($this->started) {
return true;
@@ -136,13 +131,49 @@ class NativeSessionStorage implements SessionStorageInterface
throw new \RuntimeException('Failed to start the session: already started by PHP.');
}
if (ini_get('session.use_cookies') && headers_sent($file, $line)) {
if (filter_var(\ini_get('session.use_cookies'), \FILTER_VALIDATE_BOOLEAN) && 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));
}
$sessionId = $_COOKIE[session_name()] ?? null;
/*
* Explanation of the session ID regular expression: `/^[a-zA-Z0-9,-]{22,250}$/`.
*
* ---------- Part 1
*
* The part `[a-zA-Z0-9,-]` is related to the PHP ini directive `session.sid_bits_per_character` defined as 6.
* 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,-`
*
* ---------- Part 2
*
* The part `{22,250}` is related to the PHP ini directive `session.sid_length`.
* See https://www.php.net/manual/en/session.configuration.php#ini.session.sid-length.
* Allowed values are integers between 22 and 256, but we use 250 for the max.
*
* Where does the 250 come from?
* - 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.
*
* ---------- Conclusion
*
* The parts 1 and 2 prevent the warning below:
* `PHP Warning: SessionHandler::read(): Session ID is too long or contains illegal characters. Only the A-Z, a-z, 0-9, "-", and "," characters are allowed.`
*
* The part 2 prevents the warning below:
* `PHP Warning: SessionHandler::read(): open(filepath, O_RDWR) failed: No such file or directory (2).`
*/
if ($sessionId && $this->saveHandler instanceof AbstractProxy && 'files' === $this->saveHandler->getSaveHandlerName() && !preg_match('/^[a-zA-Z0-9,-]{22,250}$/', $sessionId)) {
// the session ID in the header is invalid, create a new one
session_id(session_create_id());
}
// ok to try and start the session
if (!session_start()) {
throw new \RuntimeException('Failed to start the session');
throw new \RuntimeException('Failed to start the session.');
}
$this->loadSession();
@@ -153,7 +184,7 @@ class NativeSessionStorage implements SessionStorageInterface
/**
* {@inheritdoc}
*/
public function getId()
public function getId(): string
{
return $this->saveHandler->getId();
}
@@ -161,7 +192,7 @@ class NativeSessionStorage implements SessionStorageInterface
/**
* {@inheritdoc}
*/
public function setId($id)
public function setId(string $id)
{
$this->saveHandler->setId($id);
}
@@ -169,7 +200,7 @@ class NativeSessionStorage implements SessionStorageInterface
/**
* {@inheritdoc}
*/
public function getName()
public function getName(): string
{
return $this->saveHandler->getName();
}
@@ -177,7 +208,7 @@ class NativeSessionStorage implements SessionStorageInterface
/**
* {@inheritdoc}
*/
public function setName($name)
public function setName(string $name)
{
$this->saveHandler->setName($name);
}
@@ -185,28 +216,28 @@ class NativeSessionStorage implements SessionStorageInterface
/**
* {@inheritdoc}
*/
public function regenerate($destroy = false, $lifetime = null)
public function regenerate(bool $destroy = false, int $lifetime = null): bool
{
// Cannot regenerate the session ID for non-active sessions.
if (\PHP_SESSION_ACTIVE !== session_status()) {
return false;
}
if (null !== $lifetime) {
if (headers_sent()) {
return false;
}
if (null !== $lifetime && $lifetime != \ini_get('session.cookie_lifetime')) {
$this->save();
ini_set('session.cookie_lifetime', $lifetime);
$this->start();
}
if ($destroy) {
$this->metadataBag->stampNew();
}
$isRegenerated = session_regenerate_id($destroy);
// The reference to $_SESSION in session bags is lost in PHP7 and we need to re-create it.
// @see https://bugs.php.net/bug.php?id=70013
$this->loadSession();
return $isRegenerated;
return session_regenerate_id($destroy);
}
/**
@@ -214,24 +245,37 @@ class NativeSessionStorage implements SessionStorageInterface
*/
public function save()
{
// Register custom error handler to catch a possible failure warning during session write
set_error_handler(function ($errno, $errstr, $errfile, $errline, $errcontext) {
throw new ContextErrorException($errstr, $errno, E_WARNING, $errfile, $errline, $errcontext);
}, E_WARNING);
// Store a copy so we can restore the bags in case the session was not left empty
$session = $_SESSION;
foreach ($this->bags as $bag) {
if (empty($_SESSION[$key = $bag->getStorageKey()])) {
unset($_SESSION[$key]);
}
}
if ($_SESSION && [$key = $this->metadataBag->getStorageKey()] === array_keys($_SESSION)) {
unset($_SESSION[$key]);
}
// Register error handler to add information about the current save handler
$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', \get_class($handler));
}
return $previousHandler ? $previousHandler($type, $msg, $file, $line) : false;
});
try {
session_write_close();
} finally {
restore_error_handler();
} catch (ContextErrorException $e) {
// The default PHP error message is not very helpful, as it does not give any information on the current save handler.
// Therefore, we catch this error and trigger a warning with a better error message
$handler = $this->getSaveHandler();
if ($handler instanceof SessionHandlerProxy) {
$handler = $handler->getHandler();
}
restore_error_handler();
trigger_error(sprintf('session_write_close(): Failed to write session data with %s handler', get_class($handler)), E_USER_WARNING);
// Restore only if not empty
if ($_SESSION) {
$_SESSION = $session;
}
}
$this->closed = true;
@@ -249,7 +293,7 @@ class NativeSessionStorage implements SessionStorageInterface
}
// clear out the session
$_SESSION = array();
$_SESSION = [];
// reconnect the bags to the session
$this->loadSession();
@@ -270,13 +314,13 @@ class NativeSessionStorage implements SessionStorageInterface
/**
* {@inheritdoc}
*/
public function getBag($name)
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->saveHandler->isActive() && !$this->started) {
if (!$this->started && $this->saveHandler->isActive()) {
$this->loadSession();
} elseif (!$this->started) {
$this->start();
@@ -285,11 +329,6 @@ class NativeSessionStorage implements SessionStorageInterface
return $this->bags[$name];
}
/**
* Sets the MetadataBag.
*
* @param MetadataBag $metaBag
*/
public function setMetadataBag(MetadataBag $metaBag = null)
{
if (null === $metaBag) {
@@ -301,10 +340,8 @@ class NativeSessionStorage implements SessionStorageInterface
/**
* Gets the MetadataBag.
*
* @return MetadataBag
*/
public function getMetadataBag()
public function getMetadataBag(): MetadataBag
{
return $this->metadataBag;
}
@@ -312,7 +349,7 @@ class NativeSessionStorage implements SessionStorageInterface
/**
* {@inheritdoc}
*/
public function isStarted()
public function isStarted(): bool
{
return $this->started;
}
@@ -323,27 +360,31 @@ class NativeSessionStorage implements SessionStorageInterface
* For convenience we omit 'session.' from the beginning of the keys.
* Explicitly ignores other ini keys.
*
* @param array $options Session ini directives array(key => value)
* @param array $options Session ini directives [key => value]
*
* @see http://php.net/session.configuration
* @see https://php.net/session.configuration
*/
public function setOptions(array $options)
{
$validOptions = array_flip(array(
'cache_limiter', 'cookie_domain', 'cookie_httponly',
'cookie_lifetime', 'cookie_path', 'cookie_secure',
'entropy_file', 'entropy_length', 'gc_divisor',
'gc_maxlifetime', 'gc_probability', 'hash_bits_per_character',
'hash_function', 'name', 'referer_check',
if (headers_sent() || \PHP_SESSION_ACTIVE === session_status()) {
return;
}
$validOptions = array_flip([
'cache_expire', 'cache_limiter', 'cookie_domain', 'cookie_httponly',
'cookie_lifetime', 'cookie_path', 'cookie_secure', 'cookie_samesite',
'gc_divisor', 'gc_maxlifetime', 'gc_probability',
'lazy_write', 'name', 'referer_check',
'serialize_handler', 'use_strict_mode', 'use_cookies',
'use_only_cookies', 'use_trans_sid', 'upload_progress.enabled',
'upload_progress.cleanup', 'upload_progress.prefix', 'upload_progress.name',
'upload_progress.freq', 'upload_progress.min-freq', 'url_rewriter.tags',
'use_only_cookies', 'use_trans_sid',
'sid_length', 'sid_bits_per_character', 'trans_sid_hosts', 'trans_sid_tags',
));
]);
foreach ($options as $key => $value) {
if (isset($validOptions[$key])) {
if ('cookie_secure' === $key && 'auto' === $value) {
continue;
}
ini_set('session.'.$key, $value);
}
}
@@ -358,37 +399,36 @@ class NativeSessionStorage implements SessionStorageInterface
* ini_set('session.save_handler', 'files');
* ini_set('session.save_path', '/tmp');
*
* or pass in a NativeSessionHandler instance which configures session.save_handler in the
* constructor, for a template see NativeFileSessionHandler or use handlers in
* composer package drak/native-session
* or pass in a \SessionHandler instance which configures session.save_handler in the
* constructor, for a template see NativeFileSessionHandler.
*
* @see http://php.net/session-set-save-handler
* @see http://php.net/sessionhandlerinterface
* @see http://php.net/sessionhandler
* @see http://github.com/drak/NativeSession
*
* @param AbstractProxy|NativeSessionHandler|\SessionHandlerInterface|null $saveHandler
* @see https://php.net/session-set-save-handler
* @see https://php.net/sessionhandlerinterface
* @see https://php.net/sessionhandler
*
* @throws \InvalidArgumentException
*/
public function setSaveHandler($saveHandler = null)
public function setSaveHandler(AbstractProxy|\SessionHandlerInterface $saveHandler = null)
{
if (!$saveHandler instanceof AbstractProxy &&
!$saveHandler instanceof NativeSessionHandler &&
!$saveHandler instanceof \SessionHandlerInterface &&
null !== $saveHandler) {
throw new \InvalidArgumentException('Must be instance of AbstractProxy or NativeSessionHandler; implement \SessionHandlerInterface; or be null.');
throw new \InvalidArgumentException('Must be instance of AbstractProxy; implement \SessionHandlerInterface; or be null.');
}
// Wrap $saveHandler in proxy and prevent double wrapping of proxy
if (!$saveHandler instanceof AbstractProxy && $saveHandler instanceof \SessionHandlerInterface) {
$saveHandler = new SessionHandlerProxy($saveHandler);
} elseif (!$saveHandler instanceof AbstractProxy) {
$saveHandler = new SessionHandlerProxy(new \SessionHandler());
$saveHandler = new SessionHandlerProxy(new StrictSessionHandler(new \SessionHandler()));
}
$this->saveHandler = $saveHandler;
if ($this->saveHandler instanceof \SessionHandlerInterface) {
if (headers_sent() || \PHP_SESSION_ACTIVE === session_status()) {
return;
}
if ($this->saveHandler instanceof SessionHandlerProxy) {
session_set_save_handler($this->saveHandler, false);
}
}
@@ -400,8 +440,6 @@ class NativeSessionStorage implements SessionStorageInterface
* are set to (either PHP's internal, or a custom save handler set with session_set_save_handler()).
* PHP takes the return value from the read() handler, unserializes it
* and populates $_SESSION with the result automatically.
*
* @param array|null $session
*/
protected function loadSession(array &$session = null)
{
@@ -409,11 +447,11 @@ class NativeSessionStorage implements SessionStorageInterface
$session = &$_SESSION;
}
$bags = array_merge($this->bags, array($this->metadataBag));
$bags = array_merge($this->bags, [$this->metadataBag]);
foreach ($bags as $bag) {
$key = $bag->getStorageKey();
$session[$key] = isset($session[$key]) ? $session[$key] : array();
$session[$key] = isset($session[$key]) && \is_array($session[$key]) ? $session[$key] : [];
$bag->initialize($session[$key]);
}

View File

@@ -0,0 +1,50 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session\Storage;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy;
// Help opcache.preload discover always-needed symbols
class_exists(NativeSessionStorage::class);
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class NativeSessionStorageFactory implements SessionStorageFactoryInterface
{
private array $options;
private $handler;
private $metaBag;
private bool $secure;
/**
* @see NativeSessionStorage constructor.
*/
public function __construct(array $options = [], AbstractProxy|\SessionHandlerInterface $handler = null, MetadataBag $metaBag = null, bool $secure = false)
{
$this->options = $options;
$this->handler = $handler;
$this->metaBag = $metaBag;
$this->secure = $secure;
}
public function createStorage(?Request $request): SessionStorageInterface
{
$storage = new NativeSessionStorage($this->options, $this->handler, $this->metaBag);
if ($this->secure && $request && $request->isSecure()) {
$storage->setOptions(['cookie_secure' => true]);
}
return $storage;
}
}

View File

@@ -12,7 +12,6 @@
namespace Symfony\Component\HttpFoundation\Session\Storage;
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeSessionHandler;
/**
* Allows session to be started by PHP and managed by Symfony.
@@ -21,14 +20,12 @@ use Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeSessionHandle
*/
class PhpBridgeSessionStorage extends NativeSessionStorage
{
/**
* Constructor.
*
* @param AbstractProxy|NativeSessionHandler|\SessionHandlerInterface|null $handler
* @param MetadataBag $metaBag MetadataBag
*/
public function __construct($handler = null, MetadataBag $metaBag = null)
public function __construct(AbstractProxy|\SessionHandlerInterface $handler = null, MetadataBag $metaBag = null)
{
if (!\extension_loaded('session')) {
throw new \LogicException('PHP extension "session" is required.');
}
$this->setMetadataBag($metaBag);
$this->setSaveHandler($handler);
}
@@ -36,7 +33,7 @@ class PhpBridgeSessionStorage extends NativeSessionStorage
/**
* {@inheritdoc}
*/
public function start()
public function start(): bool
{
if ($this->started) {
return true;

View File

@@ -0,0 +1,45 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session\Storage;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy;
// Help opcache.preload discover always-needed symbols
class_exists(PhpBridgeSessionStorage::class);
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class PhpBridgeSessionStorageFactory implements SessionStorageFactoryInterface
{
private $handler;
private $metaBag;
private bool $secure;
public function __construct(AbstractProxy|\SessionHandlerInterface $handler = null, MetadataBag $metaBag = null, bool $secure = false)
{
$this->handler = $handler;
$this->metaBag = $metaBag;
$this->secure = $secure;
}
public function createStorage(?Request $request): SessionStorageInterface
{
$storage = new PhpBridgeSessionStorage($this->handler, $this->metaBag);
if ($this->secure && $request && $request->isSecure()) {
$storage->setOptions(['cookie_secure' => true]);
}
return $storage;
}
}

View File

@@ -12,8 +12,6 @@
namespace Symfony\Component\HttpFoundation\Session\Storage\Proxy;
/**
* AbstractProxy.
*
* @author Drak <drak@zikula.org>
*/
abstract class AbstractProxy
@@ -32,50 +30,40 @@ abstract class AbstractProxy
/**
* Gets the session.save_handler name.
*
* @return string
*/
public function getSaveHandlerName()
public function getSaveHandlerName(): ?string
{
return $this->saveHandlerName;
}
/**
* Is this proxy handler and instance of \SessionHandlerInterface.
*
* @return bool
*/
public function isSessionHandlerInterface()
public function isSessionHandlerInterface(): bool
{
return $this instanceof \SessionHandlerInterface;
}
/**
* Returns true if this handler wraps an internal PHP session save handler using \SessionHandler.
*
* @return bool
*/
public function isWrapper()
public function isWrapper(): bool
{
return $this->wrapper;
}
/**
* Has a session started?
*
* @return bool
*/
public function isActive()
public function isActive(): bool
{
return \PHP_SESSION_ACTIVE === session_status();
}
/**
* Gets the session ID.
*
* @return string
*/
public function getId()
public function getId(): string
{
return session_id();
}
@@ -83,14 +71,12 @@ abstract class AbstractProxy
/**
* Sets the session ID.
*
* @param string $id
*
* @throws \LogicException
*/
public function setId($id)
public function setId(string $id)
{
if ($this->isActive()) {
throw new \LogicException('Cannot change the ID of an active session');
throw new \LogicException('Cannot change the ID of an active session.');
}
session_id($id);
@@ -98,10 +84,8 @@ abstract class AbstractProxy
/**
* Gets the session name.
*
* @return string
*/
public function getName()
public function getName(): string
{
return session_name();
}
@@ -109,14 +93,12 @@ abstract class AbstractProxy
/**
* Sets the session name.
*
* @param string $name
*
* @throws \LogicException
*/
public function setName($name)
public function setName(string $name)
{
if ($this->isActive()) {
throw new \LogicException('Cannot change the name of an active session');
throw new \LogicException('Cannot change the name of an active session.');
}
session_name($name);

View File

@@ -1,41 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session\Storage\Proxy;
/**
* NativeProxy.
*
* This proxy is built-in session handlers in PHP 5.3.x
*
* @author Drak <drak@zikula.org>
*/
class NativeProxy extends AbstractProxy
{
/**
* Constructor.
*/
public function __construct()
{
// this makes an educated guess as to what the handler is since it should already be set.
$this->saveHandlerName = ini_get('session.save_handler');
}
/**
* Returns true if this handler wraps an internal PHP session save handler using \SessionHandler.
*
* @return bool False
*/
public function isWrapper()
{
return false;
}
}

View File

@@ -11,85 +11,66 @@
namespace Symfony\Component\HttpFoundation\Session\Storage\Proxy;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler;
/**
* SessionHandler proxy.
*
* @author Drak <drak@zikula.org>
*/
class SessionHandlerProxy extends AbstractProxy implements \SessionHandlerInterface
class SessionHandlerProxy extends AbstractProxy implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface
{
/**
* @var \SessionHandlerInterface
*/
protected $handler;
/**
* Constructor.
*
* @param \SessionHandlerInterface $handler
*/
public function __construct(\SessionHandlerInterface $handler)
{
$this->handler = $handler;
$this->wrapper = ($handler instanceof \SessionHandler);
$this->saveHandlerName = $this->wrapper ? ini_get('session.save_handler') : 'user';
$this->wrapper = $handler instanceof \SessionHandler;
$this->saveHandlerName = $this->wrapper || ($handler instanceof StrictSessionHandler && $handler->isWrapper()) ? \ini_get('session.save_handler') : 'user';
}
/**
* @return \SessionHandlerInterface
*/
public function getHandler()
public function getHandler(): \SessionHandlerInterface
{
return $this->handler;
}
// \SessionHandlerInterface
/**
* {@inheritdoc}
*/
public function open($savePath, $sessionName)
public function open(string $savePath, string $sessionName): bool
{
return (bool) $this->handler->open($savePath, $sessionName);
return $this->handler->open($savePath, $sessionName);
}
/**
* {@inheritdoc}
*/
public function close()
public function close(): bool
{
return (bool) $this->handler->close();
return $this->handler->close();
}
/**
* {@inheritdoc}
*/
public function read($sessionId)
public function read(string $sessionId): string|false
{
return (string) $this->handler->read($sessionId);
return $this->handler->read($sessionId);
}
/**
* {@inheritdoc}
*/
public function write($sessionId, $data)
public function write(string $sessionId, string $data): bool
{
return (bool) $this->handler->write($sessionId, $data);
return $this->handler->write($sessionId, $data);
}
/**
* {@inheritdoc}
*/
public function destroy($sessionId)
public function destroy(string $sessionId): bool
{
return (bool) $this->handler->destroy($sessionId);
return $this->handler->destroy($sessionId);
}
/**
* {@inheritdoc}
*/
public function gc($maxlifetime)
public function gc(int $maxlifetime): int|false
{
return (bool) $this->handler->gc($maxlifetime);
return $this->handler->gc($maxlifetime);
}
public function validateId(string $sessionId): bool
{
return !$this->handler instanceof \SessionUpdateTimestampHandlerInterface || $this->handler->validateId($sessionId);
}
public function updateTimestamp(string $sessionId, string $data): bool
{
return $this->handler instanceof \SessionUpdateTimestampHandlerInterface ? $this->handler->updateTimestamp($sessionId, $data) : $this->write($sessionId, $data);
}
}

View File

@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session\Storage;
use Symfony\Component\HttpFoundation\Request;
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*/
interface SessionStorageFactoryInterface
{
/**
* Creates a new instance of SessionStorageInterface.
*/
public function createStorage(?Request $request): SessionStorageInterface;
}

View File

@@ -24,46 +24,34 @@ interface SessionStorageInterface
/**
* Starts the session.
*
* @return bool True if started
*
* @throws \RuntimeException If something goes wrong starting the session.
* @throws \RuntimeException if something goes wrong starting the session
*/
public function start();
public function start(): bool;
/**
* Checks if the session is started.
*
* @return bool True if started, false otherwise
*/
public function isStarted();
public function isStarted(): bool;
/**
* Returns the session ID.
*
* @return string The session ID or empty
*/
public function getId();
public function getId(): string;
/**
* Sets the session ID.
*
* @param string $id
*/
public function setId($id);
public function setId(string $id);
/**
* Returns the session name.
*
* @return mixed The session name
*/
public function getName();
public function getName(): string;
/**
* Sets the session name.
*
* @param string $name
*/
public function setName($name);
public function setName(string $name);
/**
* Regenerates id that represents this storage.
@@ -77,7 +65,7 @@ interface SessionStorageInterface
* only delete the session data from persistent storage.
*
* Care: When regenerating the session ID no locking is involved in PHP's
* session design. See https://bugs.php.net/bug.php?id=61470 for a discussion.
* session design. See https://bugs.php.net/61470 for a discussion.
* So you must make sure the regenerated session is saved BEFORE sending the
* headers with the new ID. Symfony's HttpKernel offers a listener for this.
* See Symfony\Component\HttpKernel\EventListener\SaveSessionListener.
@@ -90,11 +78,9 @@ interface SessionStorageInterface
* to expire with browser session. Time is in seconds, and is
* not a Unix timestamp.
*
* @return bool True if session regenerated, false if error
*
* @throws \RuntimeException If an error occurs while regenerating this storage
*/
public function regenerate($destroy = false, $lifetime = null);
public function regenerate(bool $destroy = false, int $lifetime = null): bool;
/**
* Force the session to be saved and closed.
@@ -104,8 +90,8 @@ interface SessionStorageInterface
* a real PHP session would interfere with testing, in which case
* it should actually persist the session data if required.
*
* @throws \RuntimeException If the session is saved without being started, or if the session
* is already closed.
* @throws \RuntimeException if the session is saved without being started, or if the session
* is already closed
*/
public function save();
@@ -117,23 +103,14 @@ interface SessionStorageInterface
/**
* Gets a SessionBagInterface by name.
*
* @param string $name
*
* @return SessionBagInterface
*
* @throws \InvalidArgumentException If the bag does not exist
*/
public function getBag($name);
public function getBag(string $name): SessionBagInterface;
/**
* Registers a SessionBagInterface for use.
*
* @param SessionBagInterface $bag
*/
public function registerBag(SessionBagInterface $bag);
/**
* @return MetadataBag
*/
public function getMetadataBag();
public function getMetadataBag(): MetadataBag;
}

View File

@@ -17,7 +17,7 @@ namespace Symfony\Component\HttpFoundation;
* A StreamedResponse uses a callback for its content.
*
* The callback should use the standard PHP functions like echo
* to stream the response back to the client. The flush() method
* to stream the response back to the client. The flush() function
* can also be used if needed.
*
* @see flush()
@@ -28,16 +28,9 @@ class StreamedResponse extends Response
{
protected $callback;
protected $streamed;
private $headersSent;
private bool $headersSent;
/**
* Constructor.
*
* @param callable|null $callback A valid PHP callback or null to set it later
* @param int $status The response status code
* @param array $headers An array of response headers
*/
public function __construct(callable $callback = null, $status = 200, $headers = array())
public function __construct(callable $callback = null, int $status = 200, array $headers = [])
{
parent::__construct(null, $status, $headers);
@@ -48,55 +41,47 @@ class StreamedResponse extends Response
$this->headersSent = false;
}
/**
* Factory method for chainability.
*
* @param callable|null $callback A valid PHP callback or null to set it later
* @param int $status The response status code
* @param array $headers An array of response headers
*
* @return static
*/
public static function create($callback = null, $status = 200, $headers = array())
{
return new static($callback, $status, $headers);
}
/**
* Sets the PHP callback associated with this Response.
*
* @param callable $callback A valid PHP callback
* @return $this
*/
public function setCallback(callable $callback)
public function setCallback(callable $callback): static
{
$this->callback = $callback;
return $this;
}
/**
* {@inheritdoc}
*
* This method only sends the headers once.
*
* @return $this
*/
public function sendHeaders()
public function sendHeaders(): static
{
if ($this->headersSent) {
return;
return $this;
}
$this->headersSent = true;
parent::sendHeaders();
return parent::sendHeaders();
}
/**
* {@inheritdoc}
*
* This method only sends the content once.
*
* @return $this
*/
public function sendContent()
public function sendContent(): static
{
if ($this->streamed) {
return;
return $this;
}
$this->streamed = true;
@@ -105,27 +90,33 @@ class StreamedResponse extends Response
throw new \LogicException('The Response callback must not be null.');
}
call_user_func($this->callback);
($this->callback)();
return $this;
}
/**
* {@inheritdoc}
*
* @throws \LogicException when the content is not null
*
* @return $this
*/
public function setContent($content)
public function setContent(?string $content): static
{
if (null !== $content) {
throw new \LogicException('The content cannot be set on a StreamedResponse instance.');
}
$this->streamed = true;
return $this;
}
/**
* {@inheritdoc}
*
* @return false
*/
public function getContent()
public function getContent(): string|false
{
return false;
}

View File

@@ -0,0 +1,55 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Test\Constraint;
use PHPUnit\Framework\Constraint\Constraint;
use Symfony\Component\HttpFoundation\Request;
final class RequestAttributeValueSame extends Constraint
{
private string $name;
private string $value;
public function __construct(string $name, string $value)
{
$this->name = $name;
$this->value = $value;
}
/**
* {@inheritdoc}
*/
public function toString(): string
{
return sprintf('has attribute "%s" with value "%s"', $this->name, $this->value);
}
/**
* @param Request $request
*
* {@inheritdoc}
*/
protected function matches($request): bool
{
return $this->value === $request->attributes->get($this->name);
}
/**
* @param Request $request
*
* {@inheritdoc}
*/
protected function failureDescription($request): string
{
return 'the Request '.$this->toString();
}
}

View File

@@ -0,0 +1,85 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Test\Constraint;
use PHPUnit\Framework\Constraint\Constraint;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\Response;
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;
}
/**
* {@inheritdoc}
*/
public function toString(): string
{
$str = sprintf('has cookie "%s"', $this->name);
if ('/' !== $this->path) {
$str .= sprintf(' with path "%s"', $this->path);
}
if ($this->domain) {
$str .= sprintf(' for domain "%s"', $this->domain);
}
$str .= sprintf(' with value "%s"', $this->value);
return $str;
}
/**
* @param Response $response
*
* {@inheritdoc}
*/
protected function matches($response): bool
{
$cookie = $this->getCookie($response);
if (!$cookie) {
return false;
}
return $this->value === (string) $cookie->getValue();
}
/**
* @param Response $response
*
* {@inheritdoc}
*/
protected function failureDescription($response): string
{
return 'the Response '.$this->toString();
}
protected function getCookie(Response $response): ?Cookie
{
$cookies = $response->headers->getCookies();
$filteredCookies = array_filter($cookies, function (Cookie $cookie) {
return $cookie->getName() === $this->name && $cookie->getPath() === $this->path && $cookie->getDomain() === $this->domain;
});
return reset($filteredCookies) ?: null;
}
}

View File

@@ -0,0 +1,71 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Test\Constraint;
use PHPUnit\Framework\Constraint\Constraint;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Asserts that the response is in the given format.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
final class ResponseFormatSame extends Constraint
{
private $request;
private ?string $format;
public function __construct(Request $request, ?string $format)
{
$this->request = $request;
$this->format = $format;
}
/**
* {@inheritdoc}
*/
public function toString(): string
{
return 'format is '.($this->format ?? 'null');
}
/**
* @param Response $response
*
* {@inheritdoc}
*/
protected function matches($response): bool
{
return $this->format === $this->request->getFormat($response->headers->get('Content-Type'));
}
/**
* @param Response $response
*
* {@inheritdoc}
*/
protected function failureDescription($response): string
{
return 'the Response '.$this->toString();
}
/**
* @param Response $response
*
* {@inheritdoc}
*/
protected function additionalFailureDescription($response): string
{
return (string) $response;
}
}

View File

@@ -0,0 +1,77 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Test\Constraint;
use PHPUnit\Framework\Constraint\Constraint;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\Response;
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;
}
/**
* {@inheritdoc}
*/
public function toString(): string
{
$str = sprintf('has cookie "%s"', $this->name);
if ('/' !== $this->path) {
$str .= sprintf(' with path "%s"', $this->path);
}
if ($this->domain) {
$str .= sprintf(' for domain "%s"', $this->domain);
}
return $str;
}
/**
* @param Response $response
*
* {@inheritdoc}
*/
protected function matches($response): bool
{
return null !== $this->getCookie($response);
}
/**
* @param Response $response
*
* {@inheritdoc}
*/
protected function failureDescription($response): string
{
return 'the Response '.$this->toString();
}
private function getCookie(Response $response): ?Cookie
{
$cookies = $response->headers->getCookies();
$filteredCookies = array_filter($cookies, function (Cookie $cookie) {
return $cookie->getName() === $this->name && $cookie->getPath() === $this->path && $cookie->getDomain() === $this->domain;
});
return reset($filteredCookies) ?: null;
}
}

View File

@@ -0,0 +1,53 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Test\Constraint;
use PHPUnit\Framework\Constraint\Constraint;
use Symfony\Component\HttpFoundation\Response;
final class ResponseHasHeader extends Constraint
{
private string $headerName;
public function __construct(string $headerName)
{
$this->headerName = $headerName;
}
/**
* {@inheritdoc}
*/
public function toString(): string
{
return sprintf('has header "%s"', $this->headerName);
}
/**
* @param Response $response
*
* {@inheritdoc}
*/
protected function matches($response): bool
{
return $response->headers->has($this->headerName);
}
/**
* @param Response $response
*
* {@inheritdoc}
*/
protected function failureDescription($response): string
{
return 'the Response '.$this->toString();
}
}

View File

@@ -0,0 +1,55 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Test\Constraint;
use PHPUnit\Framework\Constraint\Constraint;
use Symfony\Component\HttpFoundation\Response;
final class ResponseHeaderSame extends Constraint
{
private string $headerName;
private string $expectedValue;
public function __construct(string $headerName, string $expectedValue)
{
$this->headerName = $headerName;
$this->expectedValue = $expectedValue;
}
/**
* {@inheritdoc}
*/
public function toString(): string
{
return sprintf('has header "%s" with value "%s"', $this->headerName, $this->expectedValue);
}
/**
* @param Response $response
*
* {@inheritdoc}
*/
protected function matches($response): bool
{
return $this->expectedValue === $response->headers->get($this->headerName, null);
}
/**
* @param Response $response
*
* {@inheritdoc}
*/
protected function failureDescription($response): string
{
return 'the Response '.$this->toString();
}
}

View File

@@ -0,0 +1,56 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Test\Constraint;
use PHPUnit\Framework\Constraint\Constraint;
use Symfony\Component\HttpFoundation\Response;
final class ResponseIsRedirected extends Constraint
{
/**
* {@inheritdoc}
*/
public function toString(): string
{
return 'is redirected';
}
/**
* @param Response $response
*
* {@inheritdoc}
*/
protected function matches($response): bool
{
return $response->isRedirect();
}
/**
* @param Response $response
*
* {@inheritdoc}
*/
protected function failureDescription($response): string
{
return 'the Response '.$this->toString();
}
/**
* @param Response $response
*
* {@inheritdoc}
*/
protected function additionalFailureDescription($response): string
{
return (string) $response;
}
}

View File

@@ -0,0 +1,56 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Test\Constraint;
use PHPUnit\Framework\Constraint\Constraint;
use Symfony\Component\HttpFoundation\Response;
final class ResponseIsSuccessful extends Constraint
{
/**
* {@inheritdoc}
*/
public function toString(): string
{
return 'is successful';
}
/**
* @param Response $response
*
* {@inheritdoc}
*/
protected function matches($response): bool
{
return $response->isSuccessful();
}
/**
* @param Response $response
*
* {@inheritdoc}
*/
protected function failureDescription($response): string
{
return 'the Response '.$this->toString();
}
/**
* @param Response $response
*
* {@inheritdoc}
*/
protected function additionalFailureDescription($response): string
{
return (string) $response;
}
}

View File

@@ -0,0 +1,56 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Test\Constraint;
use PHPUnit\Framework\Constraint\Constraint;
use Symfony\Component\HttpFoundation\Response;
final class ResponseIsUnprocessable extends Constraint
{
/**
* {@inheritdoc}
*/
public function toString(): string
{
return 'is unprocessable';
}
/**
* @param Response $other
*
* {@inheritdoc}
*/
protected function matches($other): bool
{
return Response::HTTP_UNPROCESSABLE_ENTITY === $other->getStatusCode();
}
/**
* @param Response $other
*
* {@inheritdoc}
*/
protected function failureDescription($other): string
{
return 'the Response '.$this->toString();
}
/**
* @param Response $other
*
* {@inheritdoc}
*/
protected function additionalFailureDescription($other): string
{
return (string) $other;
}
}

Some files were not shown because too many files have changed in this diff Show More