Update to laravel 7

This commit is contained in:
KodeStar
2022-03-10 11:54:29 +00:00
parent 61a5a1a8b0
commit f9a19fce91
7170 changed files with 274189 additions and 283773 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.
*
@@ -44,15 +47,13 @@ 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)
{
$index = 0;
$parts = HeaderUtils::split((string) $headerValue, ',;=');
$parts = HeaderUtils::split($headerValue ?? '', ',;=');
return new self(array_map(function ($subParts) use (&$index) {
$part = array_shift($subParts);
@@ -78,11 +79,9 @@ class AcceptHeader
/**
* Tests if header has given value.
*
* @param string $value
*
* @return bool
*/
public function has($value)
public function has(string $value)
{
return isset($this->items[$value]);
}
@@ -90,11 +89,9 @@ class AcceptHeader
/**
* Returns given value's item, if exists.
*
* @param string $value
*
* @return AcceptHeaderItem|null
*/
public function get($value)
public function get(string $value)
{
return $this->items[$value] ?? $this->items[explode('/', $value)[0].'/*'] ?? $this->items['*/*'] ?? $this->items['*'] ?? null;
}
@@ -127,11 +124,9 @@ class AcceptHeader
/**
* Filters items on their value using given regex.
*
* @param string $pattern
*
* @return self
*/
public function filter($pattern)
public function filter(string $pattern)
{
return new self(array_filter($this->items, function (AcceptHeaderItem $item) use ($pattern) {
return preg_match($pattern, $item->getValue());
@@ -153,7 +148,7 @@ class AcceptHeader
/**
* Sorts items by descending quality.
*/
private function sort()
private function sort(): void
{
if (!$this->sorted) {
uasort($this->items, function (AcceptHeaderItem $a, AcceptHeaderItem $b) {

View File

@@ -34,13 +34,11 @@ 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)
{
$parts = HeaderUtils::split($itemValue, ';=');
$parts = HeaderUtils::split($itemValue ?? '', ';=');
$part = array_shift($parts);
$attributes = HeaderUtils::combine($parts);
@@ -66,11 +64,9 @@ class AcceptHeaderItem
/**
* Set the item value.
*
* @param string $value
*
* @return $this
*/
public function setValue($value)
public function setValue(string $value)
{
$this->value = $value;
@@ -90,11 +86,9 @@ class AcceptHeaderItem
/**
* Set the item quality.
*
* @param float $quality
*
* @return $this
*/
public function setQuality($quality)
public function setQuality(float $quality)
{
$this->quality = $quality;
@@ -114,11 +108,9 @@ class AcceptHeaderItem
/**
* Set the item index.
*
* @param int $index
*
* @return $this
*/
public function setIndex($index)
public function setIndex(int $index)
{
$this->index = $index;
@@ -138,11 +130,9 @@ class AcceptHeaderItem
/**
* Tests if an attribute exists.
*
* @param string $name
*
* @return bool
*/
public function hasAttribute($name)
public function hasAttribute(string $name)
{
return isset($this->attributes[$name]);
}
@@ -150,14 +140,13 @@ class AcceptHeaderItem
/**
* Returns an attribute by its name.
*
* @param string $name
* @param mixed $default
* @param mixed $default
*
* @return mixed
*/
public function getAttribute($name, $default = null)
public function getAttribute(string $name, $default = null)
{
return isset($this->attributes[$name]) ? $this->attributes[$name] : $default;
return $this->attributes[$name] ?? $default;
}
/**
@@ -173,17 +162,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)
{
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

@@ -65,25 +65,26 @@ class BinaryFileResponse extends Response
* @param bool $autoLastModified Whether the Last-Modified header should be automatically set
*
* @return static
*
* @deprecated since Symfony 5.2, use __construct() instead.
*/
public static function create($file = null, $status = 200, $headers = [], $public = true, $contentDisposition = null, $autoEtag = false, $autoLastModified = true)
public static function create($file = null, int $status = 200, array $headers = [], bool $public = true, string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true)
{
trigger_deprecation('symfony/http-foundation', '5.2', 'The "%s()" method is deprecated, use "new %s()" instead.', __METHOD__, static::class);
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
* @param \SplFileInfo|string $file The file to stream
*
* @return $this
*
* @throws FileException
*/
public function setFile($file, $contentDisposition = null, $autoEtag = false, $autoLastModified = true)
public function setFile($file, string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true)
{
if (!$file instanceof File) {
if ($file instanceof \SplFileInfo) {
@@ -117,7 +118,7 @@ class BinaryFileResponse extends Response
/**
* Gets the file.
*
* @return File The file to stream
* @return File
*/
public function getFile()
{
@@ -126,6 +127,8 @@ class BinaryFileResponse extends Response
/**
* Automatically sets the Last-Modified header according the file modification date.
*
* @return $this
*/
public function setAutoLastModified()
{
@@ -136,6 +139,8 @@ class BinaryFileResponse extends Response
/**
* Automatically sets the ETag header according to the checksum of the file.
*
* @return $this
*/
public function setAutoEtag()
{
@@ -153,13 +158,13 @@ class BinaryFileResponse extends Response
*
* @return $this
*/
public function setContentDisposition($disposition, $filename = '', $filenameFallback = '')
public function setContentDisposition(string $disposition, string $filename = '', string $filenameFallback = '')
{
if ('' === $filename) {
$filename = $this->file->getFilename();
}
if ('' === $filenameFallback && (!preg_match('/^[\x20-\x7e]*$/', $filename) || false !== strpos($filename, '%'))) {
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) {
@@ -204,7 +209,7 @@ class BinaryFileResponse extends Response
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,10 +222,10 @@ class BinaryFileResponse extends Response
}
if ('x-accel-redirect' === strtolower($type)) {
// Do X-Accel-Mapping substitutions.
// @link http://wiki.nginx.org/X-accel#X-Accel-Redirect
// @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) {
list($pathPrefix, $location) = $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
@@ -234,33 +239,36 @@ class BinaryFileResponse extends Response
$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) + [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 (0 !== $start || $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);
}
}
}
}
@@ -269,7 +277,7 @@ class BinaryFileResponse extends Response
return $this;
}
private function hasValidIfRangeHeader($header)
private function hasValidIfRangeHeader(?string $header): bool
{
if ($this->getEtag() === $header) {
return true;
@@ -283,8 +291,6 @@ class BinaryFileResponse extends Response
}
/**
* Sends the file.
*
* {@inheritdoc}
*/
public function sendContent()
@@ -297,15 +303,15 @@ class BinaryFileResponse extends Response
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);
fclose($out);
fclose($file);
if ($this->deleteFileAfterSend && file_exists($this->file->getPathname())) {
if ($this->deleteFileAfterSend && is_file($this->file->getPathname())) {
unlink($this->file->getPathname());
}
@@ -317,17 +323,17 @@ class BinaryFileResponse extends Response
*
* @throws \LogicException when the content is not null
*/
public function setContent($content)
public function setContent(?string $content)
{
if (null !== $content) {
throw new \LogicException('The content cannot be set on a BinaryFileResponse instance.');
}
return $this;
}
/**
* {@inheritdoc}
*
* @return false
*/
public function getContent()
{
@@ -343,14 +349,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 = true)
public function deleteFileAfterSend(bool $shouldDelete = true)
{
$this->deleteFileAfterSend = $shouldDelete;

View File

@@ -1,6 +1,80 @@
CHANGELOG
=========
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
-----
@@ -78,7 +152,7 @@ CHANGELOG
-----
* 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

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;
private $secureDefault = false;
const SAMESITE_NONE = 'none';
const SAMESITE_LAX = 'lax';
const SAMESITE_STRICT = 'strict';
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)
{
$data = [
'expires' => 0,
@@ -60,8 +62,9 @@ class Cookie
$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'])) {
if (isset($data['max-age']) && ($data['max-age'] > 0 || $data['expires'] > time())) {
$data['expires'] = time() + (int) $data['max-age'];
}
@@ -86,14 +89,10 @@ class Cookie
*
* @throws \InvalidArgumentException
*/
public function __construct(string $name, string $value = null, $expire = 0, ?string $path = '/', string $domain = null, ?bool $secure = false, bool $httpOnly = true, bool $raw = false, string $sameSite = null)
public function __construct(string $name, string $value = null, $expire = 0, ?string $path = '/', string $domain = null, bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = 'lax')
{
if (9 > \func_num_args()) {
@trigger_error(sprintf('The default value of the "$secure" and "$samesite" arguments of "%s"\'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.', __METHOD__), E_USER_DEPRECATED);
}
// 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));
}
@@ -101,6 +100,65 @@ 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.
*
* @return static
*/
public function withValue(?string $value): self
{
$cookie = clone $this;
$cookie->value = $value;
return $cookie;
}
/**
* Creates a cookie copy with a new domain that the cookie is available to.
*
* @return static
*/
public function withDomain(?string $domain): self
{
$cookie = clone $this;
$cookie->domain = $domain;
return $cookie;
}
/**
* Creates a cookie copy with a new time the cookie expires.
*
* @param int|string|\DateTimeInterface $expire
*
* @return static
*/
public function withExpires($expire = 0): self
{
$cookie = clone $this;
$cookie->expire = self::expiresTimestamp($expire);
return $cookie;
}
/**
* Converts expires formats to a unix timestamp.
*
* @param int|string|\DateTimeInterface $expire
*/
private static function expiresTimestamp($expire = 0): int
{
// convert expiration time to a Unix timestamp
if ($expire instanceof \DateTimeInterface) {
$expire = $expire->format('U');
@@ -112,15 +170,72 @@ 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 = $secure;
$this->httpOnly = $httpOnly;
$this->raw = $raw;
return 0 < $expire ? (int) $expire : 0;
}
/**
* Creates a cookie copy with a new path on the server in which the cookie will be available on.
*
* @return static
*/
public function withPath(string $path): self
{
$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.
*
* @return static
*/
public function withSecure(bool $secure = true): self
{
$cookie = clone $this;
$cookie->secure = $secure;
return $cookie;
}
/**
* Creates a cookie copy that be accessible only through the HTTP protocol.
*
* @return static
*/
public function withHttpOnly(bool $httpOnly = true): self
{
$cookie = clone $this;
$cookie->httpOnly = $httpOnly;
return $cookie;
}
/**
* Creates a cookie copy that uses no url encoding.
*
* @return static
*/
public function withRaw(bool $raw = true): self
{
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.
*
* @return static
*/
public function withSameSite(?string $sameSite): self
{
if ('' === $sameSite) {
$sameSite = null;
} elseif (null !== $sameSite) {
@@ -131,17 +246,26 @@ class Cookie
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
* @return string
*/
public function __toString()
{
$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=0';

View File

@@ -0,0 +1,19 @@
<?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 user sends a malformed request.
*/
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 exists. 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

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

View File

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

View File

@@ -15,6 +15,6 @@ class UnexpectedTypeException extends FileException
{
public function __construct($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

@@ -47,13 +47,17 @@ 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
* @return string|null
*
* @see MimeTypes
* @see getMimeType()
*/
public function guessExtension()
{
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 MimeTypes::getDefault()->getExtensions($this->getMimeType())[0] ?? null;
}
@@ -64,34 +68,38 @@ class File extends \SplFileInfo
* 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")
* @return string|null
*
* @see MimeTypes
*/
public function getMimeType()
{
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 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
* @return self
*
* @throws FileException if the target file could not be created
*/
public function move($directory, $name = null)
public function move(string $directory, string $name = null)
{
$target = $this->getTargetFile($directory, $name);
set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; });
$renamed = rename($this->getPathname(), $target);
restore_error_handler();
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)));
throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s).', $this->getPathname(), $target, strip_tags($error)));
}
@chmod($target, 0666 & ~umask());
@@ -99,14 +107,28 @@ 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;
}
/**
* @return self
*/
protected function getTargetFile(string $directory, string $name = null)
{
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));
@@ -117,11 +139,9 @@ class File extends \SplFileInfo
/**
* Returns locale independent base name of the given path.
*
* @param string $name The new file name
*
* @return string containing
* @return string
*/
protected function getName($name)
protected function getName(string $name)
{
$originalName = str_replace('\\', '/', $name);
$pos = strrpos($originalName, '/');

View File

@@ -1,100 +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\Mime\MimeTypes;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" instead.', ExtensionGuesser::class, MimeTypes::class), E_USER_DEPRECATED);
/**
* 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.
*
* @deprecated since Symfony 4.3, use {@link MimeTypes} instead
*/
class ExtensionGuesser implements ExtensionGuesserInterface
{
/**
* The singleton instance.
*
* @var ExtensionGuesser
*/
private static $instance = null;
/**
* All registered ExtensionGuesserInterface instances.
*
* @var array
*/
protected $guessers = [];
/**
* 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.
*/
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,31 +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\Mime\MimeTypesInterface;
/**
* Guesses the file extension corresponding to a given mime type.
*
* @deprecated since Symfony 4.3, use {@link MimeTypesInterface} instead
*/
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,104 +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\AccessDeniedException;
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
use Symfony\Component\Mime\FileBinaryMimeTypeGuesser as NewFileBinaryMimeTypeGuesser;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" instead.', FileBinaryMimeTypeGuesser::class, NewFileBinaryMimeTypeGuesser::class), E_USER_DEPRECATED);
/**
* Guesses the mime type with the binary "file" (only available on *nix).
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated since Symfony 4.3, use {@link NewFileBinaryMimeTypeGuesser} instead
*/
class FileBinaryMimeTypeGuesser implements MimeTypeGuesserInterface
{
private $cmd;
/**
* 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(string $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()
{
static $supported = null;
if (null !== $supported) {
return $supported;
}
if ('\\' === \DIRECTORY_SEPARATOR || !\function_exists('passthru') || !\function_exists('escapeshellarg')) {
return $supported = false;
}
ob_start();
passthru('command -v file', $exitStatus);
$binPath = trim(ob_get_clean());
return $supported = 0 === $exitStatus && '' !== $binPath;
}
/**
* {@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,74 +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\AccessDeniedException;
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
use Symfony\Component\Mime\FileinfoMimeTypeGuesser as NewFileinfoMimeTypeGuesser;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" instead.', FileinfoMimeTypeGuesser::class, NewFileinfoMimeTypeGuesser::class), E_USER_DEPRECATED);
/**
* Guesses the mime type using the PECL extension FileInfo.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated since Symfony 4.3, use {@link NewFileinfoMimeTypeGuesser} instead
*/
class FileinfoMimeTypeGuesser implements MimeTypeGuesserInterface
{
private $magicFile;
/**
* @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(string $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,826 +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\Mime\MimeTypes;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" instead.', MimeTypeExtensionGuesser::class, MimeTypes::class), E_USER_DEPRECATED);
/**
* Provides a best-guess mapping of mime type to file extension.
*
* @deprecated since Symfony 4.3, use {@link MimeTypes} instead
*/
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 2019-01-14.
*
* @see https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types
*/
protected $defaultExtensions = [
'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-zip-compressed' => 'zip',
'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' => 'm4a',
'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-hx-aac-adts' => 'aac',
'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',
'font/collection' => 'ttc',
'font/otf' => 'otf',
'font/ttf' => 'ttf',
'font/woff' => 'woff',
'font/woff2' => 'woff2',
'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.djvu' => 'djvu',
'image/vnd.dvb.subtitle' => 'sub',
'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.mcurl' => 'mcurl',
'text/vnd.curl.scurl' => 'scurl',
'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-java-source' => 'java',
'text/x-nfo' => 'nfo',
'text/x-opml' => 'opml',
'text/x-pascal' => 'p',
'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)
{
if (isset($this->defaultExtensions[$mimeType])) {
return $this->defaultExtensions[$mimeType];
}
$lcMimeType = strtolower($mimeType);
return isset($this->defaultExtensions[$lcMimeType]) ? $this->defaultExtensions[$lcMimeType] : null;
}
}

View File

@@ -1,136 +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\AccessDeniedException;
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
use Symfony\Component\Mime\MimeTypes;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" instead.', MimeTypeGuesser::class, MimeTypes::class), E_USER_DEPRECATED);
/**
* 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 = [];
/**
* 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()
{
$this->register(new FileBinaryMimeTypeGuesser());
$this->register(new FileinfoMimeTypeGuesser());
}
/**
* Registers a new mime type guesser.
*
* When guessing, this guesser is preferred over previously registered ones.
*/
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);
}
foreach ($this->guessers as $guesser) {
if (null !== $mimeType = $guesser->guess($path)) {
return $mimeType;
}
}
if (2 === \count($this->guessers) && !FileBinaryMimeTypeGuesser::isSupported() && !FileinfoMimeTypeGuesser::isSupported()) {
throw new \LogicException('Unable to guess the mime type as no guessers are available (Did you enable the php_fileinfo extension?)');
}
}
}

View File

@@ -1,38 +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\AccessDeniedException;
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
use Symfony\Component\Mime\MimeTypesInterface;
/**
* Guesses the mime type of a file.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated since Symfony 4.3, use {@link MimeTypesInterface} instead
*/
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

@@ -20,7 +20,10 @@ class Stream extends File
{
/**
* {@inheritdoc}
*
* @return int|false
*/
#[\ReturnTypeWillChange]
public function getSize()
{
return false;

View File

@@ -31,7 +31,7 @@ use Symfony\Component\Mime\MimeTypes;
*/
class UploadedFile extends File
{
private $test = false;
private $test;
private $originalName;
private $mimeType;
private $error;
@@ -60,21 +60,14 @@ class UploadedFile extends File
* @throws FileException If file_uploads is disabled
* @throws FileNotFoundException If the file does not exist
*/
public function __construct(string $path, string $originalName, string $mimeType = null, int $error = null, $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';
if (4 < \func_num_args() ? !\is_bool($test) : null !== $error && @filesize($path) === $error) {
@trigger_error(sprintf('Passing a size as 4th argument to the constructor of "%s" is deprecated since Symfony 4.1.', __CLASS__), E_USER_DEPRECATED);
$error = $test;
$test = 5 < \func_num_args() ? func_get_arg(5) : false;
}
$this->error = $error ?: UPLOAD_ERR_OK;
$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);
}
/**
@@ -83,7 +76,7 @@ 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
* @return string
*/
public function getClientOriginalName()
{
@@ -96,11 +89,11 @@ 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
* @return string
*/
public function getClientOriginalExtension()
{
return pathinfo($this->originalName, PATHINFO_EXTENSION);
return pathinfo($this->originalName, \PATHINFO_EXTENSION);
}
/**
@@ -112,7 +105,7 @@ 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
* @return string
*
* @see getMimeType()
*/
@@ -133,40 +126,27 @@ 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
* @return string|null
*
* @see guessExtension()
* @see getClientMimeType()
*/
public function guessClientExtension()
{
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 MimeTypes::getDefault()->getExtensions($this->getClientMimeType())[0] ?? null;
}
/**
* 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.
*
* @deprecated since Symfony 4.1, use getSize() instead.
*
* @return int|null The file sizes
*/
public function getClientSize()
{
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1. Use getSize() instead.', __METHOD__), E_USER_DEPRECATED);
return $this->getSize();
}
/**
* Returns the upload error.
*
* 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
* @return int
*/
public function getError()
{
@@ -174,13 +154,13 @@ class UploadedFile extends File
}
/**
* Returns whether the file was uploaded successfully.
* Returns whether the file has been uploaded with HTTP and no error occurred.
*
* @return bool True if the file has been uploaded with HTTP and no error occurred
* @return bool
*/
public function isValid()
{
$isOk = UPLOAD_ERR_OK === $this->error;
$isOk = \UPLOAD_ERR_OK === $this->error;
return $this->test ? $isOk : $isOk && is_uploaded_file($this->getPathname());
}
@@ -188,14 +168,11 @@ 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
* @return 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)
{
if ($this->isValid()) {
if ($this->test) {
@@ -205,10 +182,13 @@ class UploadedFile extends File
$target = $this->getTargetFile($directory, $name);
set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; });
$moved = move_uploaded_file($this->getPathname(), $target);
restore_error_handler();
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)));
throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s).', $this->getPathname(), $target, strip_tags($error)));
}
@chmod($target, 0666 & ~umask());
@@ -217,19 +197,19 @@ class UploadedFile extends File
}
switch ($this->error) {
case UPLOAD_ERR_INI_SIZE:
case \UPLOAD_ERR_INI_SIZE:
throw new IniSizeFileException($this->getErrorMessage());
case UPLOAD_ERR_FORM_SIZE:
case \UPLOAD_ERR_FORM_SIZE:
throw new FormSizeFileException($this->getErrorMessage());
case UPLOAD_ERR_PARTIAL:
case \UPLOAD_ERR_PARTIAL:
throw new PartialFileException($this->getErrorMessage());
case UPLOAD_ERR_NO_FILE:
case \UPLOAD_ERR_NO_FILE:
throw new NoFileException($this->getErrorMessage());
case UPLOAD_ERR_CANT_WRITE:
case \UPLOAD_ERR_CANT_WRITE:
throw new CannotWriteFileException($this->getErrorMessage());
case UPLOAD_ERR_NO_TMP_DIR:
case \UPLOAD_ERR_NO_TMP_DIR:
throw new NoTmpDirFileException($this->getErrorMessage());
case UPLOAD_ERR_EXTENSION:
case \UPLOAD_ERR_EXTENSION:
throw new ExtensionFileException($this->getErrorMessage());
}
@@ -239,26 +219,39 @@ class UploadedFile extends File
/**
* 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()
{
$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);
}
/**
* Returns the given size from an ini value in bytes.
*
* @return int|float Returns float if size > PHP_INT_MAX
*/
private static function parseFilesize(string $size)
{
if ('' === $size) {
return 0;
}
$max = ltrim($iniMax, '+');
if (0 === strpos($max, '0x')) {
$size = strtolower($size);
$max = ltrim($size, '+');
if (str_starts_with($max, '0x')) {
$max = \intval($max, 16);
} elseif (0 === strpos($max, '0')) {
} 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;
@@ -274,23 +267,23 @@ class UploadedFile extends File
/**
* Returns an informative upload error message.
*
* @return string The error message regarding the specified error code
* @return string
*/
public function getErrorMessage()
{
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.',
\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 = UPLOAD_ERR_INI_SIZE === $errorCode ? 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,10 +21,10 @@ use Symfony\Component\HttpFoundation\File\UploadedFile;
*/
class FileBag extends ParameterBag
{
private static $fileKeys = ['error', 'name', 'size', 'tmp_name', 'type'];
private const FILE_KEYS = ['error', 'name', 'size', 'tmp_name', 'type'];
/**
* @param array $parameters An array of HTTP files
* @param array|UploadedFile[] $parameters An array of HTTP files
*/
public function __construct(array $parameters = [])
{
@@ -43,7 +43,7 @@ class FileBag extends ParameterBag
/**
* {@inheritdoc}
*/
public function set($key, $value)
public function set(string $key, $value)
{
if (!\is_array($value) && !$value instanceof UploadedFile) {
throw new \InvalidArgumentException('An uploaded file must be an array or an instance of UploadedFile.');
@@ -67,7 +67,7 @@ class FileBag extends ParameterBag
*
* @param array|UploadedFile $file A (multi-dimensional) array of uploaded file information
*
* @return UploadedFile[]|UploadedFile|null A (multi-dimensional) array of UploadedFile instances
* @return UploadedFile[]|UploadedFile|null
*/
protected function convertFileInformation($file)
{
@@ -76,21 +76,19 @@ class FileBag extends ParameterBag
}
$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['error'], false);
}
if (self::FILE_KEYS == $keys) {
if (\UPLOAD_ERR_NO_FILE == $file['error']) {
$file = null;
} else {
$file = array_map([$this, 'convertFileInformation'], $file);
if (array_keys($keys) === $keys) {
$file = array_filter($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);
}
}
@@ -111,21 +109,19 @@ class FileBag extends ParameterBag
*
* @return array
*/
protected function fixPhpFilesArray($data)
protected function fixPhpFilesArray(array $data)
{
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]);
}

View File

@@ -15,15 +15,20 @@ 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 const UPPER = '_ABCDEFGHIJKLMNOPQRSTUVWXYZ';
protected const LOWER = '-abcdefghijklmnopqrstuvwxyz';
/**
* @var array<string, list<string|null>>
*/
protected $headers = [];
protected $cacheControl = [];
/**
* @param array $headers An array of HTTP headers
*/
public function __construct(array $headers = [])
{
foreach ($headers as $key => $values) {
@@ -34,7 +39,7 @@ class HeaderBag implements \IteratorAggregate, \Countable
/**
* Returns the headers as a string.
*
* @return string The headers
* @return string
*/
public function __toString()
{
@@ -58,17 +63,23 @@ 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)
{
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()
{
@@ -77,8 +88,6 @@ class HeaderBag implements \IteratorAggregate, \Countable
/**
* Replaces the current HTTP headers by a new set.
*
* @param array $headers An array of HTTP headers
*/
public function replace(array $headers = [])
{
@@ -88,8 +97,6 @@ class HeaderBag implements \IteratorAggregate, \Countable
/**
* Adds new headers the current HTTP headers set.
*
* @param array $headers An array of HTTP headers
*/
public function add(array $headers)
{
@@ -99,44 +106,34 @@ class HeaderBag implements \IteratorAggregate, \Countable
}
/**
* Returns a header value by name.
* Returns the first header by name or the default one.
*
* @param string $key The header name
* @param string|null $default The default value
* @param bool $first Whether to return the first value or all header values
*
* @return string|string[]|null The first header value or default value if $first is true, an array of values otherwise
* @return string|null
*/
public function get($key, $default = null, $first = true)
public function get(string $key, string $default = null)
{
$key = str_replace('_', '-', strtolower($key));
$headers = $this->all();
$headers = $this->all($key);
if (!\array_key_exists($key, $headers)) {
if (null === $default) {
return $first ? null : [];
}
return $first ? $default : [$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|string[] $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, $values, bool $replace = true)
{
$key = str_replace('_', '-', strtolower($key));
$key = strtr($key, self::UPPER, self::LOWER);
if (\is_array($values)) {
$values = array_values($values);
@@ -162,36 +159,29 @@ class HeaderBag implements \IteratorAggregate, \Countable
/**
* Returns true if the HTTP header is defined.
*
* @param string $key The HTTP header
*
* @return bool true if the parameter exists, false otherwise
* @return bool
*/
public function has($key)
public function has(string $key)
{
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
* @return bool
*/
public function contains($key, $value)
public function contains(string $key, string $value)
{
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]);
@@ -203,21 +193,18 @@ class HeaderBag implements \IteratorAggregate, \Countable
/**
* Returns the HTTP header value converted to a date.
*
* @param string $key The parameter key
* @param \DateTime $default The default value
*
* @return \DateTime|null The parsed DateTime or the default value if the header does not exist
* @return \DateTimeInterface|null
*
* @throws \RuntimeException When the HTTP header is not parseable
*/
public function getDate($key, \DateTime $default = null)
public function getDate(string $key, \DateTime $default = null)
{
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;
@@ -226,10 +213,9 @@ 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
* @param bool|string $value The Cache-Control directive value
*/
public function addCacheControlDirective($key, $value = true)
public function addCacheControlDirective(string $key, $value = true)
{
$this->cacheControl[$key] = $value;
@@ -239,11 +225,9 @@ 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
* @return bool
*/
public function hasCacheControlDirective($key)
public function hasCacheControlDirective(string $key)
{
return \array_key_exists($key, $this->cacheControl);
}
@@ -251,21 +235,17 @@ class HeaderBag implements \IteratorAggregate, \Countable
/**
* Returns a Cache-Control directive value by name.
*
* @param string $key The directive name
*
* @return mixed|null The directive value if defined, null otherwise
* @return bool|string|null
*/
public function getCacheControlDirective($key)
public function getCacheControlDirective(string $key)
{
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]);
@@ -275,8 +255,9 @@ class HeaderBag implements \IteratorAggregate, \Countable
/**
* Returns an iterator for headers.
*
* @return \ArrayIterator An \ArrayIterator instance
* @return \ArrayIterator<string, list<string|null>>
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
return new \ArrayIterator($this->headers);
@@ -285,8 +266,9 @@ class HeaderBag implements \IteratorAggregate, \Countable
/**
* Returns the number of headers.
*
* @return int The number of headers
* @return int
*/
#[\ReturnTypeWillChange]
public function count()
{
return \count($this->headers);
@@ -302,11 +284,9 @@ class HeaderBag implements \IteratorAggregate, \Countable
/**
* 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
* @return array
*/
protected function parseCacheControl($header)
protected function parseCacheControl(string $header)
{
$parts = HeaderUtils::split($header, ',=');

View File

@@ -36,7 +36,6 @@ class HeaderUtils
* HeaderUtils::split("da, en-gb;q=0.8", ",;")
* // => ['da'], ['en-gb', 'q=0.8']]
*
* @param string $header HTTP header value
* @param string $separators List of characters to split on, ordered by
* precedence, e.g. ",", ";=", or ",;="
*
@@ -63,7 +62,7 @@ class HeaderUtils
\s*
(?<separator>['.$quotedSeparators.'])
\s*
/x', trim($header), $matches, PREG_SET_ORDER);
/x', trim($header), $matches, \PREG_SET_ORDER);
return self::groupParts($matches, $separators);
}
@@ -147,7 +146,7 @@ class HeaderUtils
}
/**
* Generates a HTTP Content-Disposition field-value.
* Generates an HTTP Content-Disposition field-value.
*
* @param string $disposition One of "inline" or "attachment"
* @param string $filename A unicode string
@@ -155,8 +154,6 @@ class HeaderUtils
* 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
@@ -177,12 +174,12 @@ class HeaderUtils
}
// percent characters aren't safe in fallback.
if (false !== strpos($filenameFallback, '%')) {
if (str_contains($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, '\\')) {
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.');
}
@@ -194,17 +191,81 @@ class HeaderUtils
return $disposition.'; '.self::toString($params, ';');
}
private static function groupParts(array $matches, string $separators): array
/**
* 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 (isset($match['separator']) && $match['separator'] === $separator) {
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;
}
}
@@ -212,12 +273,19 @@ class HeaderUtils
$parts = [];
if ($partSeparators) {
foreach ($partMatches as $matches) {
$parts[] = self::groupParts($matches, $partSeparators);
$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,113 @@
<?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
*
* @return string|int|float|bool|null
*/
public function get(string $key, $default = null)
{
if (null !== $default && !is_scalar($default) && !(\is_object($default) && method_exists($default, '__toString'))) {
trigger_deprecation('symfony/http-foundation', '5.1', 'Passing a non-scalar value as 2nd argument to "%s()" is deprecated, pass a scalar or null instead.', __METHOD__);
}
$value = parent::get($key, $this);
if (null !== $value && $this !== $value && !is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) {
trigger_deprecation('symfony/http-foundation', '5.1', 'Retrieving a non-string value from "%s()" is deprecated, and will throw a "%s" exception in Symfony 6.0, use "%s::all($key)" instead.', __METHOD__, BadRequestException::class, __CLASS__);
}
return $this === $value ? $default : $value;
}
/**
* {@inheritdoc}
*/
public function all(string $key = null): array
{
return parent::all($key);
}
/**
* 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, $value)
{
if (null !== $value && !is_scalar($value) && !\is_array($value) && !method_exists($value, '__toString')) {
trigger_deprecation('symfony/http-foundation', '5.1', 'Passing "%s" as a 2nd Argument to "%s()" is deprecated, pass a scalar, array, or null instead.', get_debug_type($value), __METHOD__);
}
$this->parameters[$key] = $value;
}
/**
* {@inheritdoc}
*/
public function filter(string $key, $default = null, int $filter = \FILTER_DEFAULT, $options = [])
{
$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))) {
trigger_deprecation('symfony/http-foundation', '5.1', 'Filtering an array value with "%s()" without passing the FILTER_REQUIRE_ARRAY or FILTER_FORCE_ARRAY flag is deprecated', __METHOD__);
if (!isset($options['flags'])) {
$options['flags'] = \FILTER_REQUIRE_ARRAY;
}
}
if ((\FILTER_CALLBACK & $filter) && !(($options['options'] ?? null) instanceof \Closure)) {
trigger_deprecation('symfony/http-foundation', '5.2', 'Not passing a Closure together with FILTER_CALLBACK to "%s()" is deprecated. Wrap your filter in a closure instead.', __METHOD__);
// 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

@@ -30,13 +30,18 @@ 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)
* @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
* @return bool
*/
public static function checkIp($requestIp, $ips)
public static function checkIp(?string $requestIp, $ips)
{
if (null === $requestIp) {
trigger_deprecation('symfony/http-foundation', '5.4', 'Passing null as $requestIp to "%s()" is deprecated, pass an empty string instead.', __METHOD__);
return false;
}
if (!\is_array($ips)) {
$ips = [$ips];
}
@@ -56,27 +61,32 @@ 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)
{
if (null === $requestIp) {
trigger_deprecation('symfony/http-foundation', '5.4', 'Passing null as $requestIp to "%s()" is deprecated, pass an empty string instead.', __METHOD__);
return false;
}
$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 ('0' === $netmask) {
return self::$checkedIps[$cacheKey] = filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
return self::$checkedIps[$cacheKey] = filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4);
}
if ($netmask < 0 || $netmask > 32) {
@@ -102,15 +112,20 @@ 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
* @param string $ip IPv6 address or subnet in CIDR notation
*
* @return bool Whether the IP is valid
* @return bool
*
* @throws \RuntimeException When IPV6 support is not enabled
*/
public static function checkIp6($requestIp, $ip)
public static function checkIp6(?string $requestIp, string $ip)
{
if (null === $requestIp) {
trigger_deprecation('symfony/http-foundation', '5.4', 'Passing null as $requestIp to "%s()" is deprecated, pass an empty string instead.', __METHOD__);
return false;
}
$cacheKey = $requestIp.'-'.$ip;
if (isset(self::$checkedIps[$cacheKey])) {
return self::$checkedIps[$cacheKey];
@@ -120,8 +135,8 @@ class IpUtils
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);
if (str_contains($ip, '/')) {
[$address, $netmask] = explode('/', $ip, 2);
if ('0' === $netmask) {
return (bool) unpack('n*', @inet_pton($address));
@@ -145,7 +160,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;
}
@@ -153,4 +168,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,7 +29,7 @@ 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;
@@ -43,6 +43,10 @@ class JsonResponse extends Response
{
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 +59,39 @@ class JsonResponse extends Response
*
* Example:
*
* return JsonResponse::create($data, 200)
* return JsonResponse::create(['key' => 'value'])
* ->setSharedMaxAge(300);
*
* @param mixed $data The json response data
* @param mixed $data The JSON response data
* @param int $status The response status code
* @param array $headers An array of response headers
*
* @return static
*
* @deprecated since Symfony 5.1, use __construct() instead.
*/
public static function create($data = null, $status = 200, $headers = [])
public static function create($data = null, int $status = 200, array $headers = [])
{
trigger_deprecation('symfony/http-foundation', '5.1', 'The "%s()" method is deprecated, use "new %s()" instead.', __METHOD__, static::class);
return new static($data, $status, $headers);
}
/**
* Make easier the creation of JsonResponse from raw json.
* Factory method for chainability.
*
* Example:
*
* return JsonResponse::fromJsonString('{"key": "value"}')
* ->setSharedMaxAge(300);
*
* @param string $data The JSON response string
* @param int $status The response status code
* @param array $headers An array of response headers
*
* @return static
*/
public static function fromJsonString($data = null, $status = 200, $headers = [])
public static function fromJsonString(string $data, int $status = 200, array $headers = [])
{
return new static($data, $status, $headers, true);
}
@@ -86,10 +105,10 @@ class JsonResponse extends Response
*
* @throws \InvalidArgumentException When the callback name is not valid
*/
public function setCallback($callback = null)
public function setCallback(string $callback = null)
{
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>
@@ -115,13 +134,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)
{
$this->data = $json;
@@ -142,17 +157,17 @@ class JsonResponse extends Response
try {
$data = json_encode($data, $this->encodingOptions);
} catch (\Exception $e) {
if ('Exception' === \get_class($e) && 0 === strpos($e->getMessage(), 'Failed calling ')) {
if ('Exception' === \get_class($e) && str_starts_with($e->getMessage(), 'Failed calling ')) {
throw $e->getPrevious() ?: $e;
}
throw $e;
}
if (\PHP_VERSION_ID >= 70300 && (JSON_THROW_ON_ERROR & $this->encodingOptions)) {
if (\PHP_VERSION_ID >= 70300 && (\JSON_THROW_ON_ERROR & $this->encodingOptions)) {
return $this->setJson($data);
}
if (JSON_ERROR_NONE !== json_last_error()) {
if (\JSON_ERROR_NONE !== json_last_error()) {
throw new \InvalidArgumentException(json_last_error_msg());
}
@@ -172,13 +187,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)
{
$this->encodingOptions = (int) $encodingOptions;
$this->encodingOptions = $encodingOptions;
return $this->setData(json_decode($this->data));
}

View File

@@ -1,4 +1,4 @@
Copyright (c) 2004-2019 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,10 +11,14 @@
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
{
@@ -23,9 +27,6 @@ class ParameterBag implements \IteratorAggregate, \Countable
*/
protected $parameters;
/**
* @param array $parameters An array of parameters
*/
public function __construct(array $parameters = [])
{
$this->parameters = $parameters;
@@ -34,17 +35,29 @@ 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
*
* @return array
*/
public function all()
public function all(/*string $key = null*/)
{
return $this->parameters;
$key = \func_num_args() > 0 ? func_get_arg(0) : null;
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
* @return array
*/
public function keys()
{
@@ -53,8 +66,6 @@ class ParameterBag implements \IteratorAggregate, \Countable
/**
* Replaces the current parameters by a new set.
*
* @param array $parameters An array of parameters
*/
public function replace(array $parameters = [])
{
@@ -63,8 +74,6 @@ class ParameterBag implements \IteratorAggregate, \Countable
/**
* Adds parameters.
*
* @param array $parameters An array of parameters
*/
public function add(array $parameters = [])
{
@@ -74,12 +83,11 @@ class ParameterBag implements \IteratorAggregate, \Countable
/**
* Returns a parameter by name.
*
* @param string $key The key
* @param mixed $default The default value if the parameter key does not exist
* @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, $default = null)
{
return \array_key_exists($key, $this->parameters) ? $this->parameters[$key] : $default;
}
@@ -87,10 +95,9 @@ class ParameterBag implements \IteratorAggregate, \Countable
/**
* Sets a parameter by name.
*
* @param string $key The key
* @param mixed $value The value
* @param mixed $value The value
*/
public function set($key, $value)
public function set(string $key, $value)
{
$this->parameters[$key] = $value;
}
@@ -98,21 +105,17 @@ class ParameterBag implements \IteratorAggregate, \Countable
/**
* Returns true if the parameter is defined.
*
* @param string $key The key
*
* @return bool true if the parameter exists, false otherwise
* @return bool
*/
public function has($key)
public function has(string $key)
{
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]);
}
@@ -120,12 +123,9 @@ class ParameterBag implements \IteratorAggregate, \Countable
/**
* 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
* @return string
*/
public function getAlpha($key, $default = '')
public function getAlpha(string $key, string $default = '')
{
return preg_replace('/[^[:alpha:]]/', '', $this->get($key, $default));
}
@@ -133,12 +133,9 @@ class ParameterBag implements \IteratorAggregate, \Countable
/**
* 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
* @return string
*/
public function getAlnum($key, $default = '')
public function getAlnum(string $key, string $default = '')
{
return preg_replace('/[^[:alnum:]]/', '', $this->get($key, $default));
}
@@ -146,26 +143,20 @@ class ParameterBag implements \IteratorAggregate, \Countable
/**
* 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
* @return string
*/
public function getDigits($key, $default = '')
public function getDigits(string $key, string $default = '')
{
// we need to remove - and + because they're allowed in the filter
return str_replace(['-', '+'], '', $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
* @return int
*/
public function getInt($key, $default = 0)
public function getInt(string $key, int $default = 0)
{
return (int) $this->get($key, $default);
}
@@ -173,29 +164,25 @@ class ParameterBag implements \IteratorAggregate, \Countable
/**
* Returns the parameter value converted to boolean.
*
* @param string $key The parameter key
* @param bool $default The default value if the parameter key does not exist
*
* @return bool The filtered value
* @return bool
*/
public function getBoolean($key, $default = false)
public function getBoolean(string $key, bool $default = false)
{
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 mixed $default Default = null
* @param int $filter FILTER_* constant
* @param mixed $options Filter options
*
* @see http://php.net/manual/en/function.filter-var.php
* @see https://php.net/filter-var
*
* @return mixed
*/
public function filter($key, $default = null, $filter = FILTER_DEFAULT, $options = [])
public function filter(string $key, $default = null, int $filter = \FILTER_DEFAULT, $options = [])
{
$value = $this->get($key, $default);
@@ -206,7 +193,12 @@ class ParameterBag implements \IteratorAggregate, \Countable
// Add a convenience check for arrays.
if (\is_array($value) && !isset($options['flags'])) {
$options['flags'] = FILTER_REQUIRE_ARRAY;
$options['flags'] = \FILTER_REQUIRE_ARRAY;
}
if ((\FILTER_CALLBACK & $filter) && !(($options['options'] ?? null) instanceof \Closure)) {
trigger_deprecation('symfony/http-foundation', '5.2', 'Not passing a Closure together with FILTER_CALLBACK to "%s()" is deprecated. Wrap your filter in a closure instead.', __METHOD__);
// 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);
@@ -215,8 +207,9 @@ class ParameterBag implements \IteratorAggregate, \Countable
/**
* Returns an iterator for parameters.
*
* @return \ArrayIterator An \ArrayIterator instance
* @return \ArrayIterator<string, mixed>
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
return new \ArrayIterator($this->parameters);
@@ -225,8 +218,9 @@ class ParameterBag implements \IteratorAggregate, \Countable
/**
* Returns the number of parameters.
*
* @return int The number of parameters
* @return int
*/
#[\ReturnTypeWillChange]
public function count()
{
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,57 @@
<?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);
if (null === $minimalRateLimit || $rateLimit->getRemainingTokens() < $minimalRateLimit->getRemainingTokens()) {
$minimalRateLimit = $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;
}

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(?string $url, int $status = 302, array $headers = [])
public function __construct(string $url, int $status = 302, array $headers = [])
{
parent::__construct('', $status, $headers);
@@ -50,21 +50,23 @@ class RedirectResponse extends Response
/**
* Factory method for chainability.
*
* @param string $url The url to redirect to
* @param int $status The response status code
* @param array $headers An array of response headers
* @param string $url The URL to redirect to
*
* @return static
*
* @deprecated since Symfony 5.1, use __construct() instead.
*/
public static function create($url = '', $status = 302, $headers = [])
public static function create($url = '', int $status = 302, array $headers = [])
{
trigger_deprecation('symfony/http-foundation', '5.1', 'The "%s()" method is deprecated, use "new %s()" instead.', __METHOD__, static::class);
return new static($url, $status, $headers);
}
/**
* Returns the target URL.
*
* @return string target URL
* @return string
*/
public function getTargetUrl()
{
@@ -74,15 +76,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)
{
if (empty($url)) {
if ('' === $url) {
throw new \InvalidArgumentException('Cannot redirect to an empty URL.');
}
@@ -93,14 +93,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

@@ -54,11 +54,8 @@ class RequestMatcher implements RequestMatcherInterface
private $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(string $path = null, string $host = null, $methods = null, $ips = null, array $attributes = [], $schemes = null, int $port = null)
@@ -87,10 +84,8 @@ class RequestMatcher implements RequestMatcherInterface
/**
* 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;
}
@@ -100,17 +95,15 @@ class RequestMatcher implements RequestMatcherInterface
*
* @param int|null $port The port number to connect to
*/
public function matchPort(int $port = null)
public function matchPort(?int $port)
{
$this->port = $port;
}
/**
* Adds a check for the URL path info.
*
* @param string|null $regexp A Regexp
*/
public function matchPath($regexp)
public function matchPath(?string $regexp)
{
$this->path = $regexp;
}
@@ -120,7 +113,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);
}
@@ -132,7 +125,11 @@ class RequestMatcher implements RequestMatcherInterface
*/
public function matchIps($ips)
{
$this->ips = null !== $ips ? (array) $ips : [];
$ips = null !== $ips ? (array) $ips : [];
$this->ips = array_reduce($ips, static function (array $ips, string $ip) {
return array_merge($ips, preg_split('/\s*,\s*/', $ip));
}, []);
}
/**
@@ -147,11 +144,8 @@ class RequestMatcher implements RequestMatcherInterface
/**
* 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;
}
@@ -170,7 +164,11 @@ class RequestMatcher implements RequestMatcherInterface
}
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;
}
}
@@ -187,7 +185,7 @@ class RequestMatcher implements RequestMatcherInterface
return false;
}
if (IpUtils::checkIp($request->getClientIp(), $this->ips)) {
if (IpUtils::checkIp($request->getClientIp() ?? '', $this->ips)) {
return true;
}

View File

@@ -21,7 +21,7 @@ interface RequestMatcherInterface
/**
* Decides whether the rule(s) implemented by the strategy matches the supplied request.
*
* @return bool true if the request matches, false otherwise
* @return bool
*/
public function matches(Request $request);
}

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.
*
@@ -47,7 +50,7 @@ class RequestStack
public function pop()
{
if (!$this->requests) {
return;
return null;
}
return array_pop($this->requests);
@@ -62,23 +65,35 @@ class RequestStack
}
/**
* 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];
}
/**
* Gets the master request.
*
* @return Request|null
*
* @deprecated since symfony/http-foundation 5.3, use getMainRequest() instead
*/
public function getMasterRequest()
{
trigger_deprecation('symfony/http-foundation', '5.3', '"%s()" is deprecated, use "getMainRequest()" instead.', __METHOD__);
return $this->getMainRequest();
}
/**
* Returns the parent request of the current.
*
@@ -86,7 +101,7 @@ 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.
* If current Request is the main request, it returns null.
*
* @return Request|null
*/
@@ -94,10 +109,20 @@ class RequestStack
{
$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();
}
}

View File

@@ -11,6 +11,9 @@
namespace Symfony\Component\HttpFoundation;
// Help opcache.preload discover always-needed symbols
class_exists(ResponseHeaderBag::class);
/**
* Response represents an HTTP response.
*
@@ -18,77 +21,90 @@ namespace Symfony\Component\HttpFoundation;
*/
class Response
{
const HTTP_CONTINUE = 100;
const HTTP_SWITCHING_PROTOCOLS = 101;
const HTTP_PROCESSING = 102; // RFC2518
const HTTP_EARLY_HINTS = 103; // RFC8297
const HTTP_OK = 200;
const HTTP_CREATED = 201;
const HTTP_ACCEPTED = 202;
const HTTP_NON_AUTHORITATIVE_INFORMATION = 203;
const HTTP_NO_CONTENT = 204;
const HTTP_RESET_CONTENT = 205;
const HTTP_PARTIAL_CONTENT = 206;
const HTTP_MULTI_STATUS = 207; // RFC4918
const HTTP_ALREADY_REPORTED = 208; // RFC5842
const HTTP_IM_USED = 226; // RFC3229
const HTTP_MULTIPLE_CHOICES = 300;
const HTTP_MOVED_PERMANENTLY = 301;
const HTTP_FOUND = 302;
const HTTP_SEE_OTHER = 303;
const HTTP_NOT_MODIFIED = 304;
const HTTP_USE_PROXY = 305;
const HTTP_RESERVED = 306;
const HTTP_TEMPORARY_REDIRECT = 307;
const HTTP_PERMANENTLY_REDIRECT = 308; // RFC7238
const HTTP_BAD_REQUEST = 400;
const HTTP_UNAUTHORIZED = 401;
const HTTP_PAYMENT_REQUIRED = 402;
const HTTP_FORBIDDEN = 403;
const HTTP_NOT_FOUND = 404;
const HTTP_METHOD_NOT_ALLOWED = 405;
const HTTP_NOT_ACCEPTABLE = 406;
const HTTP_PROXY_AUTHENTICATION_REQUIRED = 407;
const HTTP_REQUEST_TIMEOUT = 408;
const HTTP_CONFLICT = 409;
const HTTP_GONE = 410;
const HTTP_LENGTH_REQUIRED = 411;
const HTTP_PRECONDITION_FAILED = 412;
const HTTP_REQUEST_ENTITY_TOO_LARGE = 413;
const HTTP_REQUEST_URI_TOO_LONG = 414;
const HTTP_UNSUPPORTED_MEDIA_TYPE = 415;
const HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
const HTTP_EXPECTATION_FAILED = 417;
const HTTP_I_AM_A_TEAPOT = 418; // RFC2324
const HTTP_MISDIRECTED_REQUEST = 421; // RFC7540
const HTTP_UNPROCESSABLE_ENTITY = 422; // RFC4918
const HTTP_LOCKED = 423; // RFC4918
const HTTP_FAILED_DEPENDENCY = 424; // RFC4918
public const HTTP_CONTINUE = 100;
public const HTTP_SWITCHING_PROTOCOLS = 101;
public const HTTP_PROCESSING = 102; // RFC2518
public const HTTP_EARLY_HINTS = 103; // RFC8297
public const HTTP_OK = 200;
public const HTTP_CREATED = 201;
public const HTTP_ACCEPTED = 202;
public const HTTP_NON_AUTHORITATIVE_INFORMATION = 203;
public const HTTP_NO_CONTENT = 204;
public const HTTP_RESET_CONTENT = 205;
public const HTTP_PARTIAL_CONTENT = 206;
public const HTTP_MULTI_STATUS = 207; // RFC4918
public const HTTP_ALREADY_REPORTED = 208; // RFC5842
public const HTTP_IM_USED = 226; // RFC3229
public const HTTP_MULTIPLE_CHOICES = 300;
public const HTTP_MOVED_PERMANENTLY = 301;
public const HTTP_FOUND = 302;
public const HTTP_SEE_OTHER = 303;
public const HTTP_NOT_MODIFIED = 304;
public const HTTP_USE_PROXY = 305;
public const HTTP_RESERVED = 306;
public const HTTP_TEMPORARY_REDIRECT = 307;
public const HTTP_PERMANENTLY_REDIRECT = 308; // RFC7238
public const HTTP_BAD_REQUEST = 400;
public const HTTP_UNAUTHORIZED = 401;
public const HTTP_PAYMENT_REQUIRED = 402;
public const HTTP_FORBIDDEN = 403;
public const HTTP_NOT_FOUND = 404;
public const HTTP_METHOD_NOT_ALLOWED = 405;
public const HTTP_NOT_ACCEPTABLE = 406;
public const HTTP_PROXY_AUTHENTICATION_REQUIRED = 407;
public const HTTP_REQUEST_TIMEOUT = 408;
public const HTTP_CONFLICT = 409;
public const HTTP_GONE = 410;
public const HTTP_LENGTH_REQUIRED = 411;
public const HTTP_PRECONDITION_FAILED = 412;
public const HTTP_REQUEST_ENTITY_TOO_LARGE = 413;
public const HTTP_REQUEST_URI_TOO_LONG = 414;
public const HTTP_UNSUPPORTED_MEDIA_TYPE = 415;
public const HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
public const HTTP_EXPECTATION_FAILED = 417;
public const HTTP_I_AM_A_TEAPOT = 418; // RFC2324
public const HTTP_MISDIRECTED_REQUEST = 421; // RFC7540
public const HTTP_UNPROCESSABLE_ENTITY = 422; // RFC4918
public const HTTP_LOCKED = 423; // RFC4918
public const HTTP_FAILED_DEPENDENCY = 424; // RFC4918
public const HTTP_TOO_EARLY = 425; // RFC-ietf-httpbis-replay-04
public const HTTP_UPGRADE_REQUIRED = 426; // RFC2817
public const HTTP_PRECONDITION_REQUIRED = 428; // RFC6585
public const HTTP_TOO_MANY_REQUESTS = 429; // RFC6585
public const HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE = 431; // RFC6585
public const HTTP_UNAVAILABLE_FOR_LEGAL_REASONS = 451;
public const HTTP_INTERNAL_SERVER_ERROR = 500;
public const HTTP_NOT_IMPLEMENTED = 501;
public const HTTP_BAD_GATEWAY = 502;
public const HTTP_SERVICE_UNAVAILABLE = 503;
public const HTTP_GATEWAY_TIMEOUT = 504;
public const HTTP_VERSION_NOT_SUPPORTED = 505;
public const HTTP_VARIANT_ALSO_NEGOTIATES_EXPERIMENTAL = 506; // RFC2295
public const HTTP_INSUFFICIENT_STORAGE = 507; // RFC4918
public const HTTP_LOOP_DETECTED = 508; // RFC5842
public const HTTP_NOT_EXTENDED = 510; // RFC2774
public const HTTP_NETWORK_AUTHENTICATION_REQUIRED = 511; // RFC6585
/**
* @deprecated
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
*/
const HTTP_RESERVED_FOR_WEBDAV_ADVANCED_COLLECTIONS_EXPIRED_PROPOSAL = 425; // RFC2817
const HTTP_TOO_EARLY = 425; // RFC-ietf-httpbis-replay-04
const HTTP_UPGRADE_REQUIRED = 426; // RFC2817
const HTTP_PRECONDITION_REQUIRED = 428; // RFC6585
const HTTP_TOO_MANY_REQUESTS = 429; // RFC6585
const HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE = 431; // RFC6585
const HTTP_UNAVAILABLE_FOR_LEGAL_REASONS = 451;
const HTTP_INTERNAL_SERVER_ERROR = 500;
const HTTP_NOT_IMPLEMENTED = 501;
const HTTP_BAD_GATEWAY = 502;
const HTTP_SERVICE_UNAVAILABLE = 503;
const HTTP_GATEWAY_TIMEOUT = 504;
const HTTP_VERSION_NOT_SUPPORTED = 505;
const HTTP_VARIANT_ALSO_NEGOTIATES_EXPERIMENTAL = 506; // RFC2295
const HTTP_INSUFFICIENT_STORAGE = 507; // RFC4918
const HTTP_LOOP_DETECTED = 508; // RFC5842
const HTTP_NOT_EXTENDED = 510; // RFC2774
const HTTP_NETWORK_AUTHENTICATION_REQUIRED = 511; // RFC6585
private const HTTP_RESPONSE_CACHE_CONTROL_DIRECTIVES = [
'must_revalidate' => false,
'no_cache' => false,
'no_store' => false,
'no_transform' => false,
'public' => false,
'private' => false,
'proxy_revalidate' => false,
'max_age' => true,
's_maxage' => true,
'immutable' => false,
'last_modified' => true,
'etag' => true,
];
/**
* @var \Symfony\Component\HttpFoundation\ResponseHeaderBag
* @var ResponseHeaderBag
*/
public $headers;
@@ -121,8 +137,8 @@ class Response
* Status codes translation table.
*
* The list of codes is complete according to the
* {@link http://www.iana.org/assignments/http-status-codes/ Hypertext Transfer Protocol (HTTP) Status Code Registry}
* (last updated 2016-03-01).
* {@link https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml Hypertext Transfer Protocol (HTTP) Status Code Registry}
* (last updated 2021-10-01).
*
* Unless otherwise noted, the status code is defined in RFC2616.
*
@@ -164,14 +180,14 @@ class Response
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Payload Too Large',
413 => 'Content Too Large', // RFC-ietf-httpbis-semantics
414 => 'URI Too Long',
415 => 'Unsupported Media Type',
416 => 'Range Not Satisfiable',
417 => 'Expectation Failed',
418 => 'I\'m a teapot', // RFC2324
421 => 'Misdirected Request', // RFC7540
422 => 'Unprocessable Entity', // RFC4918
422 => 'Unprocessable Content', // RFC-ietf-httpbis-semantics
423 => 'Locked', // RFC4918
424 => 'Failed Dependency', // RFC4918
425 => 'Too Early', // RFC-ietf-httpbis-replay-04
@@ -196,7 +212,7 @@ class Response
/**
* @throws \InvalidArgumentException When the HTTP status code is not valid
*/
public function __construct($content = '', int $status = 200, array $headers = [])
public function __construct(?string $content = '', int $status = 200, array $headers = [])
{
$this->headers = new ResponseHeaderBag($headers);
$this->setContent($content);
@@ -212,14 +228,14 @@ class Response
* return Response::create($body, 200)
* ->setSharedMaxAge(300);
*
* @param mixed $content The response content, see setContent()
* @param int $status The response status code
* @param array $headers An array of response headers
*
* @return static
*
* @deprecated since Symfony 5.1, use __construct() instead.
*/
public static function create($content = '', $status = 200, $headers = [])
public static function create(?string $content = '', int $status = 200, array $headers = [])
{
trigger_deprecation('symfony/http-foundation', '5.1', 'The "%s()" method is deprecated, use "new %s()" instead.', __METHOD__, static::class);
return new static($content, $status, $headers);
}
@@ -230,7 +246,7 @@ class Response
* one that will be sent to the client only if the prepare() method
* has been called before.
*
* @return string The Response as an HTTP string
* @return string
*
* @see prepare()
*/
@@ -267,10 +283,12 @@ class Response
$this->setContent(null);
$headers->remove('Content-Type');
$headers->remove('Content-Length');
// prevent PHP from sending the Content-Type header based on default_mimetype
ini_set('default_mimetype', '');
} else {
// Content-type based on the Request
if (!$headers->has('Content-Type')) {
$format = $request->getRequestFormat();
$format = $request->getRequestFormat(null);
if (null !== $format && $mimeType = $request->getMimeType($format)) {
$headers->set('Content-Type', $mimeType);
}
@@ -306,7 +324,7 @@ class Response
}
// Check if we need to send extra expire info headers
if ('1.0' == $this->getProtocolVersion() && false !== strpos($headers->get('Cache-Control'), 'no-cache')) {
if ('1.0' == $this->getProtocolVersion() && str_contains($headers->get('Cache-Control', ''), 'no-cache')) {
$headers->set('pragma', 'no-cache');
$headers->set('expires', -1);
}
@@ -344,7 +362,7 @@ class Response
// cookies
foreach ($this->headers->getCookies() as $cookie) {
header('Set-Cookie: '.$cookie->getName().strstr($cookie, '='), false, $this->statusCode);
header('Set-Cookie: '.$cookie, false, $this->statusCode);
}
// status
@@ -377,6 +395,8 @@ class Response
if (\function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
} elseif (\function_exists('litespeed_finish_request')) {
litespeed_finish_request();
} elseif (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) {
static::closeOutputBuffers(0, true);
}
@@ -387,21 +407,11 @@ class Response
/**
* Sets the response content.
*
* Valid types are strings, numbers, null, and objects that implement a __toString() method.
*
* @param mixed $content Content that can be cast to string
*
* @return $this
*
* @throws \UnexpectedValueException
*/
public function setContent($content)
public function setContent(?string $content)
{
if (null !== $content && !\is_string($content) && !is_numeric($content) && !\is_callable([$content, '__toString'])) {
throw new \UnexpectedValueException(sprintf('The Response content must be a string or object implementing __toString(), "%s" given.', \gettype($content)));
}
$this->content = (string) $content;
$this->content = $content ?? '';
return $this;
}
@@ -409,7 +419,7 @@ class Response
/**
* Gets the current response content.
*
* @return string Content
* @return string|false
*/
public function getContent()
{
@@ -423,7 +433,7 @@ class Response
*
* @final
*/
public function setProtocolVersion(string $version)
public function setProtocolVersion(string $version): object
{
$this->version = $version;
@@ -452,7 +462,7 @@ class Response
*
* @final
*/
public function setStatusCode(int $code, $text = null)
public function setStatusCode(int $code, string $text = null): object
{
$this->statusCode = $code;
if ($this->isInvalid()) {
@@ -460,7 +470,7 @@ class Response
}
if (null === $text) {
$this->statusText = isset(self::$statusTexts[$code]) ? self::$statusTexts[$code] : 'unknown status';
$this->statusText = self::$statusTexts[$code] ?? 'unknown status';
return $this;
}
@@ -493,7 +503,7 @@ class Response
*
* @final
*/
public function setCharset(string $charset)
public function setCharset(string $charset): object
{
$this->charset = $charset;
@@ -574,7 +584,7 @@ class Response
*
* @final
*/
public function setPrivate()
public function setPrivate(): object
{
$this->headers->removeCacheControlDirective('public');
$this->headers->addCacheControlDirective('private');
@@ -591,7 +601,7 @@ class Response
*
* @final
*/
public function setPublic()
public function setPublic(): object
{
$this->headers->addCacheControlDirective('public');
$this->headers->removeCacheControlDirective('private');
@@ -606,7 +616,7 @@ class Response
*
* @final
*/
public function setImmutable(bool $immutable = true)
public function setImmutable(bool $immutable = true): object
{
if ($immutable) {
$this->headers->addCacheControlDirective('immutable');
@@ -628,7 +638,7 @@ class Response
}
/**
* Returns true if the response must be revalidated by caches.
* Returns true if the response must be revalidated by shared caches once it has become stale.
*
* This method indicates that the response must not be served stale by a
* cache in any circumstance without first revalidating with the origin.
@@ -661,7 +671,7 @@ class Response
*
* @final
*/
public function setDate(\DateTimeInterface $date)
public function setDate(\DateTimeInterface $date): object
{
if ($date instanceof \DateTime) {
$date = \DateTimeImmutable::createFromMutable($date);
@@ -684,7 +694,7 @@ class Response
return (int) $age;
}
return max(time() - $this->getDate()->format('U'), 0);
return max(time() - (int) $this->getDate()->format('U'), 0);
}
/**
@@ -726,7 +736,7 @@ class Response
*
* @final
*/
public function setExpires(\DateTimeInterface $date = null)
public function setExpires(\DateTimeInterface $date = null): object
{
if (null === $date) {
$this->headers->remove('Expires');
@@ -764,7 +774,7 @@ class Response
}
if (null !== $this->getExpires()) {
return (int) ($this->getExpires()->format('U') - $this->getDate()->format('U'));
return (int) $this->getExpires()->format('U') - (int) $this->getDate()->format('U');
}
return null;
@@ -779,7 +789,7 @@ class Response
*
* @final
*/
public function setMaxAge(int $value)
public function setMaxAge(int $value): object
{
$this->headers->addCacheControlDirective('max-age', $value);
@@ -795,7 +805,7 @@ class Response
*
* @final
*/
public function setSharedMaxAge(int $value)
public function setSharedMaxAge(int $value): object
{
$this->setPublic();
$this->headers->addCacheControlDirective('s-maxage', $value);
@@ -829,7 +839,7 @@ class Response
*
* @final
*/
public function setTtl(int $seconds)
public function setTtl(int $seconds): object
{
$this->setSharedMaxAge($this->getAge() + $seconds);
@@ -845,7 +855,7 @@ class Response
*
* @final
*/
public function setClientTtl(int $seconds)
public function setClientTtl(int $seconds): object
{
$this->setMaxAge($this->getAge() + $seconds);
@@ -873,7 +883,7 @@ class Response
*
* @final
*/
public function setLastModified(\DateTimeInterface $date = null)
public function setLastModified(\DateTimeInterface $date = null): object
{
if (null === $date) {
$this->headers->remove('Last-Modified');
@@ -911,12 +921,12 @@ class Response
*
* @final
*/
public function setEtag(string $etag = null, bool $weak = false)
public function setEtag(string $etag = null, bool $weak = false): object
{
if (null === $etag) {
$this->headers->remove('Etag');
} else {
if (0 !== strpos($etag, '"')) {
if (!str_starts_with($etag, '"')) {
$etag = '"'.$etag.'"';
}
@@ -929,7 +939,7 @@ class Response
/**
* Sets the response's cache headers (validation and/or expiration).
*
* Available options are: etag, last_modified, max_age, s_maxage, private, public and immutable.
* Available options are: must_revalidate, no_cache, no_store, no_transform, public, private, proxy_revalidate, max_age, s_maxage, immutable, last_modified and etag.
*
* @return $this
*
@@ -937,9 +947,9 @@ class Response
*
* @final
*/
public function setCache(array $options)
public function setCache(array $options): object
{
if ($diff = array_diff(array_keys($options), ['etag', 'last_modified', 'max_age', 's_maxage', 'private', 'public', 'immutable'])) {
if ($diff = array_diff(array_keys($options), array_keys(self::HTTP_RESPONSE_CACHE_CONTROL_DIRECTIVES))) {
throw new \InvalidArgumentException(sprintf('Response does not support the following options: "%s".', implode('", "', $diff)));
}
@@ -959,6 +969,16 @@ class Response
$this->setSharedMaxAge($options['s_maxage']);
}
foreach (self::HTTP_RESPONSE_CACHE_CONTROL_DIRECTIVES as $directive => $hasValue) {
if (!$hasValue && isset($options[$directive])) {
if ($options[$directive]) {
$this->headers->addCacheControlDirective(str_replace('_', '-', $directive));
} else {
$this->headers->removeCacheControlDirective(str_replace('_', '-', $directive));
}
}
}
if (isset($options['public'])) {
if ($options['public']) {
$this->setPublic();
@@ -975,10 +995,6 @@ class Response
}
}
if (isset($options['immutable'])) {
$this->setImmutable((bool) $options['immutable']);
}
return $this;
}
@@ -990,11 +1006,11 @@ class Response
*
* @return $this
*
* @see http://tools.ietf.org/html/rfc2616#section-10.3.5
* @see https://tools.ietf.org/html/rfc2616#section-10.3.5
*
* @final
*/
public function setNotModified()
public function setNotModified(): object
{
$this->setStatusCode(304);
$this->setContent(null);
@@ -1024,16 +1040,16 @@ class Response
*/
public function getVary(): array
{
if (!$vary = $this->headers->get('Vary', null, false)) {
if (!$vary = $this->headers->all('Vary')) {
return [];
}
$ret = [];
foreach ($vary as $item) {
$ret = array_merge($ret, preg_split('/[\s,]+/', $item));
$ret[] = preg_split('/[\s,]+/', $item);
}
return $ret;
return array_merge([], ...$ret);
}
/**
@@ -1046,7 +1062,7 @@ class Response
*
* @final
*/
public function setVary($headers, bool $replace = true)
public function setVary($headers, bool $replace = true): object
{
$this->headers->set('Vary', $headers, $replace);
@@ -1060,8 +1076,6 @@ class Response
* If the Response is not modified, it sets the status code to 304 and
* removes the actual content by calling the setNotModified() method.
*
* @return bool true if the Response validators match the Request, false otherwise
*
* @final
*/
public function isNotModified(Request $request): bool
@@ -1074,12 +1088,26 @@ class Response
$lastModified = $this->headers->get('Last-Modified');
$modifiedSince = $request->headers->get('If-Modified-Since');
if ($etags = $request->getETags()) {
$notModified = \in_array($this->getEtag(), $etags) || \in_array('*', $etags);
}
if (($ifNoneMatchEtags = $request->getETags()) && (null !== $etag = $this->getEtag())) {
if (0 == strncmp($etag, 'W/', 2)) {
$etag = substr($etag, 2);
}
if ($modifiedSince && $lastModified) {
$notModified = strtotime($modifiedSince) >= strtotime($lastModified) && (!$etags || $notModified);
// Use weak comparison as per https://tools.ietf.org/html/rfc7232#section-3.2.
foreach ($ifNoneMatchEtags as $ifNoneMatchEtag) {
if (0 == strncmp($ifNoneMatchEtag, 'W/', 2)) {
$ifNoneMatchEtag = substr($ifNoneMatchEtag, 2);
}
if ($ifNoneMatchEtag === $etag || '*' === $ifNoneMatchEtag) {
$notModified = true;
break;
}
}
}
// Only do If-Modified-Since date comparison when If-None-Match is not present as per https://tools.ietf.org/html/rfc7232#section-3.3.
elseif ($modifiedSince && $lastModified) {
$notModified = strtotime($modifiedSince) >= strtotime($lastModified);
}
if ($notModified) {
@@ -1092,7 +1120,7 @@ class Response
/**
* Is response invalid?
*
* @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
* @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
*
* @final
*/
@@ -1208,11 +1236,11 @@ class Response
*
* @final
*/
public static function closeOutputBuffers(int $targetLevel, bool $flush)
public static function closeOutputBuffers(int $targetLevel, bool $flush): void
{
$status = ob_get_status(true);
$level = \count($status);
$flags = PHP_OUTPUT_HANDLER_REMOVABLE | ($flush ? PHP_OUTPUT_HANDLER_FLUSHABLE : PHP_OUTPUT_HANDLER_CLEANABLE);
$flags = \PHP_OUTPUT_HANDLER_REMOVABLE | ($flush ? \PHP_OUTPUT_HANDLER_FLUSHABLE : \PHP_OUTPUT_HANDLER_CLEANABLE);
while ($level-- > $targetLevel && ($s = $status[$level]) && (!isset($s['del']) ? !isset($s['flags']) || ($s['flags'] & $flags) === $flags : $s['del'])) {
if ($flush) {
@@ -1223,6 +1251,22 @@ class Response
}
}
/**
* Marks a response as safe according to RFC8674.
*
* @see https://tools.ietf.org/html/rfc8674
*/
public function setContentSafe(bool $safe = true): void
{
if ($safe) {
$this->headers->set('Preference-Applied', 'safe');
} elseif ('safe' === $this->headers->get('Preference-Applied')) {
$this->headers->remove('Preference-Applied');
}
$this->setVary('Prefer', false);
}
/**
* Checks if we need to remove Cache-Control for SSL encrypted downloads when using IE < 9.
*
@@ -1230,9 +1274,9 @@ class Response
*
* @final
*/
protected function ensureIEOverSSLCompatibility(Request $request)
protected function ensureIEOverSSLCompatibility(Request $request): void
{
if (false !== stripos($this->headers->get('Content-Disposition'), 'attachment') && 1 == preg_match('/MSIE (.*?);/i', $request->server->get('HTTP_USER_AGENT'), $match) && true === $request->isSecure()) {
if (false !== stripos($this->headers->get('Content-Disposition') ?? '', 'attachment') && 1 == preg_match('/MSIE (.*?);/i', $request->server->get('HTTP_USER_AGENT') ?? '', $match) && true === $request->isSecure()) {
if ((int) preg_replace('/(MSIE )(.*?);/', '$2', $match[0]) < 9) {
$this->headers->remove('Cache-Control');
}

View File

@@ -18,11 +18,11 @@ 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';
protected $computedCacheControl = [];
protected $cookies = [];
@@ -45,13 +45,13 @@ class ResponseHeaderBag extends HeaderBag
/**
* Returns the headers, with original capitalizations.
*
* @return array An array of headers
* @return array
*/
public function allPreserveCase()
{
$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;
@@ -88,9 +88,16 @@ class ResponseHeaderBag extends HeaderBag
/**
* {@inheritdoc}
*/
public function all()
public function all(string $key = null)
{
$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;
}
@@ -101,9 +108,9 @@ class ResponseHeaderBag extends HeaderBag
/**
* {@inheritdoc}
*/
public function set($key, $values, $replace = true)
public function set(string $key, $values, bool $replace = true)
{
$uniqueKey = str_replace('_', '-', strtolower($key));
$uniqueKey = strtr($key, self::UPPER, self::LOWER);
if ('set-cookie' === $uniqueKey) {
if ($replace) {
@@ -132,9 +139,9 @@ 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) {
@@ -157,7 +164,7 @@ class ResponseHeaderBag extends HeaderBag
/**
* {@inheritdoc}
*/
public function hasCacheControlDirective($key)
public function hasCacheControlDirective(string $key)
{
return \array_key_exists($key, $this->computedCacheControl);
}
@@ -165,9 +172,9 @@ class ResponseHeaderBag extends HeaderBag
/**
* {@inheritdoc}
*/
public function getCacheControlDirective($key)
public function getCacheControlDirective(string $key)
{
return \array_key_exists($key, $this->computedCacheControl) ? $this->computedCacheControl[$key] : null;
return $this->computedCacheControl[$key] ?? null;
}
public function setCookie(Cookie $cookie)
@@ -178,12 +185,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 = '/';
@@ -207,13 +210,11 @@ class ResponseHeaderBag extends HeaderBag
/**
* Returns an array with all cookies.
*
* @param string $format
*
* @return Cookie[]
*
* @throws \InvalidArgumentException When the $format is invalid
*/
public function getCookies($format = self::COOKIES_FLAT)
public function getCookies(string $format = self::COOKIES_FLAT)
{
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])));
@@ -237,24 +238,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, false, null));
$this->setCookie(new Cookie($name, null, 1, $path, $domain, $secure, $httpOnly, false, $sameSite));
}
/**
* @see HeaderUtils::makeDisposition()
*/
public function makeDisposition($disposition, $filename, $filenameFallback = '')
public function makeDisposition(string $disposition, string $filename, string $filenameFallback = '')
{
return HeaderUtils::makeDisposition((string) $disposition, (string) $filename, (string) $filenameFallback);
return HeaderUtils::makeDisposition($disposition, $filename, $filenameFallback);
}
/**
@@ -267,13 +262,13 @@ class ResponseHeaderBag extends HeaderBag
*/
protected function computeCacheControlValue()
{
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();
@@ -289,10 +284,8 @@ class ResponseHeaderBag extends HeaderBag
return $header;
}
private function initDate()
private function initDate(): void
{
$now = \DateTime::createFromFormat('U', time());
$now->setTimezone(new \DateTimeZone('UTC'));
$this->set('Date', $now->format('D, d M Y H:i:s').' GMT');
$this->set('Date', gmdate('D, d M Y H:i:s').' GMT');
}
}

View File

@@ -28,31 +28,28 @@ class ServerBag extends ParameterBag
public function getHeaders()
{
$headers = [];
$contentHeaders = ['CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true];
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]
*/
@@ -69,7 +66,7 @@ class ServerBag extends ParameterBag
// 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 (2 == \count($exploded)) {
list($headers['PHP_AUTH_USER'], $headers['PHP_AUTH_PW']) = $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 +76,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 +89,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,6 +13,8 @@ namespace Symfony\Component\HttpFoundation\Session\Attribute;
/**
* This class relates to session attribute storage.
*
* @implements \IteratorAggregate<string, mixed>
*/
class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Countable
{
@@ -37,7 +39,7 @@ class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Counta
return $this->name;
}
public function setName($name)
public function setName(string $name)
{
$this->name = $name;
}
@@ -61,7 +63,7 @@ class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Counta
/**
* {@inheritdoc}
*/
public function has($name)
public function has(string $name)
{
return \array_key_exists($name, $this->attributes);
}
@@ -69,7 +71,7 @@ class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Counta
/**
* {@inheritdoc}
*/
public function get($name, $default = null)
public function get(string $name, $default = null)
{
return \array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default;
}
@@ -77,7 +79,7 @@ class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Counta
/**
* {@inheritdoc}
*/
public function set($name, $value)
public function set(string $name, $value)
{
$this->attributes[$name] = $value;
}
@@ -104,7 +106,7 @@ class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Counta
/**
* {@inheritdoc}
*/
public function remove($name)
public function remove(string $name)
{
$retval = null;
if (\array_key_exists($name, $this->attributes)) {
@@ -129,8 +131,9 @@ class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Counta
/**
* Returns an iterator for attributes.
*
* @return \ArrayIterator An \ArrayIterator instance
* @return \ArrayIterator<string, mixed>
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
return new \ArrayIterator($this->attributes);
@@ -139,8 +142,9 @@ class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Counta
/**
* Returns the number of attributes.
*
* @return int The number of attributes
* @return int
*/
#[\ReturnTypeWillChange]
public function count()
{
return \count($this->attributes);

View File

@@ -23,50 +23,39 @@ 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
* @return bool
*/
public function has($name);
public function has(string $name);
/**
* Returns an attribute.
*
* @param string $name The attribute name
* @param mixed $default The default value if not found
* @param mixed $default The default value if not found
*
* @return mixed
*/
public function get($name, $default = null);
public function get(string $name, $default = null);
/**
* Sets an attribute.
*
* @param string $name
* @param mixed $value
* @param mixed $value
*/
public function set($name, $value);
public function set(string $name, $value);
/**
* Returns attributes.
*
* @return array Attributes
* @return array<string, mixed>
*/
public function all();
/**
* 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);
}

View File

@@ -11,11 +11,15 @@
namespace Symfony\Component\HttpFoundation\Session\Attribute;
trigger_deprecation('symfony/http-foundation', '5.3', 'The "%s" class is deprecated.', NamespacedAttributeBag::class);
/**
* This class provides structured storage of session attributes using
* a name spacing character in the key.
*
* @author Drak <drak@zikula.org>
*
* @deprecated since Symfony 5.3
*/
class NamespacedAttributeBag extends AttributeBag
{
@@ -34,7 +38,7 @@ class NamespacedAttributeBag extends AttributeBag
/**
* {@inheritdoc}
*/
public function has($name)
public function has(string $name)
{
// reference mismatch: if fixed, re-introduced in array_key_exists; keep as it is
$attributes = $this->resolveAttributePath($name);
@@ -50,7 +54,7 @@ class NamespacedAttributeBag extends AttributeBag
/**
* {@inheritdoc}
*/
public function get($name, $default = null)
public function get(string $name, $default = null)
{
// reference mismatch: if fixed, re-introduced in array_key_exists; keep as it is
$attributes = $this->resolveAttributePath($name);
@@ -66,7 +70,7 @@ class NamespacedAttributeBag extends AttributeBag
/**
* {@inheritdoc}
*/
public function set($name, $value)
public function set(string $name, $value)
{
$attributes = &$this->resolveAttributePath($name, true);
$name = $this->resolveKey($name);
@@ -76,7 +80,7 @@ class NamespacedAttributeBag extends AttributeBag
/**
* {@inheritdoc}
*/
public function remove($name)
public function remove(string $name)
{
$retval = null;
$attributes = &$this->resolveAttributePath($name);
@@ -97,12 +101,12 @@ class NamespacedAttributeBag extends AttributeBag
* @param string $name Key name
* @param bool $writeContext Write context, default false
*
* @return array
* @return array|null
*/
protected function &resolveAttributePath($name, $writeContext = false)
protected function &resolveAttributePath(string $name, bool $writeContext = false)
{
$array = &$this->attributes;
$name = (0 === strpos($name, $this->namespaceCharacter)) ? substr($name, 1) : $name;
$name = (str_starts_with($name, $this->namespaceCharacter)) ? substr($name, 1) : $name;
// Check if there is anything to do, else return
if (!$name) {
@@ -144,11 +148,9 @@ class NamespacedAttributeBag extends AttributeBag
*
* This is the last part in a dot separated string.
*
* @param string $name
*
* @return string
*/
protected function resolveKey($name)
protected function resolveKey(string $name)
{
if (false !== $pos = strrpos($name, $this->namespaceCharacter)) {
$name = substr($name, $pos + 1);

View File

@@ -38,7 +38,7 @@ class AutoExpireFlashBag implements FlashBagInterface
return $this->name;
}
public function setName($name)
public function setName(string $name)
{
$this->name = $name;
}
@@ -60,7 +60,7 @@ class AutoExpireFlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function add($type, $message)
public function add(string $type, $message)
{
$this->flashes['new'][$type][] = $message;
}
@@ -68,7 +68,7 @@ class AutoExpireFlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function peek($type, array $default = [])
public function peek(string $type, array $default = [])
{
return $this->has($type) ? $this->flashes['display'][$type] : $default;
}
@@ -78,13 +78,13 @@ class AutoExpireFlashBag implements FlashBagInterface
*/
public function peekAll()
{
return \array_key_exists('display', $this->flashes) ? (array) $this->flashes['display'] : [];
return \array_key_exists('display', $this->flashes) ? $this->flashes['display'] : [];
}
/**
* {@inheritdoc}
*/
public function get($type, array $default = [])
public function get(string $type, array $default = [])
{
$return = $default;
@@ -122,7 +122,7 @@ class AutoExpireFlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function set($type, $messages)
public function set(string $type, $messages)
{
$this->flashes['new'][$type] = (array) $messages;
}
@@ -130,7 +130,7 @@ class AutoExpireFlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function has($type)
public function has(string $type)
{
return \array_key_exists($type, $this->flashes['display']) && $this->flashes['display'][$type];
}

View File

@@ -38,7 +38,7 @@ class FlashBag implements FlashBagInterface
return $this->name;
}
public function setName($name)
public function setName(string $name)
{
$this->name = $name;
}
@@ -54,7 +54,7 @@ class FlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function add($type, $message)
public function add(string $type, $message)
{
$this->flashes[$type][] = $message;
}
@@ -62,7 +62,7 @@ class FlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function peek($type, array $default = [])
public function peek(string $type, array $default = [])
{
return $this->has($type) ? $this->flashes[$type] : $default;
}
@@ -78,7 +78,7 @@ class FlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function get($type, array $default = [])
public function get(string $type, array $default = [])
{
if (!$this->has($type)) {
return $default;
@@ -105,7 +105,7 @@ class FlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function set($type, $messages)
public function set(string $type, $messages)
{
$this->flashes[$type] = (array) $messages;
}
@@ -121,7 +121,7 @@ class FlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function has($type)
public function has(string $type)
{
return \array_key_exists($type, $this->flashes) && $this->flashes[$type];
}

View File

@@ -23,18 +23,16 @@ interface FlashBagInterface extends SessionBagInterface
/**
* Adds a flash message for the given type.
*
* @param string $type
* @param mixed $message
* @param mixed $message
*/
public function add($type, $message);
public function add(string $type, $message);
/**
* Registers one or more messages for a given type.
*
* @param string $type
* @param string|array $messages
*/
public function set($type, $messages);
public function set(string $type, $messages);
/**
* Gets flash messages for a given type.
@@ -44,7 +42,7 @@ interface FlashBagInterface extends SessionBagInterface
*
* @return array
*/
public function peek($type, array $default = []);
public function peek(string $type, array $default = []);
/**
* Gets all flash messages.
@@ -56,12 +54,11 @@ interface FlashBagInterface extends SessionBagInterface
/**
* Gets and clears flash from the stack.
*
* @param string $type
* @param array $default Default value if $type does not exist
* @param array $default Default value if $type does not exist
*
* @return array
*/
public function get($type, array $default = []);
public function get(string $type, array $default = []);
/**
* Gets and clears flashes from the stack.
@@ -78,11 +75,9 @@ interface FlashBagInterface extends SessionBagInterface
/**
* Has flash messages for a given type?
*
* @param string $type
*
* @return bool
*/
public function has($type);
public function has(string $type);
/**
* Returns a list of all defined types.

View File

@@ -18,9 +18,16 @@ use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
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);
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Drak <drak@zikula.org>
*
* @implements \IteratorAggregate<string, mixed>
*/
class Session implements SessionInterface, \IteratorAggregate, \Countable
{
@@ -30,21 +37,18 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
private $attributeName;
private $data = [];
private $usageIndex = 0;
private $usageReporter;
/**
* @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;
$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);
}
@@ -60,7 +64,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
/**
* {@inheritdoc}
*/
public function has($name)
public function has(string $name)
{
return $this->getAttributeBag()->has($name);
}
@@ -68,7 +72,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
/**
* {@inheritdoc}
*/
public function get($name, $default = null)
public function get(string $name, $default = null)
{
return $this->getAttributeBag()->get($name, $default);
}
@@ -76,7 +80,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
/**
* {@inheritdoc}
*/
public function set($name, $value)
public function set(string $name, $value)
{
$this->getAttributeBag()->set($name, $value);
}
@@ -100,7 +104,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
/**
* {@inheritdoc}
*/
public function remove($name)
public function remove(string $name)
{
return $this->getAttributeBag()->remove($name);
}
@@ -124,8 +128,9 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
/**
* Returns an iterator for attributes.
*
* @return \ArrayIterator An \ArrayIterator instance
* @return \ArrayIterator<string, mixed>
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
return new \ArrayIterator($this->getAttributeBag()->all());
@@ -134,32 +139,29 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
/**
* Returns the number of attributes.
*
* @return int The number of attributes
* @return int
*/
#[\ReturnTypeWillChange]
public function count()
{
return \count($this->getAttributeBag()->all());
}
/**
* @return int
*
* @internal
*/
public function getUsageIndex()
public function &getUsageIndex(): int
{
return $this->usageIndex;
}
/**
* @return bool
*
* @internal
*/
public function isEmpty()
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)) {
@@ -173,7 +175,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
/**
* {@inheritdoc}
*/
public function invalidate($lifetime = null)
public function invalidate(int $lifetime = null)
{
$this->storage->clear();
@@ -183,7 +185,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
/**
* {@inheritdoc}
*/
public function migrate($destroy = false, $lifetime = null)
public function migrate(bool $destroy = false, int $lifetime = null)
{
return $this->storage->regenerate($destroy, $lifetime);
}
@@ -207,7 +209,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
/**
* {@inheritdoc}
*/
public function setId($id)
public function setId(string $id)
{
if ($this->storage->getId() !== $id) {
$this->storage->setId($id);
@@ -225,7 +227,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
/**
* {@inheritdoc}
*/
public function setName($name)
public function setName(string $name)
{
$this->storage->setName($name);
}
@@ -236,6 +238,9 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
public function getMetadataBag()
{
++$this->usageIndex;
if ($this->usageReporter && 0 <= $this->usageIndex) {
($this->usageReporter)();
}
return $this->storage->getMetadataBag();
}
@@ -245,15 +250,17 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
*/
public function registerBag(SessionBagInterface $bag)
{
$this->storage->registerBag(new SessionBagProxy($bag, $this->data, $this->usageIndex));
$this->storage->registerBag(new SessionBagProxy($bag, $this->data, $this->usageIndex, $this->usageReporter));
}
/**
* {@inheritdoc}
*/
public function getBag($name)
public function getBag(string $name)
{
return $this->storage->getBag($name)->getBag();
$bag = $this->storage->getBag($name);
return method_exists($bag, 'getBag') ? $bag->getBag() : $bag;
}
/**
@@ -270,10 +277,8 @@ 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->getBag($this->attributeName);
}

View File

@@ -21,33 +21,35 @@ final class SessionBagProxy implements SessionBagInterface
private $bag;
private $data;
private $usageIndex;
private $usageReporter;
public function __construct(SessionBagInterface $bag, array &$data, &$usageIndex)
public function __construct(SessionBagInterface $bag, array &$data, ?int &$usageIndex, ?callable $usageReporter)
{
$this->bag = $bag;
$this->data = &$data;
$this->usageIndex = &$usageIndex;
$this->usageReporter = $usageReporter;
}
/**
* @return SessionBagInterface
*/
public function getBag()
public function getBag(): SessionBagInterface
{
++$this->usageIndex;
if ($this->usageReporter && 0 <= $this->usageIndex) {
($this->usageReporter)();
}
return $this->bag;
}
/**
* @return bool
*/
public function isEmpty()
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()]);
}
@@ -55,7 +57,7 @@ final class SessionBagProxy implements SessionBagInterface
/**
* {@inheritdoc}
*/
public function getName()
public function getName(): string
{
return $this->bag->getName();
}
@@ -63,9 +65,13 @@ final class SessionBagProxy implements SessionBagInterface
/**
* {@inheritdoc}
*/
public function initialize(array &$array)
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);
@@ -74,7 +80,7 @@ final class SessionBagProxy implements SessionBagInterface
/**
* {@inheritdoc}
*/
public function getStorageKey()
public function getStorageKey(): string
{
return $this->bag->getStorageKey();
}

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 $usageReporter;
public function __construct(RequestStack $requestStack, SessionStorageFactoryInterface $storageFactory, callable $usageReporter = null)
{
$this->requestStack = $requestStack;
$this->storageFactory = $storageFactory;
$this->usageReporter = $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,7 +23,7 @@ interface SessionInterface
/**
* Starts the session storage.
*
* @return bool True if session started
* @return bool
*
* @throws \RuntimeException if session fails to start
*/
@@ -32,30 +32,26 @@ interface SessionInterface
/**
* Returns the session ID.
*
* @return string The session ID
* @return string
*/
public function getId();
/**
* 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
* @return string
*/
public function getName();
/**
* Sets the session name.
*
* @param string $name
*/
public function setName($name);
public function setName(string $name);
/**
* Invalidates the current session.
@@ -68,9 +64,9 @@ interface SessionInterface
* to expire with browser session. Time is in seconds, and is
* not a Unix timestamp.
*
* @return bool True if session invalidated, false if error
* @return bool
*/
public function invalidate($lifetime = null);
public function invalidate(int $lifetime = null);
/**
* Migrates the current session to a new session id while maintaining all
@@ -82,9 +78,9 @@ interface SessionInterface
* to expire with browser session. Time is in seconds, and is
* not a Unix timestamp.
*
* @return bool True if session migrated, false if error
* @return bool
*/
public function migrate($destroy = false, $lifetime = null);
public function migrate(bool $destroy = false, int $lifetime = null);
/**
* Force the session to be saved and closed.
@@ -98,52 +94,44 @@ interface SessionInterface
/**
* Checks if an attribute is defined.
*
* @param string $name The attribute name
*
* @return bool true if the attribute is defined, false otherwise
* @return bool
*/
public function has($name);
public function has(string $name);
/**
* Returns an attribute.
*
* @param string $name The attribute name
* @param mixed $default The default value if not found
* @param mixed $default The default value if not found
*
* @return mixed
*/
public function get($name, $default = null);
public function get(string $name, $default = null);
/**
* Sets an attribute.
*
* @param string $name
* @param mixed $value
* @param mixed $value
*/
public function set($name, $value);
public function set(string $name, $value);
/**
* Returns attributes.
*
* @return array Attributes
* @return array
*/
public function all();
/**
* 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);
/**
* Clears all attributes.
@@ -165,11 +153,9 @@ interface SessionInterface
/**
* Gets a bag instance by name.
*
* @param string $name
*
* @return SessionBagInterface
*/
public function getBag($name);
public function getBag(string $name);
/**
* Gets session meta.

View File

@@ -29,8 +29,9 @@ abstract class AbstractSessionHandler implements \SessionHandlerInterface, \Sess
private $igbinaryEmptyData;
/**
* {@inheritdoc}
* @return bool
*/
#[\ReturnTypeWillChange]
public function open($savePath, $sessionName)
{
$this->sessionName = $sessionName;
@@ -42,41 +43,45 @@ abstract class AbstractSessionHandler implements \SessionHandlerInterface, \Sess
}
/**
* @param string $sessionId
*
* @return string
*/
abstract protected function doRead($sessionId);
abstract protected function doRead(string $sessionId);
/**
* @param string $sessionId
* @param string $data
*
* @return bool
*/
abstract protected function doWrite($sessionId, $data);
abstract protected function doWrite(string $sessionId, string $data);
/**
* @param string $sessionId
*
* @return bool
*/
abstract protected function doDestroy($sessionId);
abstract protected function doDestroy(string $sessionId);
/**
* {@inheritdoc}
* @return bool
*/
#[\ReturnTypeWillChange]
public function validateId($sessionId)
{
$this->prefetchData = $this->read($sessionId);
$this->prefetchId = $sessionId;
if (\PHP_VERSION_ID < 70317 || (70400 <= \PHP_VERSION_ID && \PHP_VERSION_ID < 70405)) {
// work around https://bugs.php.net/79413
foreach (debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS) as $frame) {
if (!isset($frame['class']) && isset($frame['function']) && \in_array($frame['function'], ['session_regenerate_id', 'session_create_id'], true)) {
return '' === $this->prefetchData;
}
}
}
return '' !== $this->prefetchData;
}
/**
* {@inheritdoc}
* @return string
*/
#[\ReturnTypeWillChange]
public function read($sessionId)
{
if (null !== $this->prefetchId) {
@@ -98,8 +103,9 @@ abstract class AbstractSessionHandler implements \SessionHandlerInterface, \Sess
}
/**
* {@inheritdoc}
* @return bool
*/
#[\ReturnTypeWillChange]
public function write($sessionId, $data)
{
if (null === $this->igbinaryEmptyData) {
@@ -115,18 +121,27 @@ abstract class AbstractSessionHandler implements \SessionHandlerInterface, \Sess
}
/**
* {@inheritdoc}
* @return bool
*/
#[\ReturnTypeWillChange]
public function destroy($sessionId)
{
if (!headers_sent() && filter_var(ini_get('session.use_cookies'), FILTER_VALIDATE_BOOLEAN)) {
if (!headers_sent() && filter_var(ini_get('session.use_cookies'), \FILTER_VALIDATE_BOOLEAN)) {
if (!$this->sessionName) {
throw new \LogicException(sprintf('Session name cannot be empty, did you forget to call "parent::open()" in "%s"?.', \get_class($this)));
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);
if (null === $cookie) {
/*
* 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])) {
if (\PHP_VERSION_ID < 70300) {
setcookie($this->sessionName, '', 0, ini_get('session.cookie_path'), ini_get('session.cookie_domain'), filter_var(ini_get('session.cookie_secure'), FILTER_VALIDATE_BOOLEAN), filter_var(ini_get('session.cookie_httponly'), FILTER_VALIDATE_BOOLEAN));
setcookie($this->sessionName, '', 0, ini_get('session.cookie_path'), ini_get('session.cookie_domain'), filter_var(ini_get('session.cookie_secure'), \FILTER_VALIDATE_BOOLEAN), filter_var(ini_get('session.cookie_httponly'), \FILTER_VALIDATE_BOOLEAN));
} else {
$params = session_get_cookie_params();
unset($params['lifetime']);

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,108 @@
<?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;
}
/**
* @return bool
*/
#[\ReturnTypeWillChange]
public function open($savePath, $name)
{
return $this->handler->open($savePath, $name);
}
/**
* @return bool
*/
#[\ReturnTypeWillChange]
public function close()
{
return $this->handler->close();
}
/**
* @return bool
*/
#[\ReturnTypeWillChange]
public function destroy($sessionId)
{
return $this->handler->destroy($sessionId);
}
/**
* @return int|false
*/
#[\ReturnTypeWillChange]
public function gc($maxlifetime)
{
return $this->handler->gc($maxlifetime);
}
/**
* @return string
*/
#[\ReturnTypeWillChange]
public function read($sessionId)
{
return $this->marshaller->unmarshall($this->handler->read($sessionId));
}
/**
* @return bool
*/
#[\ReturnTypeWillChange]
public function write($sessionId, $data)
{
$failed = [];
$marshalledData = $this->marshaller->marshall(['data' => $data], $failed);
if (isset($failed['data'])) {
return false;
}
return $this->handler->write($sessionId, $marshalledData['data']);
}
/**
* @return bool
*/
#[\ReturnTypeWillChange]
public function validateId($sessionId)
{
return $this->handler->validateId($sessionId);
}
/**
* @return bool
*/
#[\ReturnTypeWillChange]
public function updateTimestamp($sessionId, $data)
{
return $this->handler->updateTimestamp($sessionId, $data);
}
}

View File

@@ -15,7 +15,7 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
* 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>
*/
@@ -38,10 +38,7 @@ class MemcachedSessionHandler extends AbstractSessionHandler
*
* 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
*/
@@ -49,17 +46,18 @@ class MemcachedSessionHandler extends AbstractSessionHandler
{
$this->memcached = $memcached;
if ($diff = array_diff(array_keys($options), ['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}
* @return bool
*/
#[\ReturnTypeWillChange]
public function close()
{
return $this->memcached->quit();
@@ -68,17 +66,18 @@ class MemcachedSessionHandler extends AbstractSessionHandler
/**
* {@inheritdoc}
*/
protected function doRead($sessionId)
protected function doRead(string $sessionId)
{
return $this->memcached->get($this->prefix.$sessionId) ?: '';
}
/**
* {@inheritdoc}
* @return bool
*/
#[\ReturnTypeWillChange]
public function updateTimestamp($sessionId, $data)
{
$this->memcached->touch($this->prefix.$sessionId, time() + $this->ttl);
$this->memcached->touch($this->prefix.$sessionId, time() + (int) ($this->ttl ?? ini_get('session.gc_maxlifetime')));
return true;
}
@@ -86,15 +85,15 @@ class MemcachedSessionHandler extends AbstractSessionHandler
/**
* {@inheritdoc}
*/
protected function doWrite($sessionId, $data)
protected function doWrite(string $sessionId, string $data)
{
return $this->memcached->set($this->prefix.$sessionId, $data, time() + $this->ttl);
return $this->memcached->set($this->prefix.$sessionId, $data, time() + (int) ($this->ttl ?? ini_get('session.gc_maxlifetime')));
}
/**
* {@inheritdoc}
*/
protected function doDestroy($sessionId)
protected function doDestroy(string $sessionId)
{
$result = $this->memcached->delete($this->prefix.$sessionId);
@@ -102,12 +101,13 @@ class MemcachedSessionHandler extends AbstractSessionHandler
}
/**
* {@inheritdoc}
* @return int|false
*/
#[\ReturnTypeWillChange]
public function gc($maxlifetime)
{
// not required here because memcached will auto expire the records anyhow.
return true;
return 0;
}
/**

View File

@@ -22,7 +22,14 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
*/
class MigratingSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface
{
/**
* @var \SessionHandlerInterface&\SessionUpdateTimestampHandlerInterface
*/
private $currentHandler;
/**
* @var \SessionHandlerInterface&\SessionUpdateTimestampHandlerInterface
*/
private $writeOnlyHandler;
public function __construct(\SessionHandlerInterface $currentHandler, \SessionHandlerInterface $writeOnlyHandler)
@@ -39,8 +46,9 @@ class MigratingSessionHandler implements \SessionHandlerInterface, \SessionUpdat
}
/**
* {@inheritdoc}
* @return bool
*/
#[\ReturnTypeWillChange]
public function close()
{
$result = $this->currentHandler->close();
@@ -50,8 +58,9 @@ class MigratingSessionHandler implements \SessionHandlerInterface, \SessionUpdat
}
/**
* {@inheritdoc}
* @return bool
*/
#[\ReturnTypeWillChange]
public function destroy($sessionId)
{
$result = $this->currentHandler->destroy($sessionId);
@@ -61,8 +70,9 @@ class MigratingSessionHandler implements \SessionHandlerInterface, \SessionUpdat
}
/**
* {@inheritdoc}
* @return int|false
*/
#[\ReturnTypeWillChange]
public function gc($maxlifetime)
{
$result = $this->currentHandler->gc($maxlifetime);
@@ -72,8 +82,9 @@ class MigratingSessionHandler implements \SessionHandlerInterface, \SessionUpdat
}
/**
* {@inheritdoc}
* @return bool
*/
#[\ReturnTypeWillChange]
public function open($savePath, $sessionName)
{
$result = $this->currentHandler->open($savePath, $sessionName);
@@ -83,8 +94,9 @@ class MigratingSessionHandler implements \SessionHandlerInterface, \SessionUpdat
}
/**
* {@inheritdoc}
* @return string
*/
#[\ReturnTypeWillChange]
public function read($sessionId)
{
// No reading from new handler until switch-over
@@ -92,8 +104,9 @@ class MigratingSessionHandler implements \SessionHandlerInterface, \SessionUpdat
}
/**
* {@inheritdoc}
* @return bool
*/
#[\ReturnTypeWillChange]
public function write($sessionId, $sessionData)
{
$result = $this->currentHandler->write($sessionId, $sessionData);
@@ -103,8 +116,9 @@ class MigratingSessionHandler implements \SessionHandlerInterface, \SessionUpdat
}
/**
* {@inheritdoc}
* @return bool
*/
#[\ReturnTypeWillChange]
public function validateId($sessionId)
{
// No reading from new handler until switch-over
@@ -112,8 +126,9 @@ class MigratingSessionHandler implements \SessionHandlerInterface, \SessionUpdat
}
/**
* {@inheritdoc}
* @return bool
*/
#[\ReturnTypeWillChange]
public function updateTimestamp($sessionId, $sessionData)
{
$result = $this->currentHandler->updateTimestamp($sessionId, $sessionData);

View File

@@ -11,20 +11,25 @@
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
use MongoDB\BSON\Binary;
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
use MongoDB\Collection;
/**
* 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 http://php.net/manual/en/set.mongodb.php
* @see https://php.net/mongodb
*/
class MongoDbSessionHandler extends AbstractSessionHandler
{
private $mongo;
/**
* @var \MongoDB\Collection
* @var Collection
*/
private $collection;
@@ -51,25 +56,22 @@ class MongoDbSessionHandler extends AbstractSessionHandler
* 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 \MongoDB\Client $mongo A MongoDB\Client instance
* @param array $options An associative array of field options
*
* @throws \InvalidArgumentException When "database" or "collection" not provided
*/
public function __construct(\MongoDB\Client $mongo, array $options)
public function __construct(Client $mongo, array $options)
{
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;
@@ -83,8 +85,9 @@ class MongoDbSessionHandler extends AbstractSessionHandler
}
/**
* {@inheritdoc}
* @return bool
*/
#[\ReturnTypeWillChange]
public function close()
{
return true;
@@ -93,7 +96,7 @@ class MongoDbSessionHandler extends AbstractSessionHandler
/**
* {@inheritdoc}
*/
protected function doDestroy($sessionId)
protected function doDestroy(string $sessionId)
{
$this->getCollection()->deleteOne([
$this->options['id_field'] => $sessionId,
@@ -103,28 +106,27 @@ class MongoDbSessionHandler extends AbstractSessionHandler
}
/**
* {@inheritdoc}
* @return int|false
*/
#[\ReturnTypeWillChange]
public function gc($maxlifetime)
{
$this->getCollection()->deleteMany([
$this->options['expiry_field'] => ['$lt' => new \MongoDB\BSON\UTCDateTime()],
]);
return true;
return $this->getCollection()->deleteMany([
$this->options['expiry_field'] => ['$lt' => new UTCDateTime()],
])->getDeletedCount();
}
/**
* {@inheritdoc}
*/
protected function doWrite($sessionId, $data)
protected function doWrite(string $sessionId, string $data)
{
$expiry = new \MongoDB\BSON\UTCDateTime((time() + (int) ini_get('session.gc_maxlifetime')) * 1000);
$expiry = new UTCDateTime((time() + (int) ini_get('session.gc_maxlifetime')) * 1000);
$fields = [
$this->options['time_field'] => new \MongoDB\BSON\UTCDateTime(),
$this->options['time_field'] => new UTCDateTime(),
$this->options['expiry_field'] => $expiry,
$this->options['data_field'] => new \MongoDB\BSON\Binary($data, \MongoDB\BSON\Binary::TYPE_OLD_BINARY),
$this->options['data_field'] => new Binary($data, Binary::TYPE_OLD_BINARY),
];
$this->getCollection()->updateOne(
@@ -137,16 +139,17 @@ class MongoDbSessionHandler extends AbstractSessionHandler
}
/**
* {@inheritdoc}
* @return bool
*/
#[\ReturnTypeWillChange]
public function updateTimestamp($sessionId, $data)
{
$expiry = new \MongoDB\BSON\UTCDateTime((time() + (int) ini_get('session.gc_maxlifetime')) * 1000);
$expiry = new UTCDateTime((time() + (int) ini_get('session.gc_maxlifetime')) * 1000);
$this->getCollection()->updateOne(
[$this->options['id_field'] => $sessionId],
['$set' => [
$this->options['time_field'] => new \MongoDB\BSON\UTCDateTime(),
$this->options['time_field'] => new UTCDateTime(),
$this->options['expiry_field'] => $expiry,
]]
);
@@ -157,11 +160,11 @@ class MongoDbSessionHandler extends AbstractSessionHandler
/**
* {@inheritdoc}
*/
protected function doRead($sessionId)
protected function doRead(string $sessionId)
{
$dbData = $this->getCollection()->findOne([
$this->options['id_field'] => $sessionId,
$this->options['expiry_field'] => ['$gte' => new \MongoDB\BSON\UTCDateTime()],
$this->options['expiry_field'] => ['$gte' => new UTCDateTime()],
]);
if (null === $dbData) {
@@ -171,10 +174,7 @@ class MongoDbSessionHandler extends AbstractSessionHandler
return $dbData[$this->options['data_field']]->getData();
}
/**
* @return \MongoDB\Collection
*/
private function getCollection()
private function getCollection(): Collection
{
if (null === $this->collection) {
$this->collection = $this->mongo->selectCollection($this->options['database'], $this->options['collection']);
@@ -184,7 +184,7 @@ class MongoDbSessionHandler extends AbstractSessionHandler
}
/**
* @return \MongoDB\Client
* @return Client
*/
protected function getMongo()
{

View File

@@ -23,7 +23,7 @@ class NativeFileSessionHandler extends \SessionHandler
* 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
@@ -38,7 +38,7 @@ class NativeFileSessionHandler extends \SessionHandler
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
@@ -46,7 +46,7 @@ class NativeFileSessionHandler extends \SessionHandler
}
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

@@ -19,16 +19,18 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
class NullSessionHandler extends AbstractSessionHandler
{
/**
* {@inheritdoc}
* @return bool
*/
#[\ReturnTypeWillChange]
public function close()
{
return true;
}
/**
* {@inheritdoc}
* @return bool
*/
#[\ReturnTypeWillChange]
public function validateId($sessionId)
{
return true;
@@ -37,14 +39,15 @@ class NullSessionHandler extends AbstractSessionHandler
/**
* {@inheritdoc}
*/
protected function doRead($sessionId)
protected function doRead(string $sessionId)
{
return '';
}
/**
* {@inheritdoc}
* @return bool
*/
#[\ReturnTypeWillChange]
public function updateTimestamp($sessionId, $data)
{
return true;
@@ -53,7 +56,7 @@ class NullSessionHandler extends AbstractSessionHandler
/**
* {@inheritdoc}
*/
protected function doWrite($sessionId, $data)
protected function doWrite(string $sessionId, string $data)
{
return true;
}
@@ -61,16 +64,17 @@ class NullSessionHandler extends AbstractSessionHandler
/**
* {@inheritdoc}
*/
protected function doDestroy($sessionId)
protected function doDestroy(string $sessionId)
{
return true;
}
/**
* {@inheritdoc}
* @return int|false
*/
#[\ReturnTypeWillChange]
public function gc($maxlifetime)
{
return true;
return 0;
}
}

View File

@@ -32,7 +32,7 @@ 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>
@@ -46,7 +46,7 @@ class PdoSessionHandler extends AbstractSessionHandler
* 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 extends AbstractSessionHandler
* 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,7 +63,9 @@ class PdoSessionHandler extends AbstractSessionHandler
* 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;
private const MAX_LIFETIME = 315576000;
/**
* @var \PDO|null PDO instance or null when not connected yet
@@ -71,57 +73,67 @@ class PdoSessionHandler extends AbstractSessionHandler
private $pdo;
/**
* @var string|false|null 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.
*
* @var string|false|null
*/
private $dsn = false;
/**
* @var string Database driver
* @var string|null
*/
private $driver;
/**
* @var string Table name
* @var string
*/
private $table = 'sessions';
/**
* @var string Column for session id
* @var string
*/
private $idCol = 'sess_id';
/**
* @var string Column for session data
* @var string
*/
private $dataCol = 'sess_data';
/**
* @var string Column for lifetime
* @var string
*/
private $lifetimeCol = 'sess_lifetime';
/**
* @var string Column for timestamp
* @var string
*/
private $timeCol = 'sess_time';
/**
* @var string Username when lazy-connect
* Username when lazy-connect.
*
* @var string
*/
private $username = '';
/**
* @var string Password when lazy-connect
* Password when lazy-connect.
*
* @var string
*/
private $password = '';
/**
* @var array Connection options when lazy-connect
* Connection options when lazy-connect.
*
* @var array
*/
private $connectionOptions = [];
/**
* @var int The strategy for locking, see constants
* The strategy for locking, see constants.
*
* @var int
*/
private $lockMode = self::LOCK_TRANSACTIONAL;
@@ -133,17 +145,23 @@ class PdoSessionHandler extends AbstractSessionHandler
private $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.
*
* @var bool
*/
private $sessionExpired = false;
/**
* @var bool Whether a transaction is active
* Whether a transaction is active.
*
* @var bool
*/
private $inTransaction = false;
/**
* @var bool Whether gc() has been called
* Whether gc() has been called.
*
* @var bool
*/
private $gcCalled = false;
@@ -165,7 +183,6 @@ class PdoSessionHandler extends AbstractSessionHandler
* * lock_mode: The strategy for locking, see constants [default: LOCK_TRANSACTIONAL]
*
* @param \PDO|string|null $pdoOrDsn A \PDO instance or DSN string or URL string or null
* @param array $options An associative array of options
*
* @throws \InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION
*/
@@ -173,26 +190,26 @@ class PdoSessionHandler extends AbstractSessionHandler
{
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) && false !== strpos($pdoOrDsn, '://')) {
} 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 +235,7 @@ class PdoSessionHandler extends AbstractSessionHandler
// - 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 +255,7 @@ class PdoSessionHandler extends AbstractSessionHandler
try {
$this->pdo->exec($sql);
$this->pdo->exec("CREATE INDEX EXPIRY ON $this->table ($this->lifetimeCol)");
} catch (\PDOException $e) {
$this->rollback();
@@ -250,7 +268,7 @@ class PdoSessionHandler extends AbstractSessionHandler
*
* Can be used to distinguish between a new session and one that expired due to inactivity.
*
* @return bool Whether current session expired
* @return bool
*/
public function isSessionExpired()
{
@@ -258,8 +276,9 @@ class PdoSessionHandler extends AbstractSessionHandler
}
/**
* {@inheritdoc}
* @return bool
*/
#[\ReturnTypeWillChange]
public function open($savePath, $sessionName)
{
$this->sessionExpired = false;
@@ -272,8 +291,9 @@ class PdoSessionHandler extends AbstractSessionHandler
}
/**
* {@inheritdoc}
* @return string
*/
#[\ReturnTypeWillChange]
public function read($sessionId)
{
try {
@@ -286,21 +306,22 @@ class PdoSessionHandler extends AbstractSessionHandler
}
/**
* {@inheritdoc}
* @return int|false
*/
#[\ReturnTypeWillChange]
public function gc($maxlifetime)
{
// 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}
*/
protected function doDestroy($sessionId)
protected function doDestroy(string $sessionId)
{
// delete the record associated with this id
$sql = "DELETE FROM $this->table WHERE $this->idCol = :id";
@@ -321,7 +342,7 @@ class PdoSessionHandler extends AbstractSessionHandler
/**
* {@inheritdoc}
*/
protected function doWrite($sessionId, $data)
protected function doWrite(string $sessionId, string $data)
{
$maxlifetime = (int) ini_get('session.gc_maxlifetime');
@@ -348,7 +369,7 @@ class PdoSessionHandler extends AbstractSessionHandler
$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;
@@ -365,18 +386,19 @@ class PdoSessionHandler extends AbstractSessionHandler
}
/**
* {@inheritdoc}
* @return bool
*/
#[\ReturnTypeWillChange]
public function updateTimestamp($sessionId, $data)
{
$maxlifetime = (int) ini_get('session.gc_maxlifetime');
$expiry = time() + (int) ini_get('session.gc_maxlifetime');
try {
$updateStmt = $this->pdo->prepare(
"UPDATE $this->table SET $this->lifetimeCol = :lifetime, $this->timeCol = :time WHERE $this->idCol = :id"
"UPDATE $this->table SET $this->lifetimeCol = :expiry, $this->timeCol = :time WHERE $this->idCol = :id"
);
$updateStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
$updateStmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT);
$updateStmt->bindParam(':expiry', $expiry, \PDO::PARAM_INT);
$updateStmt->bindValue(':time', time(), \PDO::PARAM_INT);
$updateStmt->execute();
} catch (\PDOException $e) {
@@ -389,8 +411,9 @@ class PdoSessionHandler extends AbstractSessionHandler
}
/**
* {@inheritdoc}
* @return bool
*/
#[\ReturnTypeWillChange]
public function close()
{
$this->commit();
@@ -403,19 +426,27 @@ class PdoSessionHandler extends AbstractSessionHandler
$this->gcCalled = false;
// delete the session records that have expired
if ('mysql' === $this->driver) {
$sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol < :time";
} else {
$sql = "DELETE FROM $this->table WHERE $this->lifetimeCol < :time - $this->timeCol";
}
$sql = "DELETE FROM $this->table WHERE $this->lifetimeCol < :time AND $this->lifetimeCol > :min";
$stmt = $this->pdo->prepare($sql);
$stmt->bindValue(':time', time(), \PDO::PARAM_INT);
$stmt->bindValue(':min', self::MAX_LIFETIME, \PDO::PARAM_INT);
$stmt->execute();
// to be removed in 6.0
if ('mysql' === $this->driver) {
$legacySql = "DELETE FROM $this->table WHERE $this->lifetimeCol <= :min AND $this->lifetimeCol + $this->timeCol < :time";
} else {
$legacySql = "DELETE FROM $this->table WHERE $this->lifetimeCol <= :min AND $this->lifetimeCol < :time - $this->timeCol";
}
$stmt = $this->pdo->prepare($legacySql);
$stmt->bindValue(':time', time(), \PDO::PARAM_INT);
$stmt->bindValue(':min', self::MAX_LIFETIME, \PDO::PARAM_INT);
$stmt->execute();
}
if (false !== $this->dsn) {
$this->pdo = null; // only close lazy-connection
$this->driver = null;
}
return true;
@@ -423,10 +454,8 @@ class PdoSessionHandler extends AbstractSessionHandler
/**
* 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);
@@ -436,13 +465,9 @@ class PdoSessionHandler extends AbstractSessionHandler
/**
* Builds a PDO DSN from a URL-like connection string.
*
* @param string $dsnOrUrl
*
* @return string
*
* @todo implement missing support for oci DSN (which look totally different from other PDO ones)
*/
private function buildDsnFromUrl($dsnOrUrl)
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);
@@ -465,7 +490,7 @@ class PdoSessionHandler extends AbstractSessionHandler
}
if (!isset($params['scheme'])) {
throw new \InvalidArgumentException('URLs without scheme are not supported to configure the PdoSessionHandler');
throw new \InvalidArgumentException('URLs without scheme are not supported to configure the PdoSessionHandler.');
}
$driverAliasMap = [
@@ -476,17 +501,39 @@ class PdoSessionHandler extends AbstractSessionHandler
'sqlite3' => 'sqlite',
];
$driver = isset($driverAliasMap[$params['scheme']]) ? $driverAliasMap[$params['scheme']] : $params['scheme'];
$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 (0 === strpos($driver, 'pdo_') || 0 === strpos($driver, 'pdo-')) {
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 = $driver.':';
$dsn ?? $dsn = 'pgsql:';
if (isset($params['host']) && '' !== $params['host']) {
$dsn .= 'host='.$params['host'].';';
@@ -538,10 +585,10 @@ class PdoSessionHandler extends AbstractSessionHandler
* 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) {
@@ -559,7 +606,7 @@ class PdoSessionHandler extends AbstractSessionHandler
/**
* Helper method to commit a transaction.
*/
private function commit()
private function commit(): void
{
if ($this->inTransaction) {
try {
@@ -581,7 +628,7 @@ class PdoSessionHandler extends AbstractSessionHandler
/**
* 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
@@ -603,11 +650,9 @@ class PdoSessionHandler extends AbstractSessionHandler
* 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
* @return string
*/
protected function doRead($sessionId)
protected function doRead(string $sessionId)
{
if (self::LOCK_ADVISORY === $this->lockMode) {
$this->unlockStatements[] = $this->doAdvisoryLock($sessionId);
@@ -618,12 +663,17 @@ class PdoSessionHandler extends AbstractSessionHandler
$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 <= self::MAX_LIFETIME) {
$expiry += $sessionRows[0][2];
}
if ($expiry < time()) {
$this->sessionExpired = true;
return '';
@@ -637,7 +687,7 @@ class PdoSessionHandler extends AbstractSessionHandler
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) {
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
@@ -648,7 +698,7 @@ class PdoSessionHandler extends AbstractSessionHandler
} 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.
@@ -662,7 +712,7 @@ class PdoSessionHandler extends AbstractSessionHandler
}
return '';
} while (true);
}
}
/**
@@ -676,12 +726,12 @@ class PdoSessionHandler extends AbstractSessionHandler
* - for oci using DBMS_LOCK.REQUEST
* - for sqlsrv using sp_getapplock with LockOwner = Session
*/
private function doAdvisoryLock(string $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);
$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)');
@@ -754,6 +804,7 @@ class PdoSessionHandler extends AbstractSessionHandler
if (self::LOCK_TRANSACTIONAL === $this->lockMode) {
$this->beginTransaction();
// selecting the time column should be removed in 6.0
switch ($this->driver) {
case 'mysql':
case 'oci':
@@ -774,32 +825,26 @@ class PdoSessionHandler extends AbstractSessionHandler
/**
* Returns an insert statement supported by the database for writing session data.
*
* @param string $sessionId Session ID
* @param string $sessionData Encoded session data
* @param int $maxlifetime session.gc_maxlifetime
*
* @return \PDOStatement The insert statement
*/
private function getInsertStatement($sessionId, $sessionData, $maxlifetime)
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(), :lifetime, :time) RETURNING $this->dataCol into :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, :lifetime, :time)";
$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->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT);
$stmt->bindValue(':expiry', time() + $maxlifetime, \PDO::PARAM_INT);
$stmt->bindValue(':time', time(), \PDO::PARAM_INT);
return $stmt;
@@ -807,32 +852,26 @@ class PdoSessionHandler extends AbstractSessionHandler
/**
* Returns an update statement supported by the database for writing session data.
*
* @param string $sessionId Session ID
* @param string $sessionData Encoded session data
* @param int $maxlifetime session.gc_maxlifetime
*
* @return \PDOStatement The update statement
*/
private function getUpdateStatement($sessionId, $sessionData, $maxlifetime)
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 = :lifetime, $this->timeCol = :time WHERE $this->idCol = :id RETURNING $this->dataCol into :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 = :lifetime, $this->timeCol = :time WHERE $this->idCol = :id";
$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->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT);
$stmt->bindValue(':expiry', time() + $maxlifetime, \PDO::PARAM_INT);
$stmt->bindValue(':time', time(), \PDO::PARAM_INT);
return $stmt;
@@ -845,25 +884,25 @@ class PdoSessionHandler extends AbstractSessionHandler
{
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 '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: http://www.oracle.com/technetwork/articles/fuecks-lobs-095315.html
// MERGE is not supported with LOBs: https://oracle.com/technetwork/articles/fuecks-lobs-095315.html
return null;
}
@@ -873,15 +912,15 @@ class PdoSessionHandler extends AbstractSessionHandler
$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(4, time() + $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(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->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT);
$mergeStmt->bindValue(':expiry', time() + $maxlifetime, \PDO::PARAM_INT);
$mergeStmt->bindValue(':time', time(), \PDO::PARAM_INT);
}

View File

@@ -30,12 +30,17 @@ class RedisSessionHandler extends AbstractSessionHandler
*/
private $prefix;
/**
* @var int Time to live in seconds
*/
private $ttl;
/**
* List of available options:
* * prefix: The prefix to use for the keys in order to avoid collision on the Redis server.
* * prefix: The prefix to use for the keys in order to avoid collision on the Redis server
* * ttl: The time to live in seconds.
*
* @param \Redis|\RedisArray|\RedisCluster|\Predis\Client|RedisProxy $redis
* @param array $options An associative array of options
* @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy $redis
*
* @throws \InvalidArgumentException When unsupported client or options are passed
*/
@@ -45,25 +50,26 @@ class RedisSessionHandler extends AbstractSessionHandler
!$redis instanceof \Redis &&
!$redis instanceof \RedisArray &&
!$redis instanceof \RedisCluster &&
!$redis instanceof \Predis\Client &&
!$redis instanceof \Predis\ClientInterface &&
!$redis instanceof RedisProxy &&
!$redis instanceof RedisClusterProxy
) {
throw new \InvalidArgumentException(sprintf('%s() expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\Client, %s given', __METHOD__, \is_object($redis) ? \get_class($redis) : \gettype($redis)));
throw new \InvalidArgumentException(sprintf('"%s()" expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\ClientInterface, "%s" given.', __METHOD__, get_debug_type($redis)));
}
if ($diff = array_diff(array_keys($options), ['prefix'])) {
throw new \InvalidArgumentException(sprintf('The following options are not supported "%s"', implode(', ', $diff)));
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($sessionId): string
protected function doRead(string $sessionId): string
{
return $this->redis->get($this->prefix.$sessionId) ?: '';
}
@@ -71,9 +77,9 @@ class RedisSessionHandler extends AbstractSessionHandler
/**
* {@inheritdoc}
*/
protected function doWrite($sessionId, $data): bool
protected function doWrite(string $sessionId, string $data): bool
{
$result = $this->redis->setEx($this->prefix.$sessionId, (int) ini_get('session.gc_maxlifetime'), $data);
$result = $this->redis->setEx($this->prefix.$sessionId, (int) ($this->ttl ?? ini_get('session.gc_maxlifetime')), $data);
return $result && !$result instanceof ErrorInterface;
}
@@ -81,9 +87,21 @@ class RedisSessionHandler extends AbstractSessionHandler
/**
* {@inheritdoc}
*/
protected function doDestroy($sessionId): bool
protected function doDestroy(string $sessionId): bool
{
$this->redis->del($this->prefix.$sessionId);
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;
}
@@ -91,6 +109,7 @@ class RedisSessionHandler extends AbstractSessionHandler
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function close(): bool
{
return true;
@@ -98,17 +117,21 @@ class RedisSessionHandler extends AbstractSessionHandler
/**
* {@inheritdoc}
*
* @return int|false
*/
public function gc($maxlifetime): bool
#[\ReturnTypeWillChange]
public function gc($maxlifetime)
{
return true;
return 0;
}
/**
* {@inheritdoc}
* @return bool
*/
#[\ReturnTypeWillChange]
public function updateTimestamp($sessionId, $data)
{
return (bool) $this->redis->expire($this->prefix.$sessionId, (int) ini_get('session.gc_maxlifetime'));
return (bool) $this->redis->expire($this->prefix.$sessionId, (int) ($this->ttl ?? ini_get('session.gc_maxlifetime')));
}
}

View File

@@ -0,0 +1,91 @@
<?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
{
/**
* @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy|\Memcached|\PDO|string $connection Connection or DSN
*/
public static function createHandler($connection): AbstractSessionHandler
{
if (!\is_string($connection) && !\is_object($connection)) {
throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be a string or a connection object, "%s" given.', __METHOD__, get_debug_type($connection)));
}
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

@@ -24,15 +24,16 @@ class StrictSessionHandler extends AbstractSessionHandler
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_class($handler), self::class));
throw new \LogicException(sprintf('"%s" is already an instance of "SessionUpdateTimestampHandlerInterface", you cannot wrap it with "%s".', get_debug_type($handler), self::class));
}
$this->handler = $handler;
}
/**
* {@inheritdoc}
* @return bool
*/
#[\ReturnTypeWillChange]
public function open($savePath, $sessionName)
{
parent::open($savePath, $sessionName);
@@ -43,14 +44,15 @@ class StrictSessionHandler extends AbstractSessionHandler
/**
* {@inheritdoc}
*/
protected function doRead($sessionId)
protected function doRead(string $sessionId)
{
return $this->handler->read($sessionId);
}
/**
* {@inheritdoc}
* @return bool
*/
#[\ReturnTypeWillChange]
public function updateTimestamp($sessionId, $data)
{
return $this->write($sessionId, $data);
@@ -59,14 +61,15 @@ class StrictSessionHandler extends AbstractSessionHandler
/**
* {@inheritdoc}
*/
protected function doWrite($sessionId, $data)
protected function doWrite(string $sessionId, string $data)
{
return $this->handler->write($sessionId, $data);
}
/**
* {@inheritdoc}
* @return bool
*/
#[\ReturnTypeWillChange]
public function destroy($sessionId)
{
$this->doDestroy = true;
@@ -78,7 +81,7 @@ class StrictSessionHandler extends AbstractSessionHandler
/**
* {@inheritdoc}
*/
protected function doDestroy($sessionId)
protected function doDestroy(string $sessionId)
{
$this->doDestroy = false;
@@ -86,16 +89,18 @@ class StrictSessionHandler extends AbstractSessionHandler
}
/**
* {@inheritdoc}
* @return bool
*/
#[\ReturnTypeWillChange]
public function close()
{
return $this->handler->close();
}
/**
* {@inheritdoc}
* @return int|false
*/
#[\ReturnTypeWillChange]
public function gc($maxlifetime)
{
return $this->handler->gc($maxlifetime);

View File

@@ -22,9 +22,9 @@ 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
@@ -100,7 +100,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);
}
@@ -139,6 +139,7 @@ class MetadataBag implements SessionBagInterface
public function clear()
{
// nothing to do
return null;
}
/**
@@ -151,18 +152,16 @@ class MetadataBag implements SessionBagInterface
/**
* 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

@@ -94,7 +94,7 @@ class MockArraySessionStorage implements SessionStorageInterface
/**
* {@inheritdoc}
*/
public function regenerate($destroy = false, $lifetime = null)
public function regenerate(bool $destroy = false, int $lifetime = null)
{
if (!$this->started) {
$this->start();
@@ -117,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.');
@@ -137,7 +137,7 @@ class MockArraySessionStorage implements SessionStorageInterface
/**
* {@inheritdoc}
*/
public function setName($name)
public function setName(string $name)
{
$this->name = $name;
}
@@ -148,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;
@@ -183,10 +183,10 @@ class MockArraySessionStorage implements SessionStorageInterface
/**
* {@inheritdoc}
*/
public function getBag($name)
public function getBag(string $name)
{
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) {
@@ -242,7 +242,7 @@ class MockArraySessionStorage implements SessionStorageInterface
foreach ($bags as $bag) {
$key = $bag->getStorageKey();
$this->data[$key] = isset($this->data[$key]) ? $this->data[$key] : [];
$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
@@ -27,9 +28,7 @@ class MockFileSessionStorage extends MockArraySessionStorage
private $savePath;
/**
* @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(string $savePath = null, string $name = 'MOCKSESSID', MetadataBag $metaBag = null)
{
@@ -38,7 +37,7 @@ class MockFileSessionStorage extends MockArraySessionStorage
}
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;
@@ -69,7 +68,7 @@ class MockFileSessionStorage extends MockArraySessionStorage
/**
* {@inheritdoc}
*/
public function regenerate($destroy = false, $lifetime = null)
public function regenerate(bool $destroy = false, int $lifetime = null)
{
if (!$this->started) {
$this->start();
@@ -88,7 +87,7 @@ 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.');
}
$data = $this->data;
@@ -104,7 +103,10 @@ class MockFileSessionStorage extends MockArraySessionStorage
try {
if ($data) {
file_put_contents($this->getFilePath(), serialize($data));
$path = $this->getFilePath();
$tmp = $path.bin2hex(random_bytes(6));
file_put_contents($tmp, serialize($data));
rename($tmp, $path);
} else {
$this->destroy();
}
@@ -112,9 +114,8 @@ class MockFileSessionStorage extends MockArraySessionStorage
$this->data = $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
// this is needed when the session object is re-used across multiple requests
// in functional tests.
$this->started = false;
}
@@ -122,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';
}
@@ -142,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)) : [];
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 $savePath;
private $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

@@ -17,6 +17,11 @@ use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandle
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.
*
@@ -60,7 +65,7 @@ class NativeSessionStorage implements SessionStorageInterface
*
* 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
@@ -81,28 +86,23 @@ class NativeSessionStorage implements SessionStorageInterface
* 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 \SessionHandlerInterface|null $handler
* @param MetadataBag $metaBag MetadataBag
* @param AbstractProxy|\SessionHandlerInterface|null $handler
*/
public function __construct(array $options = [], $handler = null, MetadataBag $metaBag = null)
{
if (!\extension_loaded('session')) {
throw new \LogicException('PHP extension "session" is required.');
}
$options += [
'cache_limiter' => '',
'cache_expire' => 0,
@@ -141,13 +141,13 @@ class NativeSessionStorage implements SessionStorageInterface
throw new \RuntimeException('Failed to start the session: already started by PHP.');
}
if (filter_var(ini_get('session.use_cookies'), FILTER_VALIDATE_BOOLEAN) && 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));
}
// 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.');
}
if (null !== $this->emulateSameSite) {
@@ -173,7 +173,7 @@ class NativeSessionStorage implements SessionStorageInterface
/**
* {@inheritdoc}
*/
public function setId($id)
public function setId(string $id)
{
$this->saveHandler->setId($id);
}
@@ -189,7 +189,7 @@ class NativeSessionStorage implements SessionStorageInterface
/**
* {@inheritdoc}
*/
public function setName($name)
public function setName(string $name)
{
$this->saveHandler->setName($name);
}
@@ -197,7 +197,7 @@ class NativeSessionStorage implements SessionStorageInterface
/**
* {@inheritdoc}
*/
public function regenerate($destroy = false, $lifetime = null)
public function regenerate(bool $destroy = false, int $lifetime = null)
{
// Cannot regenerate the session ID for non-active sessions.
if (\PHP_SESSION_ACTIVE !== session_status()) {
@@ -208,8 +208,10 @@ class NativeSessionStorage implements SessionStorageInterface
return false;
}
if (null !== $lifetime) {
if (null !== $lifetime && $lifetime != ini_get('session.cookie_lifetime')) {
$this->save();
ini_set('session.cookie_lifetime', $lifetime);
$this->start();
}
if ($destroy) {
@@ -218,10 +220,6 @@ class NativeSessionStorage implements SessionStorageInterface
$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();
if (null !== $this->emulateSameSite) {
$originalCookie = SessionUtils::popSessionCookie(session_name(), session_id());
if (null !== $originalCookie) {
@@ -237,6 +235,7 @@ class NativeSessionStorage implements SessionStorageInterface
*/
public function save()
{
// Store a copy so we can restore the bags in case the session was not left empty
$session = $_SESSION;
foreach ($this->bags as $bag) {
@@ -250,7 +249,7 @@ class NativeSessionStorage implements SessionStorageInterface
// 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 && 0 === strpos($msg, 'session_write_close():')) {
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));
}
@@ -262,7 +261,11 @@ class NativeSessionStorage implements SessionStorageInterface
session_write_close();
} finally {
restore_error_handler();
$_SESSION = $session;
// Restore only if not empty
if ($_SESSION) {
$_SESSION = $session;
}
}
$this->closed = true;
@@ -301,10 +304,10 @@ class NativeSessionStorage implements SessionStorageInterface
/**
* {@inheritdoc}
*/
public function getBag($name)
public function getBag(string $name)
{
if (!isset($this->bags[$name])) {
throw new \InvalidArgumentException(sprintf('The SessionBagInterface %s is not registered.', $name));
throw new \InvalidArgumentException(sprintf('The SessionBagInterface "%s" is not registered.', $name));
}
if (!$this->started && $this->saveHandler->isActive()) {
@@ -351,7 +354,7 @@ class NativeSessionStorage implements SessionStorageInterface
*
* @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)
{
@@ -373,12 +376,22 @@ class NativeSessionStorage implements SessionStorageInterface
foreach ($options as $key => $value) {
if (isset($validOptions[$key])) {
if (str_starts_with($key, 'upload_progress.')) {
trigger_deprecation('symfony/http-foundation', '5.4', 'Support for the "%s" session option is deprecated. The settings prefixed with "session.upload_progress." can not be changed at runtime.', $key);
continue;
}
if ('url_rewriter.tags' === $key) {
trigger_deprecation('symfony/http-foundation', '5.4', 'Support for the "%s" session option is deprecated. Use "trans_sid_tags" instead.', $key);
}
if ('cookie_samesite' === $key && \PHP_VERSION_ID < 70300) {
// PHP < 7.3 does not support same_site cookies. We will emulate it in
// the start() method instead.
$this->emulateSameSite = $value;
continue;
}
if ('cookie_secure' === $key && 'auto' === $value) {
continue;
}
ini_set('url_rewriter.tags' !== $key ? 'session.'.$key : $key, $value);
}
}
@@ -394,15 +407,13 @@ class NativeSessionStorage implements SessionStorageInterface
* ini_set('session.save_path', '/tmp');
*
* or pass in a \SessionHandler instance which configures session.save_handler in the
* constructor, for a template see NativeFileSessionHandler or use handlers in
* composer package drak/native-session
* 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
* @see https://php.net/session-set-save-handler
* @see https://php.net/sessionhandlerinterface
* @see https://php.net/sessionhandler
*
* @param \SessionHandlerInterface|null $saveHandler
* @param AbstractProxy|\SessionHandlerInterface|null $saveHandler
*
* @throws \InvalidArgumentException
*/
@@ -449,7 +460,7 @@ class NativeSessionStorage implements SessionStorageInterface
foreach ($bags as $bag) {
$key = $bag->getStorageKey();
$session[$key] = isset($session[$key]) ? $session[$key] : [];
$session[$key] = isset($session[$key]) && \is_array($session[$key]) ? $session[$key] : [];
$bag->initialize($session[$key]);
}

View File

@@ -0,0 +1,49 @@
<?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(NativeSessionStorage::class);
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class NativeSessionStorageFactory implements SessionStorageFactoryInterface
{
private $options;
private $handler;
private $metaBag;
private $secure;
/**
* @see NativeSessionStorage constructor.
*/
public function __construct(array $options = [], $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

@@ -11,6 +11,8 @@
namespace Symfony\Component\HttpFoundation\Session\Storage;
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy;
/**
* Allows session to be started by PHP and managed by Symfony.
*
@@ -19,11 +21,14 @@ namespace Symfony\Component\HttpFoundation\Session\Storage;
class PhpBridgeSessionStorage extends NativeSessionStorage
{
/**
* @param \SessionHandlerInterface|null $handler
* @param MetadataBag $metaBag MetadataBag
* @param AbstractProxy|\SessionHandlerInterface|null $handler
*/
public function __construct($handler = null, MetadataBag $metaBag = null)
{
if (!\extension_loaded('session')) {
throw new \LogicException('PHP extension "session" is required.');
}
$this->setMetadataBag($metaBag);
$this->setSaveHandler($handler);
}

View File

@@ -0,0 +1,47 @@
<?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(PhpBridgeSessionStorage::class);
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class PhpBridgeSessionStorageFactory implements SessionStorageFactoryInterface
{
private $handler;
private $metaBag;
private $secure;
/**
* @see PhpBridgeSessionStorage constructor.
*/
public function __construct($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

@@ -31,7 +31,7 @@ abstract class AbstractProxy
/**
* Gets the session.save_handler name.
*
* @return string
* @return string|null
*/
public function getSaveHandlerName()
{
@@ -81,14 +81,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);
@@ -107,14 +105,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

@@ -21,7 +21,7 @@ class SessionHandlerProxy extends AbstractProxy implements \SessionHandlerInterf
public function __construct(\SessionHandlerInterface $handler)
{
$this->handler = $handler;
$this->wrapper = ($handler instanceof \SessionHandler);
$this->wrapper = $handler instanceof \SessionHandler;
$this->saveHandlerName = $this->wrapper ? ini_get('session.save_handler') : 'user';
}
@@ -36,64 +36,72 @@ class SessionHandlerProxy extends AbstractProxy implements \SessionHandlerInterf
// \SessionHandlerInterface
/**
* {@inheritdoc}
* @return bool
*/
#[\ReturnTypeWillChange]
public function open($savePath, $sessionName)
{
return (bool) $this->handler->open($savePath, $sessionName);
return $this->handler->open($savePath, $sessionName);
}
/**
* {@inheritdoc}
* @return bool
*/
#[\ReturnTypeWillChange]
public function close()
{
return (bool) $this->handler->close();
return $this->handler->close();
}
/**
* {@inheritdoc}
* @return string|false
*/
#[\ReturnTypeWillChange]
public function read($sessionId)
{
return (string) $this->handler->read($sessionId);
return $this->handler->read($sessionId);
}
/**
* {@inheritdoc}
* @return bool
*/
#[\ReturnTypeWillChange]
public function write($sessionId, $data)
{
return (bool) $this->handler->write($sessionId, $data);
return $this->handler->write($sessionId, $data);
}
/**
* {@inheritdoc}
* @return bool
*/
#[\ReturnTypeWillChange]
public function destroy($sessionId)
{
return (bool) $this->handler->destroy($sessionId);
return $this->handler->destroy($sessionId);
}
/**
* {@inheritdoc}
* @return int|false
*/
#[\ReturnTypeWillChange]
public function gc($maxlifetime)
{
return (bool) $this->handler->gc($maxlifetime);
return $this->handler->gc($maxlifetime);
}
/**
* {@inheritdoc}
* @return bool
*/
#[\ReturnTypeWillChange]
public function validateId($sessionId)
{
return !$this->handler instanceof \SessionUpdateTimestampHandlerInterface || $this->handler->validateId($sessionId);
}
/**
* {@inheritdoc}
* @return bool
*/
#[\ReturnTypeWillChange]
public function updateTimestamp($sessionId, $data)
{
return $this->handler instanceof \SessionUpdateTimestampHandlerInterface ? $this->handler->updateTimestamp($sessionId, $data) : $this->write($sessionId, $data);

View File

@@ -0,0 +1,38 @@
<?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>
*
* @internal to be removed in Symfony 6
*/
final class ServiceSessionFactory implements SessionStorageFactoryInterface
{
private $storage;
public function __construct(SessionStorageInterface $storage)
{
$this->storage = $storage;
}
public function createStorage(?Request $request): SessionStorageInterface
{
if ($this->storage instanceof NativeSessionStorage && $request && $request->isSecure()) {
$this->storage->setOptions(['cookie_secure' => true]);
}
return $this->storage;
}
}

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,7 +24,7 @@ interface SessionStorageInterface
/**
* Starts the session.
*
* @return bool True if started
* @return bool
*
* @throws \RuntimeException if something goes wrong starting the session
*/
@@ -33,37 +33,33 @@ interface SessionStorageInterface
/**
* Checks if the session is started.
*
* @return bool True if started, false otherwise
* @return bool
*/
public function isStarted();
/**
* Returns the session ID.
*
* @return string The session ID or empty
* @return string
*/
public function getId();
/**
* 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
* @return string
*/
public function getName();
/**
* Sets the session name.
*
* @param string $name
*/
public function setName($name);
public function setName(string $name);
/**
* Regenerates id that represents this storage.
@@ -77,7 +73,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 +86,11 @@ 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
* @return bool
*
* @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);
/**
* Force the session to be saved and closed.
@@ -117,13 +113,11 @@ 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);
/**
* Registers a SessionBagInterface for use.

View File

@@ -30,11 +30,6 @@ class StreamedResponse extends Response
protected $streamed;
private $headersSent;
/**
* @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, int $status = 200, array $headers = [])
{
parent::__construct(null, $status, $headers);
@@ -50,21 +45,21 @@ class StreamedResponse extends Response
* 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
*
* @deprecated since Symfony 5.1, use __construct() instead.
*/
public static function create($callback = null, $status = 200, $headers = [])
public static function create($callback = null, int $status = 200, array $headers = [])
{
trigger_deprecation('symfony/http-foundation', '5.1', 'The "%s()" method is deprecated, use "new %s()" instead.', __METHOD__, static::class);
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)
@@ -123,7 +118,7 @@ class StreamedResponse extends Response
*
* @return $this
*/
public function setContent($content)
public function setContent(?string $content)
{
if (null !== $content) {
throw new \LogicException('The content cannot be set on a StreamedResponse instance.');
@@ -136,8 +131,6 @@ class StreamedResponse extends Response
/**
* {@inheritdoc}
*
* @return false
*/
public function getContent()
{

View File

@@ -12,6 +12,7 @@
namespace Symfony\Component\HttpFoundation\Test\Constraint;
use PHPUnit\Framework\Constraint\Constraint;
use Symfony\Component\HttpFoundation\Request;
final class RequestAttributeValueSame extends Constraint
{

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 $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

@@ -64,7 +64,7 @@ final class ResponseHasCookie extends Constraint
return 'the Response '.$this->toString();
}
protected function getCookie(Response $response): ?Cookie
private function getCookie(Response $response): ?Cookie
{
$cookies = $response->headers->getCookies();

View File

@@ -40,7 +40,7 @@ final class ResponseHeaderSame extends Constraint
*/
protected function matches($response): bool
{
return $this->expectedValue === $response->headers->get($this->headerName, null, true);
return $this->expectedValue === $response->headers->get($this->headerName, null);
}
/**

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;
}
}

View File

@@ -1,113 +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\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\AcceptHeaderItem;
class AcceptHeaderItemTest extends TestCase
{
/**
* @dataProvider provideFromStringData
*/
public function testFromString($string, $value, array $attributes)
{
$item = AcceptHeaderItem::fromString($string);
$this->assertEquals($value, $item->getValue());
$this->assertEquals($attributes, $item->getAttributes());
}
public function provideFromStringData()
{
return [
[
'text/html',
'text/html', [],
],
[
'"this;should,not=matter"',
'this;should,not=matter', [],
],
[
"text/plain; charset=utf-8;param=\"this;should,not=matter\";\tfootnotes=true",
'text/plain', ['charset' => 'utf-8', 'param' => 'this;should,not=matter', 'footnotes' => 'true'],
],
[
'"this;should,not=matter";charset=utf-8',
'this;should,not=matter', ['charset' => 'utf-8'],
],
];
}
/**
* @dataProvider provideToStringData
*/
public function testToString($value, array $attributes, $string)
{
$item = new AcceptHeaderItem($value, $attributes);
$this->assertEquals($string, (string) $item);
}
public function provideToStringData()
{
return [
[
'text/html', [],
'text/html',
],
[
'text/plain', ['charset' => 'utf-8', 'param' => 'this;should,not=matter', 'footnotes' => 'true'],
'text/plain; charset=utf-8; param="this;should,not=matter"; footnotes=true',
],
];
}
public function testValue()
{
$item = new AcceptHeaderItem('value', []);
$this->assertEquals('value', $item->getValue());
$item->setValue('new value');
$this->assertEquals('new value', $item->getValue());
$item->setValue(1);
$this->assertEquals('1', $item->getValue());
}
public function testQuality()
{
$item = new AcceptHeaderItem('value', []);
$this->assertEquals(1.0, $item->getQuality());
$item->setQuality(0.5);
$this->assertEquals(0.5, $item->getQuality());
$item->setAttribute('q', 0.75);
$this->assertEquals(0.75, $item->getQuality());
$this->assertFalse($item->hasAttribute('q'));
}
public function testAttribute()
{
$item = new AcceptHeaderItem('value', []);
$this->assertEquals([], $item->getAttributes());
$this->assertFalse($item->hasAttribute('test'));
$this->assertNull($item->getAttribute('test'));
$this->assertEquals('default', $item->getAttribute('test', 'default'));
$item->setAttribute('test', 'value');
$this->assertEquals(['test' => 'value'], $item->getAttributes());
$this->assertTrue($item->hasAttribute('test'));
$this->assertEquals('value', $item->getAttribute('test'));
$this->assertEquals('value', $item->getAttribute('test', 'default'));
}
}

View File

@@ -1,130 +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\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\AcceptHeader;
use Symfony\Component\HttpFoundation\AcceptHeaderItem;
class AcceptHeaderTest extends TestCase
{
public function testFirst()
{
$header = AcceptHeader::fromString('text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c');
$this->assertSame('text/html', $header->first()->getValue());
}
/**
* @dataProvider provideFromStringData
*/
public function testFromString($string, array $items)
{
$header = AcceptHeader::fromString($string);
$parsed = array_values($header->all());
// reset index since the fixtures don't have them set
foreach ($parsed as $item) {
$item->setIndex(0);
}
$this->assertEquals($items, $parsed);
}
public function provideFromStringData()
{
return [
['', []],
['gzip', [new AcceptHeaderItem('gzip')]],
['gzip,deflate,sdch', [new AcceptHeaderItem('gzip'), new AcceptHeaderItem('deflate'), new AcceptHeaderItem('sdch')]],
["gzip, deflate\t,sdch", [new AcceptHeaderItem('gzip'), new AcceptHeaderItem('deflate'), new AcceptHeaderItem('sdch')]],
['"this;should,not=matter"', [new AcceptHeaderItem('this;should,not=matter')]],
];
}
/**
* @dataProvider provideToStringData
*/
public function testToString(array $items, $string)
{
$header = new AcceptHeader($items);
$this->assertEquals($string, (string) $header);
}
public function provideToStringData()
{
return [
[[], ''],
[[new AcceptHeaderItem('gzip')], 'gzip'],
[[new AcceptHeaderItem('gzip'), new AcceptHeaderItem('deflate'), new AcceptHeaderItem('sdch')], 'gzip,deflate,sdch'],
[[new AcceptHeaderItem('this;should,not=matter')], 'this;should,not=matter'],
];
}
/**
* @dataProvider provideFilterData
*/
public function testFilter($string, $filter, array $values)
{
$header = AcceptHeader::fromString($string)->filter($filter);
$this->assertEquals($values, array_keys($header->all()));
}
public function provideFilterData()
{
return [
['fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4', '/fr.*/', ['fr-FR', 'fr']],
];
}
/**
* @dataProvider provideSortingData
*/
public function testSorting($string, array $values)
{
$header = AcceptHeader::fromString($string);
$this->assertEquals($values, array_keys($header->all()));
}
public function provideSortingData()
{
return [
'quality has priority' => ['*;q=0.3,ISO-8859-1,utf-8;q=0.7', ['ISO-8859-1', 'utf-8', '*']],
'order matters when q is equal' => ['*;q=0.3,ISO-8859-1;q=0.7,utf-8;q=0.7', ['ISO-8859-1', 'utf-8', '*']],
'order matters when q is equal2' => ['*;q=0.3,utf-8;q=0.7,ISO-8859-1;q=0.7', ['utf-8', 'ISO-8859-1', '*']],
];
}
/**
* @dataProvider provideDefaultValueData
*/
public function testDefaultValue($acceptHeader, $value, $expectedQuality)
{
$header = AcceptHeader::fromString($acceptHeader);
$this->assertSame($expectedQuality, $header->get($value)->getQuality());
}
public function provideDefaultValueData()
{
yield ['text/plain;q=0.5, text/html, text/x-dvi;q=0.8, *;q=0.3', 'text/xml', 0.3];
yield ['text/plain;q=0.5, text/html, text/x-dvi;q=0.8, */*;q=0.3', 'text/xml', 0.3];
yield ['text/plain;q=0.5, text/html, text/x-dvi;q=0.8, */*;q=0.3', 'text/html', 1.0];
yield ['text/plain;q=0.5, text/html, text/x-dvi;q=0.8, */*;q=0.3', 'text/plain', 0.5];
yield ['text/plain;q=0.5, text/html, text/x-dvi;q=0.8, */*;q=0.3', '*', 0.3];
yield ['text/plain;q=0.5, text/html, text/x-dvi;q=0.8, */*', '*', 1.0];
yield ['text/plain;q=0.5, text/html, text/x-dvi;q=0.8, */*', 'text/xml', 1.0];
yield ['text/plain;q=0.5, text/html, text/x-dvi;q=0.8, */*', 'text/*', 1.0];
yield ['text/plain;q=0.5, text/html, text/*;q=0.8, */*', 'text/*', 0.8];
yield ['text/plain;q=0.5, text/html, text/*;q=0.8, */*', 'text/html', 1.0];
yield ['text/plain;q=0.5, text/html, text/*;q=0.8, */*', 'text/x-dvi', 0.8];
yield ['*;q=0.3, ISO-8859-1;q=0.7, utf-8;q=0.7', '*', 0.3];
yield ['*;q=0.3, ISO-8859-1;q=0.7, utf-8;q=0.7', 'utf-8', 0.7];
yield ['*;q=0.3, ISO-8859-1;q=0.7, utf-8;q=0.7', 'SHIFT_JIS', 0.3];
}
}

View File

@@ -1,93 +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\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\ApacheRequest;
class ApacheRequestTest extends TestCase
{
/**
* @dataProvider provideServerVars
*/
public function testUriMethods($server, $expectedRequestUri, $expectedBaseUrl, $expectedPathInfo)
{
$request = new ApacheRequest();
$request->server->replace($server);
$this->assertEquals($expectedRequestUri, $request->getRequestUri(), '->getRequestUri() is correct');
$this->assertEquals($expectedBaseUrl, $request->getBaseUrl(), '->getBaseUrl() is correct');
$this->assertEquals($expectedPathInfo, $request->getPathInfo(), '->getPathInfo() is correct');
}
public function provideServerVars()
{
return [
[
[
'REQUEST_URI' => '/foo/app_dev.php/bar',
'SCRIPT_NAME' => '/foo/app_dev.php',
'PATH_INFO' => '/bar',
],
'/foo/app_dev.php/bar',
'/foo/app_dev.php',
'/bar',
],
[
[
'REQUEST_URI' => '/foo/bar',
'SCRIPT_NAME' => '/foo/app_dev.php',
],
'/foo/bar',
'/foo',
'/bar',
],
[
[
'REQUEST_URI' => '/app_dev.php/foo/bar',
'SCRIPT_NAME' => '/app_dev.php',
'PATH_INFO' => '/foo/bar',
],
'/app_dev.php/foo/bar',
'/app_dev.php',
'/foo/bar',
],
[
[
'REQUEST_URI' => '/foo/bar',
'SCRIPT_NAME' => '/app_dev.php',
],
'/foo/bar',
'',
'/foo/bar',
],
[
[
'REQUEST_URI' => '/app_dev.php',
'SCRIPT_NAME' => '/app_dev.php',
],
'/app_dev.php',
'/app_dev.php',
'/',
],
[
[
'REQUEST_URI' => '/',
'SCRIPT_NAME' => '/app_dev.php',
],
'/',
'',
'/',
],
];
}
}

View File

@@ -1,367 +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\Tests;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\File\Stream;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\HttpFoundation\Tests\File\FakeFile;
class BinaryFileResponseTest extends ResponseTestCase
{
public function testConstruction()
{
$file = __DIR__.'/../README.md';
$response = new BinaryFileResponse($file, 404, ['X-Header' => 'Foo'], true, null, true, true);
$this->assertEquals(404, $response->getStatusCode());
$this->assertEquals('Foo', $response->headers->get('X-Header'));
$this->assertTrue($response->headers->has('ETag'));
$this->assertTrue($response->headers->has('Last-Modified'));
$this->assertFalse($response->headers->has('Content-Disposition'));
$response = BinaryFileResponse::create($file, 404, [], true, ResponseHeaderBag::DISPOSITION_INLINE);
$this->assertEquals(404, $response->getStatusCode());
$this->assertFalse($response->headers->has('ETag'));
$this->assertEquals('inline; filename=README.md', $response->headers->get('Content-Disposition'));
}
public function testConstructWithNonAsciiFilename()
{
touch(sys_get_temp_dir().'/fööö.html');
$response = new BinaryFileResponse(sys_get_temp_dir().'/fööö.html', 200, [], true, 'attachment');
@unlink(sys_get_temp_dir().'/fööö.html');
$this->assertSame('fööö.html', $response->getFile()->getFilename());
}
/**
* @expectedException \LogicException
*/
public function testSetContent()
{
$response = new BinaryFileResponse(__FILE__);
$response->setContent('foo');
}
public function testGetContent()
{
$response = new BinaryFileResponse(__FILE__);
$this->assertFalse($response->getContent());
}
public function testSetContentDispositionGeneratesSafeFallbackFilename()
{
$response = new BinaryFileResponse(__FILE__);
$response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, 'föö.html');
$this->assertSame('attachment; filename=f__.html; filename*=utf-8\'\'f%C3%B6%C3%B6.html', $response->headers->get('Content-Disposition'));
}
public function testSetContentDispositionGeneratesSafeFallbackFilenameForWronglyEncodedFilename()
{
$response = new BinaryFileResponse(__FILE__);
$iso88591EncodedFilename = utf8_decode('föö.html');
$response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $iso88591EncodedFilename);
// the parameter filename* is invalid in this case (rawurldecode('f%F6%F6') does not provide a UTF-8 string but an ISO-8859-1 encoded one)
$this->assertSame('attachment; filename=f__.html; filename*=utf-8\'\'f%F6%F6.html', $response->headers->get('Content-Disposition'));
}
/**
* @dataProvider provideRanges
*/
public function testRequests($requestRange, $offset, $length, $responseRange)
{
$response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream'])->setAutoEtag();
// do a request to get the ETag
$request = Request::create('/');
$response->prepare($request);
$etag = $response->headers->get('ETag');
// prepare a request for a range of the testing file
$request = Request::create('/');
$request->headers->set('If-Range', $etag);
$request->headers->set('Range', $requestRange);
$file = fopen(__DIR__.'/File/Fixtures/test.gif', 'r');
fseek($file, $offset);
$data = fread($file, $length);
fclose($file);
$this->expectOutputString($data);
$response = clone $response;
$response->prepare($request);
$response->sendContent();
$this->assertEquals(206, $response->getStatusCode());
$this->assertEquals($responseRange, $response->headers->get('Content-Range'));
$this->assertSame($length, $response->headers->get('Content-Length'));
}
/**
* @dataProvider provideRanges
*/
public function testRequestsWithoutEtag($requestRange, $offset, $length, $responseRange)
{
$response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream']);
// do a request to get the LastModified
$request = Request::create('/');
$response->prepare($request);
$lastModified = $response->headers->get('Last-Modified');
// prepare a request for a range of the testing file
$request = Request::create('/');
$request->headers->set('If-Range', $lastModified);
$request->headers->set('Range', $requestRange);
$file = fopen(__DIR__.'/File/Fixtures/test.gif', 'r');
fseek($file, $offset);
$data = fread($file, $length);
fclose($file);
$this->expectOutputString($data);
$response = clone $response;
$response->prepare($request);
$response->sendContent();
$this->assertEquals(206, $response->getStatusCode());
$this->assertEquals($responseRange, $response->headers->get('Content-Range'));
}
public function provideRanges()
{
return [
['bytes=1-4', 1, 4, 'bytes 1-4/35'],
['bytes=-5', 30, 5, 'bytes 30-34/35'],
['bytes=30-', 30, 5, 'bytes 30-34/35'],
['bytes=30-30', 30, 1, 'bytes 30-30/35'],
['bytes=30-34', 30, 5, 'bytes 30-34/35'],
];
}
public function testRangeRequestsWithoutLastModifiedDate()
{
// prevent auto last modified
$response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream'], true, null, false, false);
// prepare a request for a range of the testing file
$request = Request::create('/');
$request->headers->set('If-Range', date('D, d M Y H:i:s').' GMT');
$request->headers->set('Range', 'bytes=1-4');
$this->expectOutputString(file_get_contents(__DIR__.'/File/Fixtures/test.gif'));
$response = clone $response;
$response->prepare($request);
$response->sendContent();
$this->assertEquals(200, $response->getStatusCode());
$this->assertNull($response->headers->get('Content-Range'));
}
/**
* @dataProvider provideFullFileRanges
*/
public function testFullFileRequests($requestRange)
{
$response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream'])->setAutoEtag();
// prepare a request for a range of the testing file
$request = Request::create('/');
$request->headers->set('Range', $requestRange);
$file = fopen(__DIR__.'/File/Fixtures/test.gif', 'r');
$data = fread($file, 35);
fclose($file);
$this->expectOutputString($data);
$response = clone $response;
$response->prepare($request);
$response->sendContent();
$this->assertEquals(200, $response->getStatusCode());
}
public function provideFullFileRanges()
{
return [
['bytes=0-'],
['bytes=0-34'],
['bytes=-35'],
// Syntactical invalid range-request should also return the full resource
['bytes=20-10'],
['bytes=50-40'],
];
}
public function testUnpreparedResponseSendsFullFile()
{
$response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200);
$data = file_get_contents(__DIR__.'/File/Fixtures/test.gif');
$this->expectOutputString($data);
$response = clone $response;
$response->sendContent();
$this->assertEquals(200, $response->getStatusCode());
}
/**
* @dataProvider provideInvalidRanges
*/
public function testInvalidRequests($requestRange)
{
$response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream'])->setAutoEtag();
// prepare a request for a range of the testing file
$request = Request::create('/');
$request->headers->set('Range', $requestRange);
$response = clone $response;
$response->prepare($request);
$response->sendContent();
$this->assertEquals(416, $response->getStatusCode());
$this->assertEquals('bytes */35', $response->headers->get('Content-Range'));
}
public function provideInvalidRanges()
{
return [
['bytes=-40'],
['bytes=30-40'],
];
}
/**
* @dataProvider provideXSendfileFiles
*/
public function testXSendfile($file)
{
$request = Request::create('/');
$request->headers->set('X-Sendfile-Type', 'X-Sendfile');
BinaryFileResponse::trustXSendfileTypeHeader();
$response = BinaryFileResponse::create($file, 200, ['Content-Type' => 'application/octet-stream']);
$response->prepare($request);
$this->expectOutputString('');
$response->sendContent();
$this->assertContains('README.md', $response->headers->get('X-Sendfile'));
}
public function provideXSendfileFiles()
{
return [
[__DIR__.'/../README.md'],
['file://'.__DIR__.'/../README.md'],
];
}
/**
* @dataProvider getSampleXAccelMappings
*/
public function testXAccelMapping($realpath, $mapping, $virtual)
{
$request = Request::create('/');
$request->headers->set('X-Sendfile-Type', 'X-Accel-Redirect');
$request->headers->set('X-Accel-Mapping', $mapping);
$file = new FakeFile($realpath, __DIR__.'/File/Fixtures/test');
BinaryFileResponse::trustXSendfileTypeHeader();
$response = new BinaryFileResponse($file, 200, ['Content-Type' => 'application/octet-stream']);
$reflection = new \ReflectionObject($response);
$property = $reflection->getProperty('file');
$property->setAccessible(true);
$property->setValue($response, $file);
$response->prepare($request);
$this->assertEquals($virtual, $response->headers->get('X-Accel-Redirect'));
}
public function testDeleteFileAfterSend()
{
$request = Request::create('/');
$path = __DIR__.'/File/Fixtures/to_delete';
touch($path);
$realPath = realpath($path);
$this->assertFileExists($realPath);
$response = new BinaryFileResponse($realPath, 200, ['Content-Type' => 'application/octet-stream']);
$response->deleteFileAfterSend(true);
$response->prepare($request);
$response->sendContent();
$this->assertFileNotExists($path);
}
public function testAcceptRangeOnUnsafeMethods()
{
$request = Request::create('/', 'POST');
$response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream']);
$response->prepare($request);
$this->assertEquals('none', $response->headers->get('Accept-Ranges'));
}
public function testAcceptRangeNotOverriden()
{
$request = Request::create('/', 'POST');
$response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream']);
$response->headers->set('Accept-Ranges', 'foo');
$response->prepare($request);
$this->assertEquals('foo', $response->headers->get('Accept-Ranges'));
}
public function getSampleXAccelMappings()
{
return [
['/var/www/var/www/files/foo.txt', '/var/www/=/files/', '/files/var/www/files/foo.txt'],
['/home/Foo/bar.txt', '/var/www/=/files/,/home/Foo/=/baz/', '/baz/bar.txt'],
['/home/Foo/bar.txt', '"/var/www/"="/files/", "/home/Foo/"="/baz/"', '/baz/bar.txt'],
['/tmp/bar.txt', '"/var/www/"="/files/", "/home/Foo/"="/baz/"', null],
];
}
public function testStream()
{
$request = Request::create('/');
$response = new BinaryFileResponse(new Stream(__DIR__.'/../README.md'), 200, ['Content-Type' => 'text/plain']);
$response->prepare($request);
$this->assertNull($response->headers->get('Content-Length'));
}
protected function provideResponse()
{
return new BinaryFileResponse(__DIR__.'/../README.md', 200, ['Content-Type' => 'application/octet-stream']);
}
public static function tearDownAfterClass()
{
$path = __DIR__.'/../Fixtures/to_delete';
if (file_exists($path)) {
@unlink($path);
}
}
}

View File

@@ -1,253 +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\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Cookie;
/**
* CookieTest.
*
* @author John Kary <john@johnkary.net>
* @author Hugo Hamon <hugo.hamon@sensio.com>
*
* @group time-sensitive
*/
class CookieTest extends TestCase
{
public function invalidNames()
{
return [
[''],
[',MyName'],
[';MyName'],
[' MyName'],
["\tMyName"],
["\rMyName"],
["\nMyName"],
["\013MyName"],
["\014MyName"],
];
}
/**
* @dataProvider invalidNames
* @expectedException \InvalidArgumentException
*/
public function testInstantiationThrowsExceptionIfCookieNameContainsInvalidCharacters($name)
{
Cookie::create($name);
}
/**
* @expectedException \InvalidArgumentException
*/
public function testInvalidExpiration()
{
Cookie::create('MyCookie', 'foo', 'bar');
}
public function testNegativeExpirationIsNotPossible()
{
$cookie = Cookie::create('foo', 'bar', -100);
$this->assertSame(0, $cookie->getExpiresTime());
}
public function testGetValue()
{
$value = 'MyValue';
$cookie = Cookie::create('MyCookie', $value);
$this->assertSame($value, $cookie->getValue(), '->getValue() returns the proper value');
}
public function testGetPath()
{
$cookie = Cookie::create('foo', 'bar');
$this->assertSame('/', $cookie->getPath(), '->getPath() returns / as the default path');
}
public function testGetExpiresTime()
{
$cookie = Cookie::create('foo', 'bar');
$this->assertEquals(0, $cookie->getExpiresTime(), '->getExpiresTime() returns the default expire date');
$cookie = Cookie::create('foo', 'bar', $expire = time() + 3600);
$this->assertEquals($expire, $cookie->getExpiresTime(), '->getExpiresTime() returns the expire date');
}
public function testGetExpiresTimeIsCastToInt()
{
$cookie = Cookie::create('foo', 'bar', 3600.9);
$this->assertSame(3600, $cookie->getExpiresTime(), '->getExpiresTime() returns the expire date as an integer');
}
public function testConstructorWithDateTime()
{
$expire = new \DateTime();
$cookie = Cookie::create('foo', 'bar', $expire);
$this->assertEquals($expire->format('U'), $cookie->getExpiresTime(), '->getExpiresTime() returns the expire date');
}
public function testConstructorWithDateTimeImmutable()
{
$expire = new \DateTimeImmutable();
$cookie = Cookie::create('foo', 'bar', $expire);
$this->assertEquals($expire->format('U'), $cookie->getExpiresTime(), '->getExpiresTime() returns the expire date');
}
public function testGetExpiresTimeWithStringValue()
{
$value = '+1 day';
$cookie = Cookie::create('foo', 'bar', $value);
$expire = strtotime($value);
$this->assertEquals($expire, $cookie->getExpiresTime(), '->getExpiresTime() returns the expire date', 1);
}
public function testGetDomain()
{
$cookie = Cookie::create('foo', 'bar', 0, '/', '.myfoodomain.com');
$this->assertEquals('.myfoodomain.com', $cookie->getDomain(), '->getDomain() returns the domain name on which the cookie is valid');
}
public function testIsSecure()
{
$cookie = Cookie::create('foo', 'bar', 0, '/', '.myfoodomain.com', true);
$this->assertTrue($cookie->isSecure(), '->isSecure() returns whether the cookie is transmitted over HTTPS');
}
public function testIsHttpOnly()
{
$cookie = Cookie::create('foo', 'bar', 0, '/', '.myfoodomain.com', false, true);
$this->assertTrue($cookie->isHttpOnly(), '->isHttpOnly() returns whether the cookie is only transmitted over HTTP');
}
public function testCookieIsNotCleared()
{
$cookie = Cookie::create('foo', 'bar', time() + 3600 * 24);
$this->assertFalse($cookie->isCleared(), '->isCleared() returns false if the cookie did not expire yet');
}
public function testCookieIsCleared()
{
$cookie = Cookie::create('foo', 'bar', time() - 20);
$this->assertTrue($cookie->isCleared(), '->isCleared() returns true if the cookie has expired');
$cookie = Cookie::create('foo', 'bar');
$this->assertFalse($cookie->isCleared());
$cookie = Cookie::create('foo', 'bar');
$this->assertFalse($cookie->isCleared());
$cookie = Cookie::create('foo', 'bar', -1);
$this->assertFalse($cookie->isCleared());
}
public function testToString()
{
$cookie = Cookie::create('foo', 'bar', $expire = strtotime('Fri, 20-May-2011 15:25:52 GMT'), '/', '.myfoodomain.com', true, true, false, null);
$this->assertEquals('foo=bar; expires=Fri, 20-May-2011 15:25:52 GMT; Max-Age=0; path=/; domain=.myfoodomain.com; secure; httponly', (string) $cookie, '->__toString() returns string representation of the cookie');
$cookie = Cookie::create('foo', 'bar with white spaces', strtotime('Fri, 20-May-2011 15:25:52 GMT'), '/', '.myfoodomain.com', true, true, false, null);
$this->assertEquals('foo=bar%20with%20white%20spaces; expires=Fri, 20-May-2011 15:25:52 GMT; Max-Age=0; path=/; domain=.myfoodomain.com; secure; httponly', (string) $cookie, '->__toString() encodes the value of the cookie according to RFC 3986 (white space = %20)');
$cookie = Cookie::create('foo', null, 1, '/admin/', '.myfoodomain.com', false, true, false, null);
$this->assertEquals('foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', $expire = time() - 31536001).'; Max-Age=0; path=/admin/; domain=.myfoodomain.com; httponly', (string) $cookie, '->__toString() returns string representation of a cleared cookie if value is NULL');
$cookie = Cookie::create('foo', 'bar');
$this->assertEquals('foo=bar; path=/; httponly; samesite=lax', (string) $cookie);
}
public function testRawCookie()
{
$cookie = Cookie::create('foo', 'b a r', 0, '/', null, false, false, false, null);
$this->assertFalse($cookie->isRaw());
$this->assertEquals('foo=b%20a%20r; path=/', (string) $cookie);
$cookie = Cookie::create('foo', 'b+a+r', 0, '/', null, false, false, true, null);
$this->assertTrue($cookie->isRaw());
$this->assertEquals('foo=b+a+r; path=/', (string) $cookie);
}
public function testGetMaxAge()
{
$cookie = Cookie::create('foo', 'bar');
$this->assertEquals(0, $cookie->getMaxAge());
$cookie = Cookie::create('foo', 'bar', $expire = time() + 100);
$this->assertEquals($expire - time(), $cookie->getMaxAge());
$cookie = Cookie::create('foo', 'bar', $expire = time() - 100);
$this->assertEquals(0, $cookie->getMaxAge());
}
public function testFromString()
{
$cookie = Cookie::fromString('foo=bar; expires=Fri, 20-May-2011 15:25:52 GMT; path=/; domain=.myfoodomain.com; secure; httponly');
$this->assertEquals(Cookie::create('foo', 'bar', strtotime('Fri, 20-May-2011 15:25:52 GMT'), '/', '.myfoodomain.com', true, true, true, null), $cookie);
$cookie = Cookie::fromString('foo=bar', true);
$this->assertEquals(Cookie::create('foo', 'bar', 0, '/', null, false, false, false, null), $cookie);
$cookie = Cookie::fromString('foo', true);
$this->assertEquals(Cookie::create('foo', null, 0, '/', null, false, false, false, null), $cookie);
}
public function testFromStringWithHttpOnly()
{
$cookie = Cookie::fromString('foo=bar; expires=Fri, 20-May-2011 15:25:52 GMT; path=/; domain=.myfoodomain.com; secure; httponly');
$this->assertTrue($cookie->isHttpOnly());
$cookie = Cookie::fromString('foo=bar; expires=Fri, 20-May-2011 15:25:52 GMT; path=/; domain=.myfoodomain.com; secure');
$this->assertFalse($cookie->isHttpOnly());
}
public function testSameSiteAttribute()
{
$cookie = new Cookie('foo', 'bar', 0, '/', null, false, true, false, 'Lax');
$this->assertEquals('lax', $cookie->getSameSite());
$cookie = new Cookie('foo', 'bar', 0, '/', null, false, true, false, '');
$this->assertNull($cookie->getSameSite());
}
public function testSetSecureDefault()
{
$cookie = Cookie::create('foo', 'bar');
$this->assertFalse($cookie->isSecure());
$cookie->setSecureDefault(true);
$this->assertTrue($cookie->isSecure());
$cookie->setSecureDefault(false);
$this->assertFalse($cookie->isSecure());
}
}

View File

@@ -1,69 +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\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\HttpFoundation\ExpressionRequestMatcher;
use Symfony\Component\HttpFoundation\Request;
class ExpressionRequestMatcherTest extends TestCase
{
/**
* @expectedException \LogicException
*/
public function testWhenNoExpressionIsSet()
{
$expressionRequestMatcher = new ExpressionRequestMatcher();
$expressionRequestMatcher->matches(new Request());
}
/**
* @dataProvider provideExpressions
*/
public function testMatchesWhenParentMatchesIsTrue($expression, $expected)
{
$request = Request::create('/foo');
$expressionRequestMatcher = new ExpressionRequestMatcher();
$expressionRequestMatcher->setExpression(new ExpressionLanguage(), $expression);
$this->assertSame($expected, $expressionRequestMatcher->matches($request));
}
/**
* @dataProvider provideExpressions
*/
public function testMatchesWhenParentMatchesIsFalse($expression)
{
$request = Request::create('/foo');
$request->attributes->set('foo', 'foo');
$expressionRequestMatcher = new ExpressionRequestMatcher();
$expressionRequestMatcher->matchAttribute('foo', 'bar');
$expressionRequestMatcher->setExpression(new ExpressionLanguage(), $expression);
$this->assertFalse($expressionRequestMatcher->matches($request));
}
public function provideExpressions()
{
return [
['request.getMethod() == method', true],
['request.getPathInfo() == path', true],
['request.getHost() == host', true],
['request.getClientIp() == ip', true],
['request.attributes.all() == attributes', true],
['request.getMethod() == method && request.getPathInfo() == path && request.getHost() == host && request.getClientIp() == ip && request.attributes.all() == attributes', true],
['request.getMethod() != method', false],
['request.getMethod() != method && request.getPathInfo() == path && request.getHost() == host && request.getClientIp() == ip && request.attributes.all() == attributes', false],
];
}
}

View File

@@ -1,45 +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\Tests\File;
use Symfony\Component\HttpFoundation\File\File as OrigFile;
class FakeFile extends OrigFile
{
private $realpath;
public function __construct($realpath, $path)
{
$this->realpath = $realpath;
parent::__construct($path, false);
}
public function isReadable()
{
return true;
}
public function getRealpath()
{
return $this->realpath;
}
public function getSize()
{
return 42;
}
public function getMTime()
{
return time();
}
}

View File

@@ -1,143 +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\Tests\File;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\File\File;
/**
* @requires extension fileinfo
*/
class FileTest extends TestCase
{
protected $file;
public function testGetMimeTypeUsesMimeTypeGuessers()
{
$file = new File(__DIR__.'/Fixtures/test.gif');
$this->assertEquals('image/gif', $file->getMimeType());
}
public function testGuessExtensionWithoutGuesser()
{
$file = new File(__DIR__.'/Fixtures/directory/.empty');
$this->assertNull($file->guessExtension());
}
public function testGuessExtensionIsBasedOnMimeType()
{
$file = new File(__DIR__.'/Fixtures/test');
$this->assertEquals('gif', $file->guessExtension());
}
public function testConstructWhenFileNotExists()
{
$this->expectException('Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException');
new File(__DIR__.'/Fixtures/not_here');
}
public function testMove()
{
$path = __DIR__.'/Fixtures/test.copy.gif';
$targetDir = __DIR__.'/Fixtures/directory';
$targetPath = $targetDir.'/test.copy.gif';
@unlink($path);
@unlink($targetPath);
copy(__DIR__.'/Fixtures/test.gif', $path);
$file = new File($path);
$movedFile = $file->move($targetDir);
$this->assertInstanceOf('Symfony\Component\HttpFoundation\File\File', $movedFile);
$this->assertFileExists($targetPath);
$this->assertFileNotExists($path);
$this->assertEquals(realpath($targetPath), $movedFile->getRealPath());
@unlink($targetPath);
}
public function testMoveWithNewName()
{
$path = __DIR__.'/Fixtures/test.copy.gif';
$targetDir = __DIR__.'/Fixtures/directory';
$targetPath = $targetDir.'/test.newname.gif';
@unlink($path);
@unlink($targetPath);
copy(__DIR__.'/Fixtures/test.gif', $path);
$file = new File($path);
$movedFile = $file->move($targetDir, 'test.newname.gif');
$this->assertFileExists($targetPath);
$this->assertFileNotExists($path);
$this->assertEquals(realpath($targetPath), $movedFile->getRealPath());
@unlink($targetPath);
}
public function getFilenameFixtures()
{
return [
['original.gif', 'original.gif'],
['..\\..\\original.gif', 'original.gif'],
['../../original.gif', 'original.gif'],
[айлfile.gif', айлfile.gif'],
['..\\..\\файлfile.gif', айлfile.gif'],
['../../файлfile.gif', айлfile.gif'],
];
}
/**
* @dataProvider getFilenameFixtures
*/
public function testMoveWithNonLatinName($filename, $sanitizedFilename)
{
$path = __DIR__.'/Fixtures/'.$sanitizedFilename;
$targetDir = __DIR__.'/Fixtures/directory/';
$targetPath = $targetDir.$sanitizedFilename;
@unlink($path);
@unlink($targetPath);
copy(__DIR__.'/Fixtures/test.gif', $path);
$file = new File($path);
$movedFile = $file->move($targetDir, $filename);
$this->assertInstanceOf('Symfony\Component\HttpFoundation\File\File', $movedFile);
$this->assertFileExists($targetPath);
$this->assertFileNotExists($path);
$this->assertEquals(realpath($targetPath), $movedFile->getRealPath());
@unlink($targetPath);
}
public function testMoveToAnUnexistentDirectory()
{
$sourcePath = __DIR__.'/Fixtures/test.copy.gif';
$targetDir = __DIR__.'/Fixtures/directory/sub';
$targetPath = $targetDir.'/test.copy.gif';
@unlink($sourcePath);
@unlink($targetPath);
@rmdir($targetDir);
copy(__DIR__.'/Fixtures/test.gif', $sourcePath);
$file = new File($sourcePath);
$movedFile = $file->move($targetDir);
$this->assertFileExists($targetPath);
$this->assertFileNotExists($sourcePath);
$this->assertEquals(realpath($targetPath), $movedFile->getRealPath());
@unlink($sourcePath);
@unlink($targetPath);
@rmdir($targetDir);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

View File

@@ -1,89 +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\Tests\File\MimeType;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\File\MimeType\FileBinaryMimeTypeGuesser;
use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesser;
/**
* @requires extension fileinfo
* @group legacy
*/
class MimeTypeTest extends TestCase
{
public function testGuessImageWithoutExtension()
{
$this->assertEquals('image/gif', MimeTypeGuesser::getInstance()->guess(__DIR__.'/../Fixtures/test'));
}
public function testGuessImageWithDirectory()
{
$this->expectException('Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException');
MimeTypeGuesser::getInstance()->guess(__DIR__.'/../Fixtures/directory');
}
public function testGuessImageWithFileBinaryMimeTypeGuesser()
{
$guesser = MimeTypeGuesser::getInstance();
$guesser->register(new FileBinaryMimeTypeGuesser());
$this->assertEquals('image/gif', MimeTypeGuesser::getInstance()->guess(__DIR__.'/../Fixtures/test'));
}
public function testGuessImageWithKnownExtension()
{
$this->assertEquals('image/gif', MimeTypeGuesser::getInstance()->guess(__DIR__.'/../Fixtures/test.gif'));
}
public function testGuessFileWithUnknownExtension()
{
$this->assertEquals('application/octet-stream', MimeTypeGuesser::getInstance()->guess(__DIR__.'/../Fixtures/.unknownextension'));
}
public function testGuessWithIncorrectPath()
{
$this->expectException('Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException');
MimeTypeGuesser::getInstance()->guess(__DIR__.'/../Fixtures/not_here');
}
public function testGuessWithNonReadablePath()
{
if ('\\' === \DIRECTORY_SEPARATOR) {
$this->markTestSkipped('Can not verify chmod operations on Windows');
}
if (!getenv('USER') || 'root' === getenv('USER')) {
$this->markTestSkipped('This test will fail if run under superuser');
}
$path = __DIR__.'/../Fixtures/to_delete';
touch($path);
@chmod($path, 0333);
if ('0333' == substr(sprintf('%o', fileperms($path)), -4)) {
$this->expectException('Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException');
MimeTypeGuesser::getInstance()->guess($path);
} else {
$this->markTestSkipped('Can not verify chmod operations, change of file permissions failed');
}
}
public static function tearDownAfterClass()
{
$path = __DIR__.'/../Fixtures/to_delete';
if (file_exists($path)) {
@chmod($path, 0666);
@unlink($path);
}
}
}

View File

@@ -1,358 +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\Tests\File;
use PHPUnit\Framework\TestCase;
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\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\HttpFoundation\File\UploadedFile;
class UploadedFileTest extends TestCase
{
protected function setUp()
{
if (!ini_get('file_uploads')) {
$this->markTestSkipped('file_uploads is disabled in php.ini');
}
}
public function testConstructWhenFileNotExists()
{
$this->expectException('Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException');
new UploadedFile(
__DIR__.'/Fixtures/not_here',
'original.gif',
null
);
}
public function testFileUploadsWithNoMimeType()
{
$file = new UploadedFile(
__DIR__.'/Fixtures/test.gif',
'original.gif',
null,
UPLOAD_ERR_OK
);
$this->assertEquals('application/octet-stream', $file->getClientMimeType());
if (\extension_loaded('fileinfo')) {
$this->assertEquals('image/gif', $file->getMimeType());
}
}
public function testFileUploadsWithUnknownMimeType()
{
$file = new UploadedFile(
__DIR__.'/Fixtures/.unknownextension',
'original.gif',
null,
UPLOAD_ERR_OK
);
$this->assertEquals('application/octet-stream', $file->getClientMimeType());
}
public function testGuessClientExtension()
{
$file = new UploadedFile(
__DIR__.'/Fixtures/test.gif',
'original.gif',
'image/gif',
null
);
$this->assertEquals('gif', $file->guessClientExtension());
}
public function testGuessClientExtensionWithIncorrectMimeType()
{
$file = new UploadedFile(
__DIR__.'/Fixtures/test.gif',
'original.gif',
'image/jpeg',
null
);
$this->assertEquals('jpeg', $file->guessClientExtension());
}
public function testCaseSensitiveMimeType()
{
$file = new UploadedFile(
__DIR__.'/Fixtures/case-sensitive-mime-type.xlsm',
'test.xlsm',
'application/vnd.ms-excel.sheet.macroEnabled.12',
null
);
$this->assertEquals('xlsm', $file->guessClientExtension());
}
public function testErrorIsOkByDefault()
{
$file = new UploadedFile(
__DIR__.'/Fixtures/test.gif',
'original.gif',
'image/gif',
null
);
$this->assertEquals(UPLOAD_ERR_OK, $file->getError());
}
public function testGetClientOriginalName()
{
$file = new UploadedFile(
__DIR__.'/Fixtures/test.gif',
'original.gif',
'image/gif',
null
);
$this->assertEquals('original.gif', $file->getClientOriginalName());
}
public function testGetClientOriginalExtension()
{
$file = new UploadedFile(
__DIR__.'/Fixtures/test.gif',
'original.gif',
'image/gif',
null
);
$this->assertEquals('gif', $file->getClientOriginalExtension());
}
/**
* @expectedException \Symfony\Component\HttpFoundation\File\Exception\FileException
*/
public function testMoveLocalFileIsNotAllowed()
{
$file = new UploadedFile(
__DIR__.'/Fixtures/test.gif',
'original.gif',
'image/gif',
UPLOAD_ERR_OK
);
$movedFile = $file->move(__DIR__.'/Fixtures/directory');
}
public function failedUploadedFile()
{
foreach ([UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE, UPLOAD_ERR_PARTIAL, UPLOAD_ERR_NO_FILE, UPLOAD_ERR_CANT_WRITE, UPLOAD_ERR_NO_TMP_DIR, UPLOAD_ERR_EXTENSION, -1] as $error) {
yield [new UploadedFile(
__DIR__.'/Fixtures/test.gif',
'original.gif',
'image/gif',
$error
)];
}
}
/**
* @dataProvider failedUploadedFile
*/
public function testMoveFailed(UploadedFile $file)
{
switch ($file->getError()) {
case UPLOAD_ERR_INI_SIZE:
$exceptionClass = IniSizeFileException::class;
break;
case UPLOAD_ERR_FORM_SIZE:
$exceptionClass = FormSizeFileException::class;
break;
case UPLOAD_ERR_PARTIAL:
$exceptionClass = PartialFileException::class;
break;
case UPLOAD_ERR_NO_FILE:
$exceptionClass = NoFileException::class;
break;
case UPLOAD_ERR_CANT_WRITE:
$exceptionClass = CannotWriteFileException::class;
break;
case UPLOAD_ERR_NO_TMP_DIR:
$exceptionClass = NoTmpDirFileException::class;
break;
case UPLOAD_ERR_EXTENSION:
$exceptionClass = ExtensionFileException::class;
break;
default:
$exceptionClass = FileException::class;
}
$this->expectException($exceptionClass);
$file->move(__DIR__.'/Fixtures/directory');
}
public function testMoveLocalFileIsAllowedInTestMode()
{
$path = __DIR__.'/Fixtures/test.copy.gif';
$targetDir = __DIR__.'/Fixtures/directory';
$targetPath = $targetDir.'/test.copy.gif';
@unlink($path);
@unlink($targetPath);
copy(__DIR__.'/Fixtures/test.gif', $path);
$file = new UploadedFile(
$path,
'original.gif',
'image/gif',
UPLOAD_ERR_OK,
true
);
$movedFile = $file->move(__DIR__.'/Fixtures/directory');
$this->assertFileExists($targetPath);
$this->assertFileNotExists($path);
$this->assertEquals(realpath($targetPath), $movedFile->getRealPath());
@unlink($targetPath);
}
public function testGetClientOriginalNameSanitizeFilename()
{
$file = new UploadedFile(
__DIR__.'/Fixtures/test.gif',
'../../original.gif',
'image/gif'
);
$this->assertEquals('original.gif', $file->getClientOriginalName());
}
public function testGetSize()
{
$file = new UploadedFile(
__DIR__.'/Fixtures/test.gif',
'original.gif',
'image/gif'
);
$this->assertEquals(filesize(__DIR__.'/Fixtures/test.gif'), $file->getSize());
$file = new UploadedFile(
__DIR__.'/Fixtures/test',
'original.gif',
'image/gif'
);
$this->assertEquals(filesize(__DIR__.'/Fixtures/test'), $file->getSize());
}
/**
* @group legacy
* @expectedDeprecation Passing a size as 4th argument to the constructor of "Symfony\Component\HttpFoundation\File\UploadedFile" is deprecated since Symfony 4.1.
*/
public function testConstructDeprecatedSize()
{
$file = new UploadedFile(
__DIR__.'/Fixtures/test.gif',
'original.gif',
'image/gif',
filesize(__DIR__.'/Fixtures/test.gif'),
UPLOAD_ERR_OK,
false
);
$this->assertEquals(filesize(__DIR__.'/Fixtures/test.gif'), $file->getSize());
}
/**
* @group legacy
* @expectedDeprecation Passing a size as 4th argument to the constructor of "Symfony\Component\HttpFoundation\File\UploadedFile" is deprecated since Symfony 4.1.
*/
public function testConstructDeprecatedSizeWhenPassingOnlyThe4Needed()
{
$file = new UploadedFile(
__DIR__.'/Fixtures/test.gif',
'original.gif',
'image/gif',
filesize(__DIR__.'/Fixtures/test.gif')
);
$this->assertEquals(filesize(__DIR__.'/Fixtures/test.gif'), $file->getSize());
}
public function testGetExtension()
{
$file = new UploadedFile(
__DIR__.'/Fixtures/test.gif',
'original.gif'
);
$this->assertEquals('gif', $file->getExtension());
}
public function testIsValid()
{
$file = new UploadedFile(
__DIR__.'/Fixtures/test.gif',
'original.gif',
null,
UPLOAD_ERR_OK,
true
);
$this->assertTrue($file->isValid());
}
/**
* @dataProvider uploadedFileErrorProvider
*/
public function testIsInvalidOnUploadError($error)
{
$file = new UploadedFile(
__DIR__.'/Fixtures/test.gif',
'original.gif',
null,
$error
);
$this->assertFalse($file->isValid());
}
public function uploadedFileErrorProvider()
{
return [
[UPLOAD_ERR_INI_SIZE],
[UPLOAD_ERR_FORM_SIZE],
[UPLOAD_ERR_PARTIAL],
[UPLOAD_ERR_NO_TMP_DIR],
[UPLOAD_ERR_EXTENSION],
];
}
public function testIsInvalidIfNotHttpUpload()
{
$file = new UploadedFile(
__DIR__.'/Fixtures/test.gif',
'original.gif',
null,
UPLOAD_ERR_OK
);
$this->assertFalse($file->isValid());
}
}

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