diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..84c7add05 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/AcceptHeader.php b/AcceptHeader.php index 3f5fbb8f3..c3c8d0c35 100644 --- a/AcceptHeader.php +++ b/AcceptHeader.php @@ -11,6 +11,9 @@ namespace Symfony\Component\HttpFoundation; +// Help opcache.preload discover always-needed symbols +class_exists(AcceptHeaderItem::class); + /** * Represents an Accept-* header. * @@ -153,7 +156,7 @@ public function first() /** * Sorts items by descending quality. */ - private function sort() + private function sort(): void { if (!$this->sorted) { uasort($this->items, function (AcceptHeaderItem $a, AcceptHeaderItem $b) { diff --git a/ApacheRequest.php b/ApacheRequest.php index 4e99186dc..f189cde58 100644 --- a/ApacheRequest.php +++ b/ApacheRequest.php @@ -11,9 +11,13 @@ namespace Symfony\Component\HttpFoundation; +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', ApacheRequest::class, Request::class), E_USER_DEPRECATED); + /** * Request represents an HTTP request from an Apache server. * + * @deprecated since Symfony 4.4. Use the Request class instead. + * * @author Fabien Potencier */ class ApacheRequest extends Request diff --git a/BinaryFileResponse.php b/BinaryFileResponse.php index e21782095..7bdbf4def 100644 --- a/BinaryFileResponse.php +++ b/BinaryFileResponse.php @@ -204,7 +204,7 @@ public function prepare(Request $request) 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,7 +217,7 @@ public function prepare(Request $request) } 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; @@ -269,7 +269,7 @@ public function prepare(Request $request) return $this; } - private function hasValidIfRangeHeader($header) + private function hasValidIfRangeHeader(?string $header): bool { if ($this->getEtag() === $header) { return true; @@ -322,12 +322,12 @@ public function setContent($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,7 +343,7 @@ public static function trustXSendfileTypeHeader() } /** - * 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 54acd6ae1..3fa73a26a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,21 @@ CHANGELOG ========= +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 +93,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 diff --git a/Cookie.php b/Cookie.php index e6b8b798f..1e22c745a 100644 --- a/Cookie.php +++ b/Cookie.php @@ -18,6 +18,10 @@ */ class Cookie { + const SAMESITE_NONE = 'none'; + const SAMESITE_LAX = 'lax'; + const SAMESITE_STRICT = 'strict'; + protected $name; protected $value; protected $domain; @@ -25,13 +29,14 @@ 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 static $reservedCharsList = "=,; \t\r\n\v\f"; + private static $reservedCharsFrom = ['=', ',', ';', ' ', "\t", "\r", "\n", "\v", "\f"]; + private static $reservedCharsTo = ['%3D', '%2C', '%3B', '%20', '%09', '%0D', '%0A', '%0B', '%0C']; /** * Creates cookie from raw header string. @@ -93,7 +98,7 @@ public function __construct(string $name, string $value = null, $expire = 0, ?st } // from PHP source code - if (preg_match("/[=,; \t\r\n\013\014]/", $name)) { + if ($raw && false !== strpbrk($name, self::$reservedCharsList)) { throw new \InvalidArgumentException(sprintf('The cookie name "%s" contains invalid characters.', $name)); } @@ -141,7 +146,13 @@ public function __construct(string $name, string $value = null, $expire = 0, ?st */ public function __toString() { - $str = ($this->isRaw() ? $this->getName() : urlencode($this->getName())).'='; + if ($this->isRaw()) { + $str = $this->getName(); + } else { + $str = str_replace(self::$reservedCharsFrom, self::$reservedCharsTo, $this->getName()); + } + + $str .= '='; if ('' === (string) $this->getValue()) { $str .= 'deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; Max-Age=0'; diff --git a/File/Exception/AccessDeniedException.php b/File/Exception/AccessDeniedException.php index c25c3629b..136d2a9f5 100644 --- a/File/Exception/AccessDeniedException.php +++ b/File/Exception/AccessDeniedException.php @@ -18,9 +18,6 @@ */ 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)); diff --git a/File/Exception/FileNotFoundException.php b/File/Exception/FileNotFoundException.php index 0f1f3f951..31bdf68fe 100644 --- a/File/Exception/FileNotFoundException.php +++ b/File/Exception/FileNotFoundException.php @@ -18,9 +18,6 @@ */ 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)); diff --git a/File/File.php b/File/File.php index 396ff3450..c72a6d991 100644 --- a/File/File.php +++ b/File/File.php @@ -91,7 +91,7 @@ public function move($directory, $name = null) $renamed = rename($this->getPathname(), $target); 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 +99,17 @@ public function move($directory, $name = null) return $target; } + /** + * @return self + */ protected function getTargetFile($directory, $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)); @@ -119,7 +122,7 @@ protected function getTargetFile($directory, $name = null) * * @param string $name The new file name * - * @return string containing + * @return string */ protected function getName($name) { diff --git a/File/MimeType/ExtensionGuesser.php b/File/MimeType/ExtensionGuesser.php index 06e5b4620..4ac201330 100644 --- a/File/MimeType/ExtensionGuesser.php +++ b/File/MimeType/ExtensionGuesser.php @@ -96,5 +96,7 @@ public function guess($mimeType) return $extension; } } + + return null; } } diff --git a/File/MimeType/FileBinaryMimeTypeGuesser.php b/File/MimeType/FileBinaryMimeTypeGuesser.php index b4d51023c..5d3ae1064 100644 --- a/File/MimeType/FileBinaryMimeTypeGuesser.php +++ b/File/MimeType/FileBinaryMimeTypeGuesser.php @@ -36,7 +36,7 @@ class FileBinaryMimeTypeGuesser implements MimeTypeGuesserInterface * * @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') + public function __construct(string $cmd = 'file -b --mime -- %s 2>/dev/null') { $this->cmd = $cmd; } @@ -79,24 +79,24 @@ public function guess($path) } if (!self::isSupported()) { - return; + return null; } ob_start(); // need to use --mime instead of -i. see #6641 - passthru(sprintf($this->cmd, escapeshellarg($path)), $return); + passthru(sprintf($this->cmd, escapeshellarg((0 === strpos($path, '-') ? './' : '').$path)), $return); if ($return > 0) { ob_end_clean(); - return; + return null; } $type = trim(ob_get_clean()); - if (!preg_match('#^([a-z0-9\-]+/[a-z0-9\-\.]+)#i', $type, $match)) { + if (!preg_match('#^([a-z0-9\-]+/[a-z0-9\-\+\.]+)#i', $type, $match)) { // it's not a type, but an error message - return; + return null; } return $match[1]; diff --git a/File/MimeType/FileinfoMimeTypeGuesser.php b/File/MimeType/FileinfoMimeTypeGuesser.php index bb3237017..648307708 100644 --- a/File/MimeType/FileinfoMimeTypeGuesser.php +++ b/File/MimeType/FileinfoMimeTypeGuesser.php @@ -31,7 +31,7 @@ class FileinfoMimeTypeGuesser implements MimeTypeGuesserInterface /** * @param string $magicFile A magic file to use with the finfo instance * - * @see http://www.php.net/manual/en/function.finfo-open.php + * @see https://php.net/finfo-open */ public function __construct(string $magicFile = null) { @@ -62,13 +62,19 @@ public function guess($path) } if (!self::isSupported()) { - return; + return null; } if (!$finfo = new \finfo(FILEINFO_MIME_TYPE, $this->magicFile)) { - return; + return null; } + $mimeType = $finfo->file($path); - return $finfo->file($path); + if ($mimeType && 0 === (\strlen($mimeType) % 2)) { + $mimeStart = substr($mimeType, 0, \strlen($mimeType) >> 1); + $mimeType = $mimeStart.$mimeStart === $mimeType ? $mimeStart : $mimeType; + } + + return $mimeType; } } diff --git a/File/MimeType/MimeTypeExtensionGuesser.php b/File/MimeType/MimeTypeExtensionGuesser.php index 651be070e..9b8ac70ad 100644 --- a/File/MimeType/MimeTypeExtensionGuesser.php +++ b/File/MimeType/MimeTypeExtensionGuesser.php @@ -625,7 +625,7 @@ class MimeTypeExtensionGuesser implements ExtensionGuesserInterface 'audio/basic' => 'au', 'audio/midi' => 'mid', 'audio/mp4' => 'm4a', - 'audio/mpeg' => 'mpga', + 'audio/mpeg' => 'mp3', 'audio/ogg' => 'oga', 'audio/s3m' => 's3m', 'audio/silk' => 'sil', diff --git a/File/MimeType/MimeTypeGuesser.php b/File/MimeType/MimeTypeGuesser.php index 2b30a62a5..65b4b25be 100644 --- a/File/MimeType/MimeTypeGuesser.php +++ b/File/MimeType/MimeTypeGuesser.php @@ -130,7 +130,9 @@ public function guess($path) } 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?)'); + throw new \LogicException('Unable to guess the mime type as no guessers are available (Did you enable the php_fileinfo extension?).'); } + + return null; } } diff --git a/File/MimeType/MimeTypeGuesserInterface.php b/File/MimeType/MimeTypeGuesserInterface.php index 0f048b533..eab444890 100644 --- a/File/MimeType/MimeTypeGuesserInterface.php +++ b/File/MimeType/MimeTypeGuesserInterface.php @@ -29,7 +29,7 @@ interface MimeTypeGuesserInterface * * @param string $path The path to the file * - * @return string The mime type or NULL, if none could be guessed + * @return string|null 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 diff --git a/File/UploadedFile.php b/File/UploadedFile.php index 568d192fc..5d5063e46 100644 --- a/File/UploadedFile.php +++ b/File/UploadedFile.php @@ -31,7 +31,7 @@ */ class UploadedFile extends File { - private $test = false; + private $test; private $originalName; private $mimeType; private $error; @@ -83,7 +83,7 @@ public function __construct(string $path, string $originalName, string $mimeType * 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 The original name */ public function getClientOriginalName() { @@ -112,7 +112,7 @@ public function getClientOriginalExtension() * 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 The mime type * * @see getMimeType() */ @@ -208,7 +208,7 @@ public function move($directory, $name = null) $moved = move_uploaded_file($this->getPathname(), $target); 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()); @@ -243,13 +243,24 @@ public function move($directory, $name = null) */ 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. + */ + private static function parseFilesize($size): int + { + if ('' === $size) { + return 0; } - $max = ltrim($iniMax, '+'); + $size = strtolower($size); + + $max = ltrim($size, '+'); if (0 === strpos($max, '0x')) { $max = \intval($max, 16); } elseif (0 === strpos($max, '0')) { @@ -258,7 +269,7 @@ public static function getMaxFilesize() $max = (int) $max; } - switch (substr($iniMax, -1)) { + switch (substr($size, -1)) { case 't': $max *= 1024; // no break case 'g': $max *= 1024; diff --git a/FileBag.php b/FileBag.php index efd83ffeb..d79075c92 100644 --- a/FileBag.php +++ b/FileBag.php @@ -24,7 +24,7 @@ class FileBag extends ParameterBag private static $fileKeys = ['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 = []) { @@ -75,8 +75,8 @@ protected function convertFileInformation($file) return $file; } - $file = $this->fixPhpFilesArray($file); if (\is_array($file)) { + $file = $this->fixPhpFilesArray($file); $keys = array_keys($file); sort($keys); @@ -109,14 +109,12 @@ protected function convertFileInformation($file) * It's safe to pass an already converted array, in which case this method * just returns the original array unmodified. * + * @param array $data + * * @return array */ protected function fixPhpFilesArray($data) { - if (!\is_array($data)) { - return $data; - } - $keys = array_keys($data); sort($keys); diff --git a/HeaderBag.php b/HeaderBag.php index fa9d17313..da794554b 100644 --- a/HeaderBag.php +++ b/HeaderBag.php @@ -18,12 +18,12 @@ */ class HeaderBag implements \IteratorAggregate, \Countable { + protected const UPPER = '_ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + protected const LOWER = '-abcdefghijklmnopqrstuvwxyz'; + protected $headers = []; protected $cacheControl = []; - /** - * @param array $headers An array of HTTP headers - */ public function __construct(array $headers = []) { foreach ($headers as $key => $values) { @@ -58,10 +58,16 @@ public function __toString() /** * Returns the headers. * + * @param string|null $key The name of the headers to return or null to get them all + * * @return array An array of headers */ - public function all() + public function all(/*string $key = null*/) { + if (1 <= \func_num_args() && null !== $key = func_get_arg(0)) { + return $this->headers[strtr($key, self::UPPER, self::LOWER)] ?? []; + } + return $this->headers; } @@ -77,8 +83,6 @@ public function keys() /** * Replaces the current HTTP headers by a new set. - * - * @param array $headers An array of HTTP headers */ public function replace(array $headers = []) { @@ -88,8 +92,6 @@ public function replace(array $headers = []) /** * Adds new headers the current HTTP headers set. - * - * @param array $headers An array of HTTP headers */ public function add(array $headers) { @@ -103,28 +105,29 @@ public function add(array $headers) * * @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 The first header value or default value */ - public function get($key, $default = null, $first = true) + public function get($key, $default = null) { - $key = str_replace('_', '-', strtolower($key)); - $headers = $this->all(); + $headers = $this->all((string) $key); + if (2 < \func_num_args()) { + @trigger_error(sprintf('Passing a third argument to "%s()" is deprecated since Symfony 4.4, use method "all()" instead', __METHOD__), E_USER_DEPRECATED); - if (!\array_key_exists($key, $headers)) { - if (null === $default) { - return $first ? null : []; + if (!func_get_arg(2)) { + return $headers; } + } - 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]; } /** @@ -136,7 +139,7 @@ public function get($key, $default = null, $first = true) */ public function set($key, $values, $replace = true) { - $key = str_replace('_', '-', strtolower($key)); + $key = strtr($key, self::UPPER, self::LOWER); if (\is_array($values)) { $values = array_values($values); @@ -168,7 +171,7 @@ public function set($key, $values, $replace = true) */ public function has($key) { - return \array_key_exists(str_replace('_', '-', strtolower($key)), $this->all()); + return \array_key_exists(strtr($key, self::UPPER, self::LOWER), $this->all()); } /** @@ -181,7 +184,7 @@ public function has($key) */ public function contains($key, $value) { - return \in_array($value, $this->get($key, null, false)); + return \in_array($value, $this->all((string) $key)); } /** @@ -191,7 +194,7 @@ public function contains($key, $value) */ public function remove($key) { - $key = str_replace('_', '-', strtolower($key)); + $key = strtr($key, self::UPPER, self::LOWER); unset($this->headers[$key]); @@ -203,10 +206,9 @@ public function remove($key) /** * Returns the HTTP header value converted to a date. * - * @param string $key The parameter key - * @param \DateTime $default The default value + * @param string $key The parameter key * - * @return \DateTime|null The parsed DateTime or the default value if the header does not exist + * @return \DateTimeInterface|null The parsed DateTime or the default value if the header does not exist * * @throws \RuntimeException When the HTTP header is not parseable */ @@ -217,7 +219,7 @@ public function getDate($key, \DateTime $default = null) } if (false === $date = \DateTime::createFromFormat(DATE_RFC2822, $value)) { - throw new \RuntimeException(sprintf('The %s HTTP header is not parseable (%s).', $key, $value)); + throw new \RuntimeException(sprintf('The "%s" HTTP header is not parseable (%s).', $key, $value)); } return $date; diff --git a/HeaderUtils.php b/HeaderUtils.php index 31db1bd0d..5866e3b2b 100644 --- a/HeaderUtils.php +++ b/HeaderUtils.php @@ -36,7 +36,6 @@ private function __construct() * 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 ",;=" * diff --git a/IpUtils.php b/IpUtils.php index 67d13e57a..72c53a471 100644 --- a/IpUtils.php +++ b/IpUtils.php @@ -153,4 +153,36 @@ public static function checkIp6($requestIp, $ip) 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; + } } diff --git a/JsonResponse.php b/JsonResponse.php index c0a6916f0..11a0bebf8 100644 --- a/JsonResponse.php +++ b/JsonResponse.php @@ -100,7 +100,7 @@ public static function fromJsonString($data = null, $status = 200, $headers = [] public function setCallback($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 diff --git a/LICENSE b/LICENSE index a677f4376..9e936ec04 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 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 diff --git a/ParameterBag.php b/ParameterBag.php index f05e4a215..20ca6758b 100644 --- a/ParameterBag.php +++ b/ParameterBag.php @@ -23,9 +23,6 @@ class ParameterBag implements \IteratorAggregate, \Countable */ protected $parameters; - /** - * @param array $parameters An array of parameters - */ public function __construct(array $parameters = []) { $this->parameters = $parameters; @@ -53,8 +50,6 @@ public function keys() /** * Replaces the current parameters by a new set. - * - * @param array $parameters An array of parameters */ public function replace(array $parameters = []) { @@ -63,8 +58,6 @@ public function replace(array $parameters = []) /** * Adds parameters. - * - * @param array $parameters An array of parameters */ public function add(array $parameters = []) { @@ -191,7 +184,7 @@ public function getBoolean($key, $default = false) * @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 */ diff --git a/README.md b/README.md index 8907f0b96..ac98f9b80 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ specification. Resources --------- - * [Documentation](https://symfony.com/doc/current/components/http_foundation/index.html) + * [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) diff --git a/RedirectResponse.php b/RedirectResponse.php index 8d04aa42c..4347f3a84 100644 --- a/RedirectResponse.php +++ b/RedirectResponse.php @@ -30,10 +30,15 @@ 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 = []) { + if (null === $url) { + @trigger_error(sprintf('Passing a null url when instantiating a "%s" is deprecated since Symfony 4.4.', __CLASS__), E_USER_DEPRECATED); + $url = ''; + } + parent::__construct('', $status, $headers); $this->setTargetUrl($url); @@ -42,7 +47,7 @@ public function __construct(?string $url, int $status = 302, array $headers = [] throw new \InvalidArgumentException(sprintf('The HTTP status code is not a redirect ("%s" given).', $status)); } - if (301 == $status && !\array_key_exists('cache-control', array_change_key_case($headers, \CASE_LOWER))) { + if (301 == $status && !\array_key_exists('cache-control', array_change_key_case($headers, CASE_LOWER))) { $this->headers->remove('cache-control'); } } @@ -82,7 +87,7 @@ public function getTargetUrl() */ public function setTargetUrl($url) { - if (empty($url)) { + if ('' === ($url ?? '')) { throw new \InvalidArgumentException('Cannot redirect to an empty URL.'); } @@ -93,7 +98,7 @@ public function setTargetUrl($url) - + Redirecting to %1$s diff --git a/Request.php b/Request.php index 27d5f43b6..33f4efb5e 100644 --- a/Request.php +++ b/Request.php @@ -15,6 +15,14 @@ use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException; use Symfony\Component\HttpFoundation\Session\SessionInterface; +// Help opcache.preload discover always-needed symbols +class_exists(AcceptHeader::class); +class_exists(FileBag::class); +class_exists(HeaderBag::class); +class_exists(HeaderUtils::class); +class_exists(ParameterBag::class); +class_exists(ServerBag::class); + /** * Request represents an HTTP request. * @@ -69,49 +77,49 @@ class Request /** * Custom parameters. * - * @var \Symfony\Component\HttpFoundation\ParameterBag + * @var ParameterBag */ public $attributes; /** * Request body parameters ($_POST). * - * @var \Symfony\Component\HttpFoundation\ParameterBag + * @var ParameterBag */ public $request; /** * Query string parameters ($_GET). * - * @var \Symfony\Component\HttpFoundation\ParameterBag + * @var ParameterBag */ public $query; /** * Server and execution environment parameters ($_SERVER). * - * @var \Symfony\Component\HttpFoundation\ServerBag + * @var ServerBag */ public $server; /** * Uploaded files ($_FILES). * - * @var \Symfony\Component\HttpFoundation\FileBag + * @var FileBag */ public $files; /** * Cookies ($_COOKIE). * - * @var \Symfony\Component\HttpFoundation\ParameterBag + * @var ParameterBag */ public $cookies; /** * Headers (taken from the $_SERVER). * - * @var \Symfony\Component\HttpFoundation\HeaderBag + * @var HeaderBag */ public $headers; @@ -171,7 +179,7 @@ class Request protected $format; /** - * @var \Symfony\Component\HttpFoundation\Session\SessionInterface + * @var SessionInterface */ protected $session; @@ -192,6 +200,10 @@ class Request protected static $requestFactory; + /** + * @var string|null + */ + private $preferredFormat; private $isHostValid = true; private $isForwardedValid = true; @@ -495,6 +507,10 @@ public function __toString() try { $content = $this->getContent(); } catch (\LogicException $e) { + if (\PHP_VERSION_ID >= 70400) { + throw $e; + } + return trigger_error($e, E_USER_ERROR); } @@ -533,7 +549,7 @@ public function overrideGlobals() foreach ($this->headers->all() as $key => $value) { $key = strtoupper(str_replace('-', '_', $key)); - if (\in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH'])) { + if (\in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH', 'CONTENT_MD5'], true)) { $_SERVER[$key] = implode(', ', $value); } else { $_SERVER['HTTP_'.$key] = implode(', ', $value); @@ -559,14 +575,22 @@ public function overrideGlobals() * * You should only list the reverse proxies that you manage directly. * - * @param array $proxies A list of trusted proxies + * @param array $proxies A list of trusted proxies, the string 'REMOTE_ADDR' will be replaced with $_SERVER['REMOTE_ADDR'] * @param int $trustedHeaderSet A bit field of Request::HEADER_*, to set which headers to trust from your proxies * * @throws \InvalidArgumentException When $trustedHeaderSet is invalid */ public static function setTrustedProxies(array $proxies, int $trustedHeaderSet) { - self::$trustedProxies = $proxies; + self::$trustedProxies = array_reduce($proxies, function ($proxies, $proxy) { + if ('REMOTE_ADDR' !== $proxy) { + $proxies[] = $proxy; + } elseif (isset($_SERVER['REMOTE_ADDR'])) { + $proxies[] = $_SERVER['REMOTE_ADDR']; + } + + return $proxies; + }, []); self::$trustedHeaderSet = $trustedHeaderSet; } @@ -628,7 +652,7 @@ public static function getTrustedHosts() */ public static function normalizeQueryString($qs) { - if ('' == $qs) { + if ('' === ($qs ?? '')) { return ''; } @@ -698,7 +722,7 @@ public function get($key, $default = null) /** * Gets the Session. * - * @return SessionInterface|null The session + * @return SessionInterface The session */ public function getSession() { @@ -709,7 +733,7 @@ public function getSession() if (null === $session) { @trigger_error(sprintf('Calling "%s()" when no session has been set is deprecated since Symfony 4.1 and will throw an exception in 5.0. Use "hasSession()" instead.', __METHOD__), E_USER_DEPRECATED); - // throw new \BadMethodCallException('Session has not been set'); + // throw new \BadMethodCallException('Session has not been set.'); } return $session; @@ -741,11 +765,6 @@ public function hasSession() return null !== $this->session; } - /** - * Sets the Session. - * - * @param SessionInterface $session The Session - */ public function setSession(SessionInterface $session) { $this->session = $session; @@ -792,10 +811,14 @@ public function getClientIps() * being the original client, and each successive proxy that passed the request * adding the IP address where it received the request from. * + * If your reverse proxy uses a different header name than "X-Forwarded-For", + * ("Client-Ip" for instance), configure it via the $trustedHeaderSet + * argument of the Request::setTrustedProxies() method instead. + * * @return string|null The client IP address * * @see getClientIps() - * @see http://en.wikipedia.org/wiki/X-Forwarded-For + * @see https://wikipedia.org/wiki/X-Forwarded-For */ public function getClientIp() { @@ -1080,7 +1103,7 @@ public function getRelativeUriForPath($path) // A reference to the same base directory or an empty subdirectory must be prefixed with "./". // This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used // as the first segment of a relative-path reference, as it would be mistaken for a scheme name - // (see http://tools.ietf.org/html/rfc3986#section-4.2). + // (see https://tools.ietf.org/html/rfc3986#section-4.2). return !isset($path[0]) || '/' === $path[0] || false !== ($colonPos = strpos($path, ':')) && ($colonPos < ($slashPos = strpos($path, '/')) || false === $slashPos) ? "./$path" : $path; @@ -1317,6 +1340,8 @@ public function getFormat($mimeType) return $format; } } + + return null; } /** @@ -1343,6 +1368,8 @@ public function setFormat($format, $mimeTypes) * * _format request attribute * * $default * + * @see getPreferredFormat + * * @param string|null $default The default format * * @return string|null The request format @@ -1437,15 +1464,12 @@ public function isMethod($method) * * @see https://tools.ietf.org/html/rfc7231#section-4.2.1 * - * @param bool $andCacheable Adds the additional condition that the method should be cacheable. True by default. - * * @return bool */ - public function isMethodSafe(/* $andCacheable = true */) + public function isMethodSafe() { - if (!\func_num_args() || func_get_arg(0)) { - // setting $andCacheable to false should be deprecated in 4.1 - throw new \BadMethodCallException('Checking only for cacheable HTTP methods with Symfony\Component\HttpFoundation\Request::isMethodSafe() is not supported.'); + if (\func_num_args() > 0) { + @trigger_error(sprintf('Passing arguments to "%s()" has been deprecated since Symfony 4.4; use "%s::isMethodCacheable()" to check if the method is cacheable instead.', __METHOD__, __CLASS__), E_USER_DEPRECATED); } return \in_array($this->getMethod(), ['GET', 'HEAD', 'OPTIONS', 'TRACE']); @@ -1562,10 +1586,33 @@ public function isNoCache() return $this->headers->hasCacheControlDirective('no-cache') || 'no-cache' == $this->headers->get('Pragma'); } + /** + * Gets the preferred format for the response by inspecting, in the following order: + * * the request format set using setRequestFormat + * * the values of the Accept HTTP header + * + * Note that if you use this method, you should send the "Vary: Accept" header + * in the response to prevent any issues with intermediary HTTP caches. + */ + public function getPreferredFormat(?string $default = 'html'): ?string + { + if (null !== $this->preferredFormat || null !== $this->preferredFormat = $this->getRequestFormat(null)) { + return $this->preferredFormat; + } + + foreach ($this->getAcceptableContentTypes() as $mimeType) { + if ($this->preferredFormat = $this->getFormat($mimeType)) { + return $this->preferredFormat; + } + } + + return $default; + } + /** * Returns the preferred language. * - * @param array $locales An array of ordered available locales + * @param string[] $locales An array of ordered available locales * * @return string|null The preferred locale */ @@ -1685,7 +1732,7 @@ public function getAcceptableContentTypes() * It works if your JavaScript library sets an X-Requested-With HTTP header. * It is known to work with common JavaScript frameworks: * - * @see http://en.wikipedia.org/wiki/List_of_Ajax_frameworks#JavaScript + * @see https://wikipedia.org/wiki/List_of_Ajax_frameworks#JavaScript * * @return bool true if the request is an XMLHttpRequest, false otherwise */ @@ -1697,9 +1744,9 @@ public function isXmlHttpRequest() /* * The following methods are derived from code of the Zend Framework (1.10dev - 2010-01-24) * - * Code subject to the new BSD license (http://framework.zend.com/license/new-bsd). + * Code subject to the new BSD license (https://framework.zend.com/license). * - * Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) + * Copyright (c) 2005-2010 Zend Technologies USA Inc. (https://www.zend.com/) */ protected function prepareRequestUri() @@ -1785,12 +1832,12 @@ protected function prepareBaseUrl() $requestUri = '/'.$requestUri; } - if ($baseUrl && false !== $prefix = $this->getUrlencodedPrefix($requestUri, $baseUrl)) { + if ($baseUrl && null !== $prefix = $this->getUrlencodedPrefix($requestUri, $baseUrl)) { // full $baseUrl matches return $prefix; } - if ($baseUrl && false !== $prefix = $this->getUrlencodedPrefix($requestUri, rtrim(\dirname($baseUrl), '/'.\DIRECTORY_SEPARATOR).'/')) { + if ($baseUrl && null !== $prefix = $this->getUrlencodedPrefix($requestUri, rtrim(\dirname($baseUrl), '/'.\DIRECTORY_SEPARATOR).'/')) { // directory portion of $baseUrl matches return rtrim($prefix, '/'.\DIRECTORY_SEPARATOR); } @@ -1894,7 +1941,7 @@ protected static function initializeFormats() ]; } - private function setPhpDefaultLocale(string $locale) + private function setPhpDefaultLocale(string $locale): void { // if either the class Locale doesn't exist, or an exception is thrown when // setting the default locale, the intl module is not installed, and @@ -1909,14 +1956,12 @@ private function setPhpDefaultLocale(string $locale) /** * Returns the prefix as encoded in the string when the string starts with - * the given prefix, false otherwise. - * - * @return string|false The prefix as it is encoded in $string, or false + * the given prefix, null otherwise. */ - private function getUrlencodedPrefix(string $string, string $prefix) + private function getUrlencodedPrefix(string $string, string $prefix): ?string { if (0 !== strpos(rawurldecode($string), $prefix)) { - return false; + return null; } $len = \strlen($prefix); @@ -1925,10 +1970,10 @@ private function getUrlencodedPrefix(string $string, string $prefix) return $match[0]; } - return false; + return null; } - private static function createRequestFromFactory(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null) + private static function createRequestFromFactory(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null): self { if (self::$requestFactory) { $request = (self::$requestFactory)($query, $request, $attributes, $cookies, $files, $server, $content); @@ -1956,7 +2001,7 @@ public function isFromTrustedProxy() return self::$trustedProxies && IpUtils::checkIp($this->server->get('REMOTE_ADDR'), self::$trustedProxies); } - private function getTrustedValues($type, $ip = null) + private function getTrustedValues(int $type, string $ip = null): array { $clientValues = []; $forwardedValues = []; @@ -2007,7 +2052,7 @@ private function getTrustedValues($type, $ip = null) throw new ConflictingHeadersException(sprintf('The request has both a trusted "%s" header and a trusted "%s" header, conflicting with each other. You should either configure your proxy to remove one of them, or configure your project to distrust the offending one.', self::$trustedHeaders[self::HEADER_FORWARDED], self::$trustedHeaders[$type])); } - private function normalizeAndFilterClientIps(array $clientIps, $ip) + private function normalizeAndFilterClientIps(array $clientIps, string $ip): array { if (!$clientIps) { return []; diff --git a/RequestMatcher.php b/RequestMatcher.php index d79c7f2ea..9a4a2a137 100644 --- a/RequestMatcher.php +++ b/RequestMatcher.php @@ -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) @@ -100,7 +97,7 @@ public function matchHost($regexp) * * @param int|null $port The port number to connect to */ - public function matchPort(int $port = null) + public function matchPort(?int $port) { $this->port = $port; } diff --git a/RequestStack.php b/RequestStack.php index 885d78a50..244a77d63 100644 --- a/RequestStack.php +++ b/RequestStack.php @@ -47,7 +47,7 @@ public function push(Request $request) public function pop() { if (!$this->requests) { - return; + return null; } return array_pop($this->requests); @@ -73,7 +73,7 @@ public function getCurrentRequest() public function getMasterRequest() { if (!$this->requests) { - return; + return null; } return $this->requests[0]; @@ -95,7 +95,7 @@ public function getParentRequest() $pos = \count($this->requests) - 2; if (!isset($this->requests[$pos])) { - return; + return null; } return $this->requests[$pos]; diff --git a/Response.php b/Response.php index d1263ca7a..b9177934a 100644 --- a/Response.php +++ b/Response.php @@ -11,6 +11,9 @@ namespace Symfony\Component\HttpFoundation; +// Help opcache.preload discover always-needed symbols +class_exists(ResponseHeaderBag::class); + /** * Response represents an HTTP response. * @@ -88,7 +91,7 @@ class Response const HTTP_NETWORK_AUTHENTICATION_REQUIRED = 511; // RFC6585 /** - * @var \Symfony\Component\HttpFoundation\ResponseHeaderBag + * @var ResponseHeaderBag */ public $headers; @@ -121,7 +124,7 @@ 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} + * {@link https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml Hypertext Transfer Protocol (HTTP) Status Code Registry} * (last updated 2016-03-01). * * Unless otherwise noted, the status code is defined in RFC2616. @@ -267,10 +270,12 @@ public function prepare(Request $request) $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); } @@ -344,7 +349,7 @@ public function sendHeaders() // 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 @@ -409,7 +414,7 @@ public function setContent($content) /** * Gets the current response content. * - * @return string Content + * @return string|false */ public function getContent() { @@ -628,7 +633,7 @@ public function isImmutable(): bool } /** - * 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. @@ -990,7 +995,7 @@ public function setCache(array $options) * * @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 */ @@ -1024,7 +1029,7 @@ public function hasVary(): bool */ public function getVary(): array { - if (!$vary = $this->headers->get('Vary', null, false)) { + if (!$vary = $this->headers->all('Vary')) { return []; } @@ -1092,7 +1097,7 @@ public function isNotModified(Request $request): bool /** * 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,7 +1213,7 @@ public function isEmpty(): bool * * @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); @@ -1230,7 +1235,7 @@ public static function closeOutputBuffers(int $targetLevel, bool $flush) * * @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 ((int) preg_replace('/(MSIE )(.*?);/', '$2', $match[0]) < 9) { diff --git a/ResponseHeaderBag.php b/ResponseHeaderBag.php index cf44d0ece..e71034aba 100644 --- a/ResponseHeaderBag.php +++ b/ResponseHeaderBag.php @@ -51,7 +51,7 @@ 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; @@ -87,10 +87,19 @@ public function replace(array $headers = []) /** * {@inheritdoc} + * + * @param string|null $key The name of the headers to return or null to get them all */ - public function all() + public function all(/*string $key = null*/) { $headers = parent::all(); + + if (1 <= \func_num_args() && null !== $key = func_get_arg(0)) { + $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; } @@ -103,7 +112,7 @@ public function all() */ public function set($key, $values, $replace = true) { - $uniqueKey = str_replace('_', '-', strtolower($key)); + $uniqueKey = strtr($key, self::UPPER, self::LOWER); if ('set-cookie' === $uniqueKey) { if ($replace) { @@ -134,7 +143,7 @@ public function set($key, $values, $replace = true) */ public function remove($key) { - $uniqueKey = str_replace('_', '-', strtolower($key)); + $uniqueKey = strtr($key, self::UPPER, self::LOWER); unset($this->headerNames[$uniqueKey]); if ('set-cookie' === $uniqueKey) { @@ -243,10 +252,13 @@ public function getCookies($format = self::COOKIES_FLAT) * @param string $domain * @param bool $secure * @param bool $httpOnly + * @param string $sameSite */ - public function clearCookie($name, $path = '/', $domain = null, $secure = false, $httpOnly = true) + public function clearCookie($name, $path = '/', $domain = null, $secure = false, $httpOnly = true/*, $sameSite = null*/) { - $this->setCookie(new Cookie($name, null, 1, $path, $domain, $secure, $httpOnly, false, null)); + $sameSite = \func_num_args() > 5 ? func_get_arg(5) : null; + + $this->setCookie(new Cookie($name, null, 1, $path, $domain, $secure, $httpOnly, false, $sameSite)); } /** @@ -267,13 +279,13 @@ public function makeDisposition($disposition, $filename, $filenameFallback = '') */ 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,7 +301,7 @@ protected function computeCacheControlValue() return $header; } - private function initDate() + private function initDate(): void { $now = \DateTime::createFromFormat('U', time()); $now->setTimezone(new \DateTimeZone('UTC')); diff --git a/ServerBag.php b/ServerBag.php index 90da49fae..02c70911c 100644 --- a/ServerBag.php +++ b/ServerBag.php @@ -28,13 +28,10 @@ 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_')) { $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; } } @@ -46,13 +43,13 @@ public function getHeaders() /* * 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] */ @@ -79,7 +76,7 @@ public function getHeaders() /* * 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; } diff --git a/Session/Attribute/AttributeBagInterface.php b/Session/Attribute/AttributeBagInterface.php index 0d8d17991..6fa229397 100644 --- a/Session/Attribute/AttributeBagInterface.php +++ b/Session/Attribute/AttributeBagInterface.php @@ -50,15 +50,10 @@ public function set($name, $value); /** * Returns attributes. * - * @return array Attributes + * @return array */ public function all(); - /** - * Sets attributes. - * - * @param array $attributes Attributes - */ public function replace(array $attributes); /** diff --git a/Session/Attribute/NamespacedAttributeBag.php b/Session/Attribute/NamespacedAttributeBag.php index 162c4b529..2cf0743cf 100644 --- a/Session/Attribute/NamespacedAttributeBag.php +++ b/Session/Attribute/NamespacedAttributeBag.php @@ -97,7 +97,7 @@ public function remove($name) * @param string $name Key name * @param bool $writeContext Write context, default false * - * @return array + * @return array|null */ protected function &resolveAttributePath($name, $writeContext = false) { diff --git a/Session/Session.php b/Session/Session.php index db0b9aeb0..b6973aaab 100644 --- a/Session/Session.php +++ b/Session/Session.php @@ -18,6 +18,11 @@ 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 * @author Drak @@ -31,11 +36,6 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable private $data = []; private $usageIndex = 0; - /** - * @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) { $this->storage = $storage ?: new NativeSessionStorage(); @@ -134,29 +134,22 @@ public function getIterator() /** * Returns the number of attributes. * - * @return int The number of attributes + * @return int */ 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; @@ -272,10 +265,8 @@ public function getFlashBag() * 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); } diff --git a/Session/SessionBagProxy.php b/Session/SessionBagProxy.php index 3504bdfe7..0ae8231ef 100644 --- a/Session/SessionBagProxy.php +++ b/Session/SessionBagProxy.php @@ -22,27 +22,21 @@ final class SessionBagProxy implements SessionBagInterface private $data; private $usageIndex; - public function __construct(SessionBagInterface $bag, array &$data, &$usageIndex) + public function __construct(SessionBagInterface $bag, array &$data, ?int &$usageIndex) { $this->bag = $bag; $this->data = &$data; $this->usageIndex = &$usageIndex; } - /** - * @return SessionBagInterface - */ - public function getBag() + public function getBag(): SessionBagInterface { ++$this->usageIndex; return $this->bag; } - /** - * @return bool - */ - public function isEmpty() + public function isEmpty(): bool { if (!isset($this->data[$this->bag->getStorageKey()])) { return true; @@ -55,7 +49,7 @@ public function isEmpty() /** * {@inheritdoc} */ - public function getName() + public function getName(): string { return $this->bag->getName(); } @@ -63,7 +57,7 @@ public function getName() /** * {@inheritdoc} */ - public function initialize(array &$array) + public function initialize(array &$array): void { ++$this->usageIndex; $this->data[$this->bag->getStorageKey()] = &$array; @@ -74,7 +68,7 @@ public function initialize(array &$array) /** * {@inheritdoc} */ - public function getStorageKey() + public function getStorageKey(): string { return $this->bag->getStorageKey(); } diff --git a/Session/SessionInterface.php b/Session/SessionInterface.php index 95fca857e..e758c6bda 100644 --- a/Session/SessionInterface.php +++ b/Session/SessionInterface.php @@ -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,7 +32,7 @@ public function start(); /** * Returns the session ID. * - * @return string The session ID + * @return string */ public function getId(); @@ -46,7 +46,7 @@ public function setId($id); /** * Returns the session name. * - * @return mixed The session name + * @return string */ public function getName(); @@ -68,7 +68,7 @@ public function setName($name); * 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); @@ -82,7 +82,7 @@ public function invalidate($lifetime = null); * 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); @@ -100,7 +100,7 @@ public function save(); * * @param string $name The attribute name * - * @return bool true if the attribute is defined, false otherwise + * @return bool */ public function has($name); @@ -125,14 +125,12 @@ public function set($name, $value); /** * Returns attributes. * - * @return array Attributes + * @return array */ public function all(); /** * Sets attributes. - * - * @param array $attributes Attributes */ public function replace(array $attributes); diff --git a/Session/Storage/Handler/AbstractSessionHandler.php b/Session/Storage/Handler/AbstractSessionHandler.php index defca606e..c4fa1fae2 100644 --- a/Session/Storage/Handler/AbstractSessionHandler.php +++ b/Session/Storage/Handler/AbstractSessionHandler.php @@ -29,7 +29,7 @@ abstract class AbstractSessionHandler implements \SessionHandlerInterface, \Sess private $igbinaryEmptyData; /** - * {@inheritdoc} + * @return bool */ public function open($savePath, $sessionName) { @@ -64,18 +64,27 @@ abstract protected function doWrite($sessionId, $data); abstract protected function doDestroy($sessionId); /** - * {@inheritdoc} + * @return bool */ 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 */ public function read($sessionId) { @@ -98,7 +107,7 @@ public function read($sessionId) } /** - * {@inheritdoc} + * @return bool */ public function write($sessionId, $data) { @@ -115,16 +124,24 @@ public function write($sessionId, $data) } /** - * {@inheritdoc} + * @return bool */ public function destroy($sessionId) { 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)); } else { diff --git a/Session/Storage/Handler/MemcachedSessionHandler.php b/Session/Storage/Handler/MemcachedSessionHandler.php index 1db590b36..6711e0a55 100644 --- a/Session/Storage/Handler/MemcachedSessionHandler.php +++ b/Session/Storage/Handler/MemcachedSessionHandler.php @@ -15,7 +15,7 @@ * 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 */ @@ -40,9 +40,6 @@ class MemcachedSessionHandler extends AbstractSessionHandler * * 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 - * * @throws \InvalidArgumentException When unsupported options are passed */ public function __construct(\Memcached $memcached, array $options = []) @@ -50,7 +47,7 @@ public function __construct(\Memcached $memcached, array $options = []) $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))); + throw new \InvalidArgumentException(sprintf('The following options are not supported "%s".', implode(', ', $diff))); } $this->ttl = isset($options['expiretime']) ? (int) $options['expiretime'] : 86400; @@ -58,7 +55,7 @@ public function __construct(\Memcached $memcached, array $options = []) } /** - * {@inheritdoc} + * @return bool */ public function close() { @@ -74,7 +71,7 @@ protected function doRead($sessionId) } /** - * {@inheritdoc} + * @return bool */ public function updateTimestamp($sessionId, $data) { @@ -102,7 +99,7 @@ protected function doDestroy($sessionId) } /** - * {@inheritdoc} + * @return bool */ public function gc($maxlifetime) { diff --git a/Session/Storage/Handler/MigratingSessionHandler.php b/Session/Storage/Handler/MigratingSessionHandler.php index 5293d2448..c6b16d11c 100644 --- a/Session/Storage/Handler/MigratingSessionHandler.php +++ b/Session/Storage/Handler/MigratingSessionHandler.php @@ -39,7 +39,7 @@ public function __construct(\SessionHandlerInterface $currentHandler, \SessionHa } /** - * {@inheritdoc} + * @return bool */ public function close() { @@ -50,7 +50,7 @@ public function close() } /** - * {@inheritdoc} + * @return bool */ public function destroy($sessionId) { @@ -61,7 +61,7 @@ public function destroy($sessionId) } /** - * {@inheritdoc} + * @return bool */ public function gc($maxlifetime) { @@ -72,7 +72,7 @@ public function gc($maxlifetime) } /** - * {@inheritdoc} + * @return bool */ public function open($savePath, $sessionName) { @@ -83,7 +83,7 @@ public function open($savePath, $sessionName) } /** - * {@inheritdoc} + * @return string */ public function read($sessionId) { @@ -92,7 +92,7 @@ public function read($sessionId) } /** - * {@inheritdoc} + * @return bool */ public function write($sessionId, $sessionData) { @@ -103,7 +103,7 @@ public function write($sessionId, $sessionData) } /** - * {@inheritdoc} + * @return bool */ public function validateId($sessionId) { @@ -112,7 +112,7 @@ public function validateId($sessionId) } /** - * {@inheritdoc} + * @return bool */ public function updateTimestamp($sessionId, $sessionData) { diff --git a/Session/Storage/Handler/MongoDbSessionHandler.php b/Session/Storage/Handler/MongoDbSessionHandler.php index 4efaf412a..6cb884778 100644 --- a/Session/Storage/Handler/MongoDbSessionHandler.php +++ b/Session/Storage/Handler/MongoDbSessionHandler.php @@ -56,20 +56,17 @@ class MongoDbSessionHandler extends AbstractSessionHandler * { "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) { 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,7 +80,7 @@ public function __construct(\MongoDB\Client $mongo, array $options) } /** - * {@inheritdoc} + * @return bool */ public function close() { @@ -103,7 +100,7 @@ protected function doDestroy($sessionId) } /** - * {@inheritdoc} + * @return bool */ public function gc($maxlifetime) { @@ -137,7 +134,7 @@ protected function doWrite($sessionId, $data) } /** - * {@inheritdoc} + * @return bool */ public function updateTimestamp($sessionId, $data) { @@ -171,10 +168,7 @@ protected function doRead($sessionId) return $dbData[$this->options['data_field']]->getData(); } - /** - * @return \MongoDB\Collection - */ - private function getCollection() + private function getCollection(): \MongoDB\Collection { if (null === $this->collection) { $this->collection = $this->mongo->selectCollection($this->options['database'], $this->options['collection']); diff --git a/Session/Storage/Handler/NativeFileSessionHandler.php b/Session/Storage/Handler/NativeFileSessionHandler.php index fbc25c3ef..effc9db54 100644 --- a/Session/Storage/Handler/NativeFileSessionHandler.php +++ b/Session/Storage/Handler/NativeFileSessionHandler.php @@ -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 https://php.net/manual/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 @@ public function __construct(string $savePath = null) if ($count = substr_count($savePath, ';')) { if ($count > 2) { - throw new \InvalidArgumentException(sprintf('Invalid argument $savePath \'%s\'', $savePath)); + throw new \InvalidArgumentException(sprintf('Invalid argument $savePath \'%s\'.', $savePath)); } // characters after last ';' are the path @@ -46,7 +46,7 @@ public function __construct(string $savePath = null) } if ($baseDir && !is_dir($baseDir) && !@mkdir($baseDir, 0777, true) && !is_dir($baseDir)) { - throw new \RuntimeException(sprintf('Session Storage was not able to create directory "%s"', $baseDir)); + throw new \RuntimeException(sprintf('Session Storage was not able to create directory "%s".', $baseDir)); } ini_set('session.save_path', $savePath); diff --git a/Session/Storage/Handler/NullSessionHandler.php b/Session/Storage/Handler/NullSessionHandler.php index 8d193155b..0634e46dd 100644 --- a/Session/Storage/Handler/NullSessionHandler.php +++ b/Session/Storage/Handler/NullSessionHandler.php @@ -19,7 +19,7 @@ class NullSessionHandler extends AbstractSessionHandler { /** - * {@inheritdoc} + * @return bool */ public function close() { @@ -27,7 +27,7 @@ public function close() } /** - * {@inheritdoc} + * @return bool */ public function validateId($sessionId) { @@ -43,7 +43,7 @@ protected function doRead($sessionId) } /** - * {@inheritdoc} + * @return bool */ public function updateTimestamp($sessionId, $data) { @@ -67,7 +67,7 @@ protected function doDestroy($sessionId) } /** - * {@inheritdoc} + * @return bool */ public function gc($maxlifetime) { diff --git a/Session/Storage/Handler/PdoSessionHandler.php b/Session/Storage/Handler/PdoSessionHandler.php index a3877ef4c..330ac7f2a 100644 --- a/Session/Storage/Handler/PdoSessionHandler.php +++ b/Session/Storage/Handler/PdoSessionHandler.php @@ -32,7 +32,7 @@ * 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 * @author Michael Williams @@ -65,6 +65,8 @@ class PdoSessionHandler extends AbstractSessionHandler */ const LOCK_TRANSACTIONAL = 2; + private const MAX_LIFETIME = 315576000; + /** * @var \PDO|null PDO instance or null when not connected yet */ @@ -165,7 +167,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,7 +174,7 @@ public function __construct($pdoOrDsn = null, array $options = []) { if ($pdoOrDsn instanceof \PDO) { if (\PDO::ERRMODE_EXCEPTION !== $pdoOrDsn->getAttribute(\PDO::ATTR_ERRMODE)) { - throw new \InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION))', __CLASS__)); + throw new \InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION)).', __CLASS__)); } $this->pdo = $pdoOrDsn; @@ -218,7 +219,7 @@ public function createTable() // - 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 +239,7 @@ public function createTable() try { $this->pdo->exec($sql); + $this->pdo->exec("CREATE INDEX EXPIRY ON $this->table ($this->lifetimeCol)"); } catch (\PDOException $e) { $this->rollback(); @@ -258,7 +260,7 @@ public function isSessionExpired() } /** - * {@inheritdoc} + * @return bool */ public function open($savePath, $sessionName) { @@ -272,7 +274,7 @@ public function open($savePath, $sessionName) } /** - * {@inheritdoc} + * @return string */ public function read($sessionId) { @@ -286,7 +288,7 @@ public function read($sessionId) } /** - * {@inheritdoc} + * @return bool */ public function gc($maxlifetime) { @@ -365,18 +367,18 @@ protected function doWrite($sessionId, $data) } /** - * {@inheritdoc} + * @return bool */ 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,7 +391,7 @@ public function updateTimestamp($sessionId, $data) } /** - * {@inheritdoc} + * @return bool */ public function close() { @@ -403,14 +405,21 @@ public function close() $this->gcCalled = false; // delete the session records that have expired + $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) { - $sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol < :time"; + $legacySql = "DELETE FROM $this->table WHERE $this->lifetimeCol <= :min AND $this->lifetimeCol + $this->timeCol < :time"; } else { - $sql = "DELETE FROM $this->table WHERE $this->lifetimeCol < :time - $this->timeCol"; + $legacySql = "DELETE FROM $this->table WHERE $this->lifetimeCol <= :min AND $this->lifetimeCol < :time - $this->timeCol"; } - $stmt = $this->pdo->prepare($sql); + $stmt = $this->pdo->prepare($legacySql); $stmt->bindValue(':time', time(), \PDO::PARAM_INT); + $stmt->bindValue(':min', self::MAX_LIFETIME, \PDO::PARAM_INT); $stmt->execute(); } @@ -423,10 +432,8 @@ public function close() /** * 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 +443,9 @@ private function connect($dsn) /** * 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 +468,7 @@ private function buildDsnFromUrl($dsnOrUrl) } 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 = [ @@ -538,10 +541,10 @@ private function buildDsnFromUrl($dsnOrUrl) * 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 +562,7 @@ private function beginTransaction() /** * Helper method to commit a transaction. */ - private function commit() + private function commit(): void { if ($this->inTransaction) { try { @@ -581,7 +584,7 @@ private function commit() /** * 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 @@ -623,7 +626,12 @@ protected function doRead($sessionId) $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 ''; @@ -676,7 +684,7 @@ protected function doRead($sessionId) * - 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': @@ -754,6 +762,7 @@ private function getSelectSql(): string 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 +783,26 @@ private function getSelectSql(): string /** * 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 +810,26 @@ private function getInsertStatement($sessionId, $sessionData, $maxlifetime) /** * 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 +842,25 @@ private function getMergeStatement(string $sessionId, string $data, int $maxlife { 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 +870,15 @@ private function getMergeStatement(string $sessionId, string $data, int $maxlife $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); } diff --git a/Session/Storage/Handler/RedisSessionHandler.php b/Session/Storage/Handler/RedisSessionHandler.php index a6498b882..699d6da6f 100644 --- a/Session/Storage/Handler/RedisSessionHandler.php +++ b/Session/Storage/Handler/RedisSessionHandler.php @@ -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,19 +50,20 @@ public function __construct($redis, array $options = []) !$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__, \is_object($redis) ? \get_class($redis) : \gettype($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; } /** @@ -73,7 +79,7 @@ protected function doRead($sessionId): string */ protected function doWrite($sessionId, $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; } @@ -105,10 +111,10 @@ public function gc($maxlifetime): bool } /** - * {@inheritdoc} + * @return bool */ 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'))); } } diff --git a/Session/Storage/Handler/SessionHandlerFactory.php b/Session/Storage/Handler/SessionHandlerFactory.php new file mode 100644 index 000000000..a5ebd29eb --- /dev/null +++ b/Session/Storage/Handler/SessionHandlerFactory.php @@ -0,0 +1,85 @@ + + * + * 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 + */ +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__, \gettype($connection))); + } + + 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_class($connection))); + case 0 === strpos($connection, 'file://'): + return new StrictSessionHandler(new NativeFileSessionHandler(substr($connection, 7))); + + case 0 === strpos($connection, 'redis:'): + case 0 === strpos($connection, 'rediss:'): + case 0 === strpos($connection, 'memcached:'): + if (!class_exists(AbstractAdapter::class)) { + throw new \InvalidArgumentException(sprintf('Unsupported DSN "%s". Try running "composer require symfony/cache".', $connection)); + } + $handlerClass = 0 === strpos($connection, 'memcached:') ? MemcachedSessionHandler::class : RedisSessionHandler::class; + $connection = AbstractAdapter::createConnection($connection, ['lazy' => true]); + + return new $handlerClass($connection); + + case 0 === strpos($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 0 === strpos($connection, 'mssql://'): + case 0 === strpos($connection, 'mysql://'): + case 0 === strpos($connection, 'mysql2://'): + case 0 === strpos($connection, 'pgsql://'): + case 0 === strpos($connection, 'postgres://'): + case 0 === strpos($connection, 'postgresql://'): + case 0 === strpos($connection, 'sqlsrv://'): + case 0 === strpos($connection, 'sqlite://'): + case 0 === strpos($connection, 'sqlite3://'): + return new PdoSessionHandler($connection); + } + + throw new \InvalidArgumentException(sprintf('Unsupported Connection: "%s".', $connection)); + } +} diff --git a/Session/Storage/Handler/StrictSessionHandler.php b/Session/Storage/Handler/StrictSessionHandler.php index 83a1f2c06..3144ea597 100644 --- a/Session/Storage/Handler/StrictSessionHandler.php +++ b/Session/Storage/Handler/StrictSessionHandler.php @@ -31,7 +31,7 @@ public function __construct(\SessionHandlerInterface $handler) } /** - * {@inheritdoc} + * @return bool */ public function open($savePath, $sessionName) { @@ -49,7 +49,7 @@ protected function doRead($sessionId) } /** - * {@inheritdoc} + * @return bool */ public function updateTimestamp($sessionId, $data) { @@ -65,7 +65,7 @@ protected function doWrite($sessionId, $data) } /** - * {@inheritdoc} + * @return bool */ public function destroy($sessionId) { @@ -86,7 +86,7 @@ protected function doDestroy($sessionId) } /** - * {@inheritdoc} + * @return bool */ public function close() { @@ -94,7 +94,7 @@ public function close() } /** - * {@inheritdoc} + * @return bool */ public function gc($maxlifetime) { diff --git a/Session/Storage/MetadataBag.php b/Session/Storage/MetadataBag.php index 2eff4109b..5fe40fc10 100644 --- a/Session/Storage/MetadataBag.php +++ b/Session/Storage/MetadataBag.php @@ -159,7 +159,7 @@ public function setName($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; diff --git a/Session/Storage/MockArraySessionStorage.php b/Session/Storage/MockArraySessionStorage.php index 37b6f145b..db8f85e75 100644 --- a/Session/Storage/MockArraySessionStorage.php +++ b/Session/Storage/MockArraySessionStorage.php @@ -148,7 +148,7 @@ public function setName($name) 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; @@ -186,7 +186,7 @@ public function registerBag(SessionBagInterface $bag) public function getBag($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) { diff --git a/Session/Storage/MockFileSessionStorage.php b/Session/Storage/MockFileSessionStorage.php index c0316c2c7..c96b3cd9d 100644 --- a/Session/Storage/MockFileSessionStorage.php +++ b/Session/Storage/MockFileSessionStorage.php @@ -27,9 +27,8 @@ 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 $savePath Path of directory to save session files + * @param string $name Session name */ public function __construct(string $savePath = null, string $name = 'MOCKSESSID', MetadataBag $metaBag = null) { @@ -38,7 +37,7 @@ public function __construct(string $savePath = null, string $name = 'MOCKSESSID' } 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; @@ -88,7 +87,7 @@ public function regenerate($destroy = false, $lifetime = null) 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; @@ -122,7 +121,7 @@ public function save() * 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())) { unlink($this->getFilePath()); @@ -131,10 +130,8 @@ private function destroy() /** * Calculate path to file. - * - * @return string File path */ - private function getFilePath() + private function getFilePath(): string { return $this->savePath.'/'.$this->id.'.mocksess'; } @@ -142,7 +139,7 @@ private function getFilePath() /** * 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)) : []; diff --git a/Session/Storage/NativeSessionStorage.php b/Session/Storage/NativeSessionStorage.php index 9e03bfa41..b6cce8165 100644 --- a/Session/Storage/NativeSessionStorage.php +++ b/Session/Storage/NativeSessionStorage.php @@ -17,6 +17,11 @@ 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 @@ -97,9 +102,7 @@ class NativeSessionStorage implements SessionStorageInterface * 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) { @@ -141,7 +144,7 @@ public function start() return true; } - if (\PHP_SESSION_ACTIVE === session_status()) { + if (PHP_SESSION_ACTIVE === session_status()) { throw new \RuntimeException('Failed to start the session: already started by PHP.'); } @@ -151,7 +154,7 @@ public function start() // 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) { @@ -204,7 +207,7 @@ public function setName($name) public function regenerate($destroy = false, $lifetime = null) { // Cannot regenerate the session ID for non-active sessions. - if (\PHP_SESSION_ACTIVE !== session_status()) { + if (PHP_SESSION_ACTIVE !== session_status()) { return false; } @@ -212,8 +215,10 @@ public function regenerate($destroy = false, $lifetime = null) 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) { @@ -222,10 +227,6 @@ public function regenerate($destroy = false, $lifetime = null) $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) { @@ -241,6 +242,7 @@ public function regenerate($destroy = false, $lifetime = null) */ 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) { @@ -266,7 +268,11 @@ public function save() session_write_close(); } finally { restore_error_handler(); - $_SESSION = $session; + + // Restore only if not empty + if ($_SESSION) { + $_SESSION = $session; + } } $this->closed = true; @@ -308,7 +314,7 @@ public function registerBag(SessionBagInterface $bag) public function getBag($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()) { @@ -355,11 +361,11 @@ public function isStarted() * * @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) { - if (headers_sent() || \PHP_SESSION_ACTIVE === session_status()) { + if (headers_sent() || PHP_SESSION_ACTIVE === session_status()) { return; } @@ -398,15 +404,13 @@ public function setOptions(array $options) * 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 */ @@ -426,7 +430,7 @@ public function setSaveHandler($saveHandler = null) } $this->saveHandler = $saveHandler; - if (headers_sent() || \PHP_SESSION_ACTIVE === session_status()) { + if (headers_sent() || PHP_SESSION_ACTIVE === session_status()) { return; } @@ -453,7 +457,7 @@ protected function loadSession(array &$session = null) 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]); } diff --git a/Session/Storage/PhpBridgeSessionStorage.php b/Session/Storage/PhpBridgeSessionStorage.php index 8969e609a..72dbef134 100644 --- a/Session/Storage/PhpBridgeSessionStorage.php +++ b/Session/Storage/PhpBridgeSessionStorage.php @@ -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,8 +21,7 @@ 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) { diff --git a/Session/Storage/Proxy/AbstractProxy.php b/Session/Storage/Proxy/AbstractProxy.php index 09c92483c..b9c2682b3 100644 --- a/Session/Storage/Proxy/AbstractProxy.php +++ b/Session/Storage/Proxy/AbstractProxy.php @@ -31,7 +31,7 @@ abstract class AbstractProxy /** * Gets the session.save_handler name. * - * @return string + * @return string|null */ public function getSaveHandlerName() { @@ -65,7 +65,7 @@ public function isWrapper() */ public function isActive() { - return \PHP_SESSION_ACTIVE === session_status(); + return PHP_SESSION_ACTIVE === session_status(); } /** @@ -88,7 +88,7 @@ public function getId() public function setId($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); @@ -114,7 +114,7 @@ public function getName() public function setName($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); diff --git a/Session/Storage/Proxy/SessionHandlerProxy.php b/Session/Storage/Proxy/SessionHandlerProxy.php index b11cc397a..de4f550ba 100644 --- a/Session/Storage/Proxy/SessionHandlerProxy.php +++ b/Session/Storage/Proxy/SessionHandlerProxy.php @@ -36,7 +36,7 @@ public function getHandler() // \SessionHandlerInterface /** - * {@inheritdoc} + * @return bool */ public function open($savePath, $sessionName) { @@ -44,7 +44,7 @@ public function open($savePath, $sessionName) } /** - * {@inheritdoc} + * @return bool */ public function close() { @@ -52,7 +52,7 @@ public function close() } /** - * {@inheritdoc} + * @return string */ public function read($sessionId) { @@ -60,7 +60,7 @@ public function read($sessionId) } /** - * {@inheritdoc} + * @return bool */ public function write($sessionId, $data) { @@ -68,7 +68,7 @@ public function write($sessionId, $data) } /** - * {@inheritdoc} + * @return bool */ public function destroy($sessionId) { @@ -76,7 +76,7 @@ public function destroy($sessionId) } /** - * {@inheritdoc} + * @return bool */ public function gc($maxlifetime) { @@ -84,7 +84,7 @@ public function gc($maxlifetime) } /** - * {@inheritdoc} + * @return bool */ public function validateId($sessionId) { @@ -92,7 +92,7 @@ public function validateId($sessionId) } /** - * {@inheritdoc} + * @return bool */ public function updateTimestamp($sessionId, $data) { diff --git a/Session/Storage/SessionStorageInterface.php b/Session/Storage/SessionStorageInterface.php index 66e8b33dd..eeb396a2f 100644 --- a/Session/Storage/SessionStorageInterface.php +++ b/Session/Storage/SessionStorageInterface.php @@ -77,7 +77,7 @@ public function setName($name); * 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. diff --git a/StreamedResponse.php b/StreamedResponse.php index 8310ea72d..ef8095bbe 100644 --- a/StreamedResponse.php +++ b/StreamedResponse.php @@ -63,8 +63,6 @@ public static function create($callback = null, $status = 200, $headers = []) /** * Sets the PHP callback associated with this Response. * - * @param callable $callback A valid PHP callback - * * @return $this */ public function setCallback(callable $callback) @@ -136,8 +134,6 @@ public function setContent($content) /** * {@inheritdoc} - * - * @return false */ public function getContent() { diff --git a/Test/Constraint/RequestAttributeValueSame.php b/Test/Constraint/RequestAttributeValueSame.php index 2d1056278..cb216ea12 100644 --- a/Test/Constraint/RequestAttributeValueSame.php +++ b/Test/Constraint/RequestAttributeValueSame.php @@ -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 { diff --git a/Test/Constraint/ResponseHasCookie.php b/Test/Constraint/ResponseHasCookie.php index bd792b0d8..eae9e271b 100644 --- a/Test/Constraint/ResponseHasCookie.php +++ b/Test/Constraint/ResponseHasCookie.php @@ -64,7 +64,7 @@ protected function failureDescription($response): string return 'the Response '.$this->toString(); } - protected function getCookie(Response $response): ?Cookie + private function getCookie(Response $response): ?Cookie { $cookies = $response->headers->getCookies(); diff --git a/Test/Constraint/ResponseHeaderSame.php b/Test/Constraint/ResponseHeaderSame.php index acdea71d1..a27d0c73f 100644 --- a/Test/Constraint/ResponseHeaderSame.php +++ b/Test/Constraint/ResponseHeaderSame.php @@ -40,7 +40,7 @@ public function toString(): string */ protected function matches($response): bool { - return $this->expectedValue === $response->headers->get($this->headerName, null, true); + return $this->expectedValue === $response->headers->get($this->headerName, null); } /** diff --git a/Tests/ApacheRequestTest.php b/Tests/ApacheRequestTest.php index 6fa3b8891..7a5bd378a 100644 --- a/Tests/ApacheRequestTest.php +++ b/Tests/ApacheRequestTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\ApacheRequest; +/** @group legacy */ class ApacheRequestTest extends TestCase { /** diff --git a/Tests/BinaryFileResponseTest.php b/Tests/BinaryFileResponseTest.php index effffe925..734f8e845 100644 --- a/Tests/BinaryFileResponseTest.php +++ b/Tests/BinaryFileResponseTest.php @@ -46,11 +46,9 @@ public function testConstructWithNonAsciiFilename() $this->assertSame('fööö.html', $response->getFile()->getFilename()); } - /** - * @expectedException \LogicException - */ public function testSetContent() { + $this->expectException('LogicException'); $response = new BinaryFileResponse(__FILE__); $response->setContent('foo'); } @@ -109,7 +107,7 @@ public function testRequests($requestRange, $offset, $length, $responseRange) $this->assertEquals(206, $response->getStatusCode()); $this->assertEquals($responseRange, $response->headers->get('Content-Range')); - $this->assertSame($length, $response->headers->get('Content-Length')); + $this->assertSame((string) $length, $response->headers->get('Content-Length')); } /** @@ -263,7 +261,7 @@ public function testXSendfile($file) $this->expectOutputString(''); $response->sendContent(); - $this->assertContains('README.md', $response->headers->get('X-Sendfile')); + $this->assertStringContainsString('README.md', $response->headers->get('X-Sendfile')); } public function provideXSendfileFiles() @@ -357,7 +355,7 @@ protected function provideResponse() return new BinaryFileResponse(__DIR__.'/../README.md', 200, ['Content-Type' => 'application/octet-stream']); } - public static function tearDownAfterClass() + public static function tearDownAfterClass(): void { $path = __DIR__.'/../Fixtures/to_delete'; if (file_exists($path)) { diff --git a/Tests/CookieTest.php b/Tests/CookieTest.php index 4aa32eea4..55287e082 100644 --- a/Tests/CookieTest.php +++ b/Tests/CookieTest.php @@ -24,10 +24,9 @@ */ class CookieTest extends TestCase { - public function invalidNames() + public function namesWithSpecialCharacters() { return [ - [''], [',MyName'], [';MyName'], [' MyName'], @@ -40,19 +39,31 @@ public function invalidNames() } /** - * @dataProvider invalidNames - * @expectedException \InvalidArgumentException + * @dataProvider namesWithSpecialCharacters */ - public function testInstantiationThrowsExceptionIfCookieNameContainsInvalidCharacters($name) + public function testInstantiationThrowsExceptionIfRawCookieNameContainsSpecialCharacters($name) { - Cookie::create($name); + $this->expectException('InvalidArgumentException'); + Cookie::create($name, null, 0, null, null, null, false, true); } /** - * @expectedException \InvalidArgumentException + * @dataProvider namesWithSpecialCharacters */ + public function testInstantiationSucceedNonRawCookieNameContainsSpecialCharacters($name) + { + $this->assertInstanceOf(Cookie::class, Cookie::create($name)); + } + + public function testInstantiationThrowsExceptionIfCookieNameIsEmpty() + { + $this->expectException('InvalidArgumentException'); + Cookie::create(''); + } + public function testInvalidExpiration() { + $this->expectException('InvalidArgumentException'); Cookie::create('MyCookie', 'foo', 'bar'); } @@ -118,7 +129,7 @@ public function testGetExpiresTimeWithStringValue() $cookie = Cookie::create('foo', 'bar', $value); $expire = strtotime($value); - $this->assertEquals($expire, $cookie->getExpiresTime(), '->getExpiresTime() returns the expire date', 1); + $this->assertEqualsWithDelta($expire, $cookie->getExpiresTime(), 1, '->getExpiresTime() returns the expire date'); } public function testGetDomain() diff --git a/Tests/ExpressionRequestMatcherTest.php b/Tests/ExpressionRequestMatcherTest.php index 2afdade67..8a389329e 100644 --- a/Tests/ExpressionRequestMatcherTest.php +++ b/Tests/ExpressionRequestMatcherTest.php @@ -18,11 +18,9 @@ class ExpressionRequestMatcherTest extends TestCase { - /** - * @expectedException \LogicException - */ public function testWhenNoExpressionIsSet() { + $this->expectException('LogicException'); $expressionRequestMatcher = new ExpressionRequestMatcher(); $expressionRequestMatcher->matches(new Request()); } diff --git a/Tests/File/FileTest.php b/Tests/File/FileTest.php index 9c1854d90..2ef259ed1 100644 --- a/Tests/File/FileTest.php +++ b/Tests/File/FileTest.php @@ -42,6 +42,7 @@ public function testGuessExtensionIsBasedOnMimeType() public function testConstructWhenFileNotExists() { $this->expectException('Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException'); + new File(__DIR__.'/Fixtures/not_here'); } diff --git a/Tests/File/Fixtures/-test b/Tests/File/Fixtures/-test new file mode 100644 index 000000000..b636f4b8d Binary files /dev/null and b/Tests/File/Fixtures/-test differ diff --git a/Tests/File/Fixtures/test.docx b/Tests/File/Fixtures/test.docx new file mode 100644 index 000000000..2e86b6fce Binary files /dev/null and b/Tests/File/Fixtures/test.docx differ diff --git a/Tests/File/MimeType/MimeTypeTest.php b/Tests/File/MimeType/MimeTypeTest.php index f990a4f3b..4b568e551 100644 --- a/Tests/File/MimeType/MimeTypeTest.php +++ b/Tests/File/MimeType/MimeTypeTest.php @@ -21,6 +21,17 @@ */ class MimeTypeTest extends TestCase { + public function testGuessWithLeadingDash() + { + $cwd = getcwd(); + chdir(__DIR__.'/../Fixtures'); + try { + $this->assertEquals('image/gif', MimeTypeGuesser::getInstance()->guess('-test')); + } finally { + chdir($cwd); + } + } + public function testGuessImageWithoutExtension() { $this->assertEquals('image/gif', MimeTypeGuesser::getInstance()->guess(__DIR__.'/../Fixtures/test')); @@ -50,6 +61,11 @@ public function testGuessFileWithUnknownExtension() $this->assertEquals('application/octet-stream', MimeTypeGuesser::getInstance()->guess(__DIR__.'/../Fixtures/.unknownextension')); } + public function testGuessWithDuplicatedFileType() + { + $this->assertSame('application/vnd.openxmlformats-officedocument.wordprocessingml.document', MimeTypeGuesser::getInstance()->guess(__DIR__.'/../Fixtures/test.docx')); + } + public function testGuessWithIncorrectPath() { $this->expectException('Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException'); @@ -78,7 +94,7 @@ public function testGuessWithNonReadablePath() } } - public static function tearDownAfterClass() + public static function tearDownAfterClass(): void { $path = __DIR__.'/../Fixtures/to_delete'; if (file_exists($path)) { diff --git a/Tests/File/UploadedFileTest.php b/Tests/File/UploadedFileTest.php index 8518df266..17d319581 100644 --- a/Tests/File/UploadedFileTest.php +++ b/Tests/File/UploadedFileTest.php @@ -24,7 +24,7 @@ class UploadedFileTest extends TestCase { - protected function setUp() + protected function setUp(): void { if (!ini_get('file_uploads')) { $this->markTestSkipped('file_uploads is disabled in php.ini'); @@ -142,11 +142,9 @@ public function testGetClientOriginalExtension() $this->assertEquals('gif', $file->getClientOriginalExtension()); } - /** - * @expectedException \Symfony\Component\HttpFoundation\File\Exception\FileException - */ public function testMoveLocalFileIsNotAllowed() { + $this->expectException('Symfony\Component\HttpFoundation\File\Exception\FileException'); $file = new UploadedFile( __DIR__.'/Fixtures/test.gif', 'original.gif', @@ -154,7 +152,7 @@ public function testMoveLocalFileIsNotAllowed() UPLOAD_ERR_OK ); - $movedFile = $file->move(__DIR__.'/Fixtures/directory'); + $file->move(__DIR__.'/Fixtures/directory'); } public function failedUploadedFile() @@ -355,4 +353,18 @@ public function testIsInvalidIfNotHttpUpload() $this->assertFalse($file->isValid()); } + + public function testGetMaxFilesize() + { + $size = UploadedFile::getMaxFilesize(); + + $this->assertIsInt($size); + $this->assertGreaterThan(0, $size); + + if (0 === (int) ini_get('post_max_size') && 0 === (int) ini_get('upload_max_filesize')) { + $this->assertSame(PHP_INT_MAX, $size); + } else { + $this->assertLessThan(PHP_INT_MAX, $size); + } + } } diff --git a/Tests/FileBagTest.php b/Tests/FileBagTest.php index 5eaf64f89..bde664194 100644 --- a/Tests/FileBagTest.php +++ b/Tests/FileBagTest.php @@ -23,11 +23,9 @@ */ class FileBagTest extends TestCase { - /** - * @expectedException \InvalidArgumentException - */ public function testFileMustBeAnArrayOrUploadedFile() { + $this->expectException('InvalidArgumentException'); new FileBag(['file' => 'foo']); } @@ -162,12 +160,12 @@ protected function createTempFile() return $tempFile; } - protected function setUp() + protected function setUp(): void { mkdir(sys_get_temp_dir().'/form_test', 0777, true); } - protected function tearDown() + protected function tearDown(): void { foreach (glob(sys_get_temp_dir().'/form_test/*') as $file) { unlink($file); diff --git a/Tests/Fixtures/response-functional/cookie_urlencode.expected b/Tests/Fixtures/response-functional/cookie_urlencode.expected index 14e44a398..17a9efc66 100644 --- a/Tests/Fixtures/response-functional/cookie_urlencode.expected +++ b/Tests/Fixtures/response-functional/cookie_urlencode.expected @@ -4,7 +4,8 @@ Array [0] => Content-Type: text/plain; charset=utf-8 [1] => Cache-Control: no-cache, private [2] => Date: Sat, 12 Nov 1955 20:04:00 GMT - [3] => Set-Cookie: ?*():@&+$/%#[]=%3F%2A%28%29%3A%40%26%2B%24%2F%25%23%5B%5D; path=/ + [3] => Set-Cookie: %3D%2C%3B%20%09%0D%0A%0B%0C=%3D%2C%3B%20%09%0D%0A%0B%0C; path=/ [4] => Set-Cookie: ?*():@&+$/%#[]=%3F%2A%28%29%3A%40%26%2B%24%2F%25%23%5B%5D; path=/ + [5] => Set-Cookie: ?*():@&+$/%#[]=%3F%2A%28%29%3A%40%26%2B%24%2F%25%23%5B%5D; path=/ ) shutdown diff --git a/Tests/Fixtures/response-functional/cookie_urlencode.php b/Tests/Fixtures/response-functional/cookie_urlencode.php index c0363b829..9ffb0dfec 100644 --- a/Tests/Fixtures/response-functional/cookie_urlencode.php +++ b/Tests/Fixtures/response-functional/cookie_urlencode.php @@ -4,9 +4,12 @@ $r = require __DIR__.'/common.inc'; -$str = '?*():@&+$/%#[]'; +$str1 = "=,; \t\r\n\v\f"; +$r->headers->setCookie(new Cookie($str1, $str1, 0, '', null, false, false, false, null)); -$r->headers->setCookie(new Cookie($str, $str, 0, '', null, false, false, false, null)); +$str2 = '?*():@&+$/%#[]'; + +$r->headers->setCookie(new Cookie($str2, $str2, 0, '', null, false, false, false, null)); $r->sendHeaders(); -setcookie($str, $str, 0, '/'); +setcookie($str2, $str2, 0, '/'); diff --git a/Tests/Fixtures/response-functional/invalid_cookie_name.php b/Tests/Fixtures/response-functional/invalid_cookie_name.php index 0afaaa8a5..3acf86039 100644 --- a/Tests/Fixtures/response-functional/invalid_cookie_name.php +++ b/Tests/Fixtures/response-functional/invalid_cookie_name.php @@ -5,7 +5,7 @@ $r = require __DIR__.'/common.inc'; try { - $r->headers->setCookie(Cookie::create('Hello + world', 'hodor')); + $r->headers->setCookie(new Cookie('Hello + world', 'hodor', 0, null, null, null, false, true)); } catch (\InvalidArgumentException $e) { echo $e->getMessage(); } diff --git a/Tests/HeaderBagTest.php b/Tests/HeaderBagTest.php index 6c4915f2e..3ce4a7dd4 100644 --- a/Tests/HeaderBagTest.php +++ b/Tests/HeaderBagTest.php @@ -48,13 +48,18 @@ public function testGetDate() $this->assertInstanceOf('DateTime', $headerDate); } - /** - * @expectedException \RuntimeException - */ + public function testGetDateNull() + { + $bag = new HeaderBag(['foo' => null]); + $headerDate = $bag->getDate('foo'); + $this->assertNull($headerDate); + } + public function testGetDateException() { + $this->expectException('RuntimeException'); $bag = new HeaderBag(['foo' => 'Tue']); - $headerDate = $bag->getDate('foo'); + $bag->getDate('foo'); } public function testGetCacheControlHeader() @@ -88,16 +93,32 @@ public function testGet() $bag = new HeaderBag(['foo' => 'bar', 'fuzz' => 'bizz']); $this->assertEquals('bar', $bag->get('foo'), '->get return current value'); $this->assertEquals('bar', $bag->get('FoO'), '->get key in case insensitive'); - $this->assertEquals(['bar'], $bag->get('foo', 'nope', false), '->get return the value as array'); + $this->assertEquals(['bar'], $bag->all('foo'), '->get return the value as array'); // defaults $this->assertNull($bag->get('none'), '->get unknown values returns null'); $this->assertEquals('default', $bag->get('none', 'default'), '->get unknown values returns default'); - $this->assertEquals(['default'], $bag->get('none', 'default', false), '->get unknown values returns default as array'); + $this->assertEquals([], $bag->all('none'), '->get unknown values returns an empty array'); $bag->set('foo', 'bor', false); $this->assertEquals('bar', $bag->get('foo'), '->get return first value'); - $this->assertEquals(['bar', 'bor'], $bag->get('foo', 'nope', false), '->get return all values as array'); + $this->assertEquals(['bar', 'bor'], $bag->all('foo'), '->get return all values as array'); + + $bag->set('baz', null); + $this->assertNull($bag->get('baz', 'nope'), '->get return null although different default value is given'); + } + + /** + * @group legacy + * @expectedDeprecation Passing a third argument to "Symfony\Component\HttpFoundation\HeaderBag::get()" is deprecated since Symfony 4.4, use method "all()" instead + */ + public function testGetIsEqualToNewMethod() + { + $bag = new HeaderBag(['foo' => 'bar', 'fuzz' => 'bizz']); + $this->assertSame($bag->all('none'), $bag->get('none', [], false), '->get unknown values returns default as array'); + + $bag->set('foo', 'bor', false); + $this->assertSame(['bar', 'bor'], $bag->get('foo', 'nope', false), '->get return all values as array'); } public function testSetAssociativeArray() @@ -105,7 +126,7 @@ public function testSetAssociativeArray() $bag = new HeaderBag(); $bag->set('foo', ['bad-assoc-index' => 'value']); $this->assertSame('value', $bag->get('foo')); - $this->assertEquals(['value'], $bag->get('foo', 'nope', false), 'assoc indices of multi-valued headers are ignored'); + $this->assertSame(['value'], $bag->all('foo'), 'assoc indices of multi-valued headers are ignored'); } public function testContains() diff --git a/Tests/HeaderUtilsTest.php b/Tests/HeaderUtilsTest.php index 2f82dc4e6..d2b19ca84 100644 --- a/Tests/HeaderUtilsTest.php +++ b/Tests/HeaderUtilsTest.php @@ -83,11 +83,9 @@ public function testUnquote() $this->assertEquals('foo \\ bar', HeaderUtils::unquote('"foo \\\\ bar"')); } - /** - * @expectedException \InvalidArgumentException - */ public function testMakeDispositionInvalidDisposition() { + $this->expectException('InvalidArgumentException'); HeaderUtils::makeDisposition('invalid', 'foo.html'); } @@ -113,10 +111,10 @@ public function provideMakeDisposition() /** * @dataProvider provideMakeDispositionFail - * @expectedException \InvalidArgumentException */ public function testMakeDispositionFail($disposition, $filename) { + $this->expectException('InvalidArgumentException'); HeaderUtils::makeDisposition($disposition, $filename); } diff --git a/Tests/IpUtilsTest.php b/Tests/IpUtilsTest.php index c7f76b5de..13b574379 100644 --- a/Tests/IpUtilsTest.php +++ b/Tests/IpUtilsTest.php @@ -73,11 +73,11 @@ public function getIpv6Data() } /** - * @expectedException \RuntimeException * @requires extension sockets */ public function testAnIpv6WithOptionDisabledIpv6() { + $this->expectException('RuntimeException'); if (\defined('AF_INET6')) { $this->markTestSkipped('Only works when PHP is compiled with the option "disable-ipv6".'); } @@ -101,4 +101,30 @@ public function invalidIpAddressData() 'invalid request IP with invalid proxy wildcard' => ['0.0.0.0', '*'], ]; } + + /** + * @dataProvider anonymizedIpData + */ + public function testAnonymize($ip, $expected) + { + $this->assertSame($expected, IpUtils::anonymize($ip)); + } + + public function anonymizedIpData() + { + return [ + ['192.168.1.1', '192.168.1.0'], + ['1.2.3.4', '1.2.3.0'], + ['2a01:198:603:0:396e:4789:8e99:890f', '2a01:198:603::'], + ['2a01:198:603:10:396e:4789:8e99:890f', '2a01:198:603:10::'], + ['::1', '::'], + ['0:0:0:0:0:0:0:1', '::'], + ['1:0:0:0:0:0:0:1', '1::'], + ['0:0:603:50:396e:4789:8e99:0001', '0:0:603:50::'], + ['[0:0:603:50:396e:4789:8e99:0001]', '[0:0:603:50::]'], + ['[2a01:198::3]', '[2a01:198::]'], + ['::ffff:123.234.235.236', '::ffff:123.234.235.0'], // IPv4-mapped IPv6 addresses + ['::123.234.235.236', '::123.234.235.0'], // deprecated IPv4-compatible IPv6 address + ]; + } } diff --git a/Tests/JsonResponseTest.php b/Tests/JsonResponseTest.php index 261c9d3e0..aa8441799 100644 --- a/Tests/JsonResponseTest.php +++ b/Tests/JsonResponseTest.php @@ -43,8 +43,8 @@ public function testConstructorWithSimpleTypes() $this->assertSame('0', $response->getContent()); $response = new JsonResponse(0.1); - $this->assertEquals('0.1', $response->getContent()); - $this->assertInternalType('string', $response->getContent()); + $this->assertEquals(0.1, $response->getContent()); + $this->assertIsString($response->getContent()); $response = new JsonResponse(true); $this->assertSame('true', $response->getContent()); @@ -132,8 +132,8 @@ public function testStaticCreateWithSimpleTypes() $response = JsonResponse::create(0.1); $this->assertInstanceOf('Symfony\Component\HttpFoundation\JsonResponse', $response); - $this->assertEquals('0.1', $response->getContent()); - $this->assertInternalType('string', $response->getContent()); + $this->assertEquals(0.1, $response->getContent()); + $this->assertIsString($response->getContent()); $response = JsonResponse::create(true); $this->assertInstanceOf('Symfony\Component\HttpFoundation\JsonResponse', $response); @@ -207,29 +207,23 @@ public function testItAcceptsJsonAsString() $this->assertSame('{"foo":"bar"}', $response->getContent()); } - /** - * @expectedException \InvalidArgumentException - */ public function testSetCallbackInvalidIdentifier() { + $this->expectException('InvalidArgumentException'); $response = new JsonResponse('foo'); $response->setCallback('+invalid'); } - /** - * @expectedException \InvalidArgumentException - */ public function testSetContent() { + $this->expectException('InvalidArgumentException'); JsonResponse::create("\xB1\x31"); } - /** - * @expectedException \Exception - * @expectedExceptionMessage This error is expected - */ public function testSetContentJsonSerializeError() { + $this->expectException('Exception'); + $this->expectExceptionMessage('This error is expected'); if (!interface_exists('JsonSerializable', false)) { $this->markTestSkipped('JsonSerializable is required.'); } diff --git a/Tests/RedirectResponseTest.php b/Tests/RedirectResponseTest.php index 5f6a8ac08..9d9797f7c 100644 --- a/Tests/RedirectResponseTest.php +++ b/Tests/RedirectResponseTest.php @@ -20,26 +20,20 @@ public function testGenerateMetaRedirect() { $response = new RedirectResponse('foo.bar'); - $this->assertEquals(1, preg_match( - '##', - preg_replace(['/\s+/', '/\'/'], [' ', '"'], $response->getContent()) - )); + $this->assertRegExp('##', preg_replace('/\s+/', ' ', $response->getContent())); } - /** - * @expectedException \InvalidArgumentException - */ - public function testRedirectResponseConstructorNullUrl() + public function testRedirectResponseConstructorEmptyUrl() { - $response = new RedirectResponse(null); + $this->expectException('InvalidArgumentException'); + $this->expectExceptionMessage('Cannot redirect to an empty URL.'); + new RedirectResponse(''); } - /** - * @expectedException \InvalidArgumentException - */ public function testRedirectResponseConstructorWrongStatusCode() { - $response = new RedirectResponse('foo.bar', 404); + $this->expectException('InvalidArgumentException'); + new RedirectResponse('foo.bar', 404); } public function testGenerateLocationHeader() @@ -65,11 +59,9 @@ public function testSetTargetUrl() $this->assertEquals('baz.beep', $response->getTargetUrl()); } - /** - * @expectedException \InvalidArgumentException - */ public function testSetTargetUrlNull() { + $this->expectException('InvalidArgumentException'); $response = new RedirectResponse('foo.bar'); $response->setTargetUrl(null); } diff --git a/Tests/RequestTest.php b/Tests/RequestTest.php index 20883221d..7c53ec2d5 100644 --- a/Tests/RequestTest.php +++ b/Tests/RequestTest.php @@ -19,7 +19,7 @@ class RequestTest extends TestCase { - protected function tearDown() + protected function tearDown(): void { Request::setTrustedProxies([], -1); Request::setTrustedHosts([]); @@ -399,6 +399,30 @@ public function testDuplicateWithFormat() $this->assertEquals('xml', $dup->getRequestFormat()); } + public function testGetPreferredFormat() + { + $request = new Request(); + $this->assertNull($request->getPreferredFormat(null)); + $this->assertSame('html', $request->getPreferredFormat()); + $this->assertSame('json', $request->getPreferredFormat('json')); + + $request->setRequestFormat('atom'); + $request->headers->set('Accept', 'application/ld+json'); + $this->assertSame('atom', $request->getPreferredFormat()); + + $request = new Request(); + $request->headers->set('Accept', 'application/xml'); + $this->assertSame('xml', $request->getPreferredFormat()); + + $request = new Request(); + $request->headers->set('Accept', 'application/xml'); + $this->assertSame('xml', $request->getPreferredFormat()); + + $request = new Request(); + $request->headers->set('Accept', 'application/json;q=0.8,application/xml;q=0.9'); + $this->assertSame('xml', $request->getPreferredFormat()); + } + /** * @dataProvider getFormatToMimeTypeMapProviderWithAdditionalNullFormat */ @@ -892,11 +916,9 @@ public function testGetPort() $this->assertEquals(80, $port, 'With only PROTO set and value is not recognized, getPort() defaults to 80.'); } - /** - * @expectedException \RuntimeException - */ public function testGetHostWithFakeHttpHostValue() { + $this->expectException('RuntimeException'); $request = new Request(); $request->initialize([], [], [], [], [], ['HTTP_HOST' => 'www.host.com?query=string']); $request->getHost(); @@ -1061,11 +1083,11 @@ public function getClientIpsProvider() } /** - * @expectedException \Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException * @dataProvider getClientIpsWithConflictingHeadersProvider */ public function testGetClientIpsWithConflictingHeaders($httpForwarded, $httpXForwardedFor) { + $this->expectException('Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException'); $request = new Request(); $server = [ @@ -1159,7 +1181,7 @@ public function testGetContentReturnsResource() { $req = new Request(); $retval = $req->getContent(true); - $this->assertInternalType('resource', $retval); + $this->assertIsResource($retval); $this->assertEquals('', fread($retval, 1)); $this->assertTrue(feof($retval)); } @@ -1169,7 +1191,7 @@ public function testGetContentReturnsResourceWhenContentSetInConstructor() $req = new Request([], [], [], [], [], [], 'MyContent'); $resource = $req->getContent(true); - $this->assertInternalType('resource', $resource); + $this->assertIsResource($resource); $this->assertEquals('MyContent', stream_get_contents($resource)); } @@ -1541,7 +1563,6 @@ public function testGetLanguages() $request = new Request(); $request->headers->set('Accept-language', 'zh, en-us; q=0.8, en; q=0.6'); $this->assertEquals(['zh', 'en_US', 'en'], $request->getLanguages()); - $this->assertEquals(['zh', 'en_US', 'en'], $request->getLanguages()); $request = new Request(); $request->headers->set('Accept-language', 'zh, en-us; q=0.6, en; q=0.8'); @@ -1632,14 +1653,14 @@ public function testToString() $asString = (string) $request; - $this->assertContains('Accept-Language: zh, en-us; q=0.8, en; q=0.6', $asString); - $this->assertContains('Cookie: Foo=Bar', $asString); + $this->assertStringContainsString('Accept-Language: zh, en-us; q=0.8, en; q=0.6', $asString); + $this->assertStringContainsString('Cookie: Foo=Bar', $asString); $request->cookies->set('Another', 'Cookie'); $asString = (string) $request; - $this->assertContains('Cookie: Foo=Bar; Another=Cookie', $asString); + $this->assertStringContainsString('Cookie: Foo=Bar; Another=Cookie', $asString); } public function testIsMethod() @@ -1780,7 +1801,7 @@ private function disableHttpMethodParameterOverride() $property->setValue(false); } - private function getRequestInstanceForClientIpTests($remoteAddr, $httpForwardedFor, $trustedProxies) + private function getRequestInstanceForClientIpTests(string $remoteAddr, ?string $httpForwardedFor, ?array $trustedProxies): Request { $request = new Request(); @@ -1798,7 +1819,7 @@ private function getRequestInstanceForClientIpTests($remoteAddr, $httpForwardedF return $request; } - private function getRequestInstanceForClientIpsForwardedTests($remoteAddr, $httpForwarded, $trustedProxies) + private function getRequestInstanceForClientIpsForwardedTests(string $remoteAddr, ?string $httpForwarded, ?array $trustedProxies): Request { $request = new Request(); @@ -2050,12 +2071,8 @@ public function testHostValidity($host, $isValid, $expectedHost = null, $expecte $this->assertSame($expectedPort, $request->getPort()); } } else { - if (method_exists($this, 'expectException')) { - $this->expectException(SuspiciousOperationException::class); - $this->expectExceptionMessage('Invalid Host'); - } else { - $this->setExpectedException(SuspiciousOperationException::class, 'Invalid Host'); - } + $this->expectException(SuspiciousOperationException::class); + $this->expectExceptionMessage('Invalid Host'); $request->getHost(); } @@ -2115,7 +2132,7 @@ public function testMethodSafe($method, $safe) { $request = new Request(); $request->setMethod($method); - $this->assertEquals($safe, $request->isMethodSafe(false)); + $this->assertEquals($safe, $request->isMethodSafe()); } public function methodSafeProvider() @@ -2134,16 +2151,6 @@ public function methodSafeProvider() ]; } - /** - * @expectedException \BadMethodCallException - */ - public function testMethodSafeChecksCacheable() - { - $request = new Request(); - $request->setMethod('OPTIONS'); - $request->isMethodSafe(); - } - /** * @dataProvider methodCacheableProvider */ @@ -2315,6 +2322,26 @@ public function testTrustedPortDoesNotDefaultToZero() $this->assertSame(80, $request->getPort()); } + + /** + * @dataProvider trustedProxiesRemoteAddr + */ + public function testTrustedProxiesRemoteAddr($serverRemoteAddr, $trustedProxies, $result) + { + $_SERVER['REMOTE_ADDR'] = $serverRemoteAddr; + Request::setTrustedProxies($trustedProxies, Request::HEADER_X_FORWARDED_ALL); + $this->assertSame($result, Request::getTrustedProxies()); + } + + public function trustedProxiesRemoteAddr() + { + return [ + ['1.1.1.1', ['REMOTE_ADDR'], ['1.1.1.1']], + ['1.1.1.1', ['REMOTE_ADDR', '2.2.2.2'], ['1.1.1.1', '2.2.2.2']], + [null, ['REMOTE_ADDR'], []], + [null, ['REMOTE_ADDR', '2.2.2.2'], ['2.2.2.2']], + ]; + } } class RequestContentProxy extends Request diff --git a/Tests/ResponseFunctionalTest.php b/Tests/ResponseFunctionalTest.php index 3d3e696c7..849a1395d 100644 --- a/Tests/ResponseFunctionalTest.php +++ b/Tests/ResponseFunctionalTest.php @@ -13,14 +13,11 @@ use PHPUnit\Framework\TestCase; -/** - * @requires PHP 7.0 - */ class ResponseFunctionalTest extends TestCase { private static $server; - public static function setUpBeforeClass() + public static function setUpBeforeClass(): void { $spec = [ 1 => ['file', '/dev/null', 'w'], @@ -32,7 +29,7 @@ public static function setUpBeforeClass() sleep(1); } - public static function tearDownAfterClass() + public static function tearDownAfterClass(): void { if (self::$server) { proc_terminate(self::$server); diff --git a/Tests/ResponseHeaderBagTest.php b/Tests/ResponseHeaderBagTest.php index 35df36c1c..18b3c36a6 100644 --- a/Tests/ResponseHeaderBagTest.php +++ b/Tests/ResponseHeaderBagTest.php @@ -51,9 +51,9 @@ public function testCacheControlHeader() $this->assertTrue($bag->hasCacheControlDirective('public')); $bag = new ResponseHeaderBag(['ETag' => 'abcde']); - $this->assertEquals('private, must-revalidate', $bag->get('Cache-Control')); + $this->assertEquals('no-cache, private', $bag->get('Cache-Control')); $this->assertTrue($bag->hasCacheControlDirective('private')); - $this->assertTrue($bag->hasCacheControlDirective('must-revalidate')); + $this->assertTrue($bag->hasCacheControlDirective('no-cache')); $this->assertFalse($bag->hasCacheControlDirective('max-age')); $bag = new ResponseHeaderBag(['Expires' => 'Wed, 16 Feb 2011 14:17:43 GMT']); @@ -89,13 +89,13 @@ public function testCacheControlHeader() $bag = new ResponseHeaderBag(); $bag->set('Cache-Control', ['public', 'must-revalidate']); - $this->assertCount(1, $bag->get('Cache-Control', null, false)); + $this->assertCount(1, $bag->all('Cache-Control')); $this->assertEquals('must-revalidate, public', $bag->get('Cache-Control')); $bag = new ResponseHeaderBag(); $bag->set('Cache-Control', 'public'); $bag->set('Cache-Control', 'must-revalidate', false); - $this->assertCount(1, $bag->get('Cache-Control', null, false)); + $this->assertCount(1, $bag->all('Cache-Control')); $this->assertEquals('must-revalidate, public', $bag->get('Cache-Control')); } @@ -128,6 +128,14 @@ public function testClearCookieSecureNotHttpOnly() $this->assertSetCookieHeader('foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; Max-Age=0; path=/; secure', $bag); } + public function testClearCookieSamesite() + { + $bag = new ResponseHeaderBag([]); + + $bag->clearCookie('foo', '/', null, true, false, 'none'); + $this->assertSetCookieHeader('foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; Max-Age=0; path=/; secure; samesite=none', $bag); + } + public function testReplace() { $bag = new ResponseHeaderBag([]); @@ -166,7 +174,7 @@ public function testCookiesWithSameNames() 'foo=bar; path=/path/bar; domain=foo.bar; httponly; samesite=lax', 'foo=bar; path=/path/bar; domain=bar.foo; httponly; samesite=lax', 'foo=bar; path=/; httponly; samesite=lax', - ], $bag->get('set-cookie', null, false)); + ], $bag->all('set-cookie')); $this->assertSetCookieHeader('foo=bar; path=/path/foo; domain=foo.bar; httponly; samesite=lax', $bag); $this->assertSetCookieHeader('foo=bar; path=/path/bar; domain=foo.bar; httponly; samesite=lax', $bag); @@ -240,11 +248,9 @@ public function testSetCookieHeader() $this->assertEquals([], $bag->getCookies()); } - /** - * @expectedException \InvalidArgumentException - */ public function testGetCookiesWithInvalidArgument() { + $this->expectException('InvalidArgumentException'); $bag = new ResponseHeaderBag(); $bag->getCookies('invalid_argument'); @@ -301,7 +307,7 @@ public function testDateHeaderWillBeRecreatedWhenHeadersAreReplaced() $this->assertTrue($bag->has('Date')); } - private function assertSetCookieHeader($expected, ResponseHeaderBag $actual) + private function assertSetCookieHeader(string $expected, ResponseHeaderBag $actual) { $this->assertRegExp('#^Set-Cookie:\s+'.preg_quote($expected, '#').'$#m', str_replace("\r\n", "\n", (string) $actual)); } diff --git a/Tests/ResponseTest.php b/Tests/ResponseTest.php index 7856a77c0..0d3c37aaa 100644 --- a/Tests/ResponseTest.php +++ b/Tests/ResponseTest.php @@ -370,6 +370,12 @@ public function testExpire() $this->assertNull($response->headers->get('Expires'), '->expire() removes the Expires header when the response is fresh'); } + public function testNullExpireHeader() + { + $response = new Response(null, 200, ['Expires' => null]); + $this->assertNull($response->getExpires()); + } + public function testGetTtl() { $response = new Response(); @@ -455,18 +461,10 @@ public function testSetVary() public function testDefaultContentType() { - $headerMock = $this->getMockBuilder('Symfony\Component\HttpFoundation\ResponseHeaderBag')->setMethods(['set'])->getMock(); - $headerMock->expects($this->at(0)) - ->method('set') - ->with('Content-Type', 'text/html'); - $headerMock->expects($this->at(1)) - ->method('set') - ->with('Content-Type', 'text/html; charset=UTF-8'); - $response = new Response('foo'); - $response->headers = $headerMock; - $response->prepare(new Request()); + + $this->assertSame('text/html; charset=UTF-8', $response->headers->get('Content-Type')); } public function testContentTypeCharset() @@ -499,6 +497,20 @@ public function testPrepareDoesNothingIfRequestFormatIsNotDefined() $this->assertEquals('text/html; charset=UTF-8', $response->headers->get('content-type')); } + /** + * Same URL cannot produce different Content-Type based on the value of the Accept header, + * unless explicitly stated in the response object. + */ + public function testPrepareDoesNotSetContentTypeBasedOnRequestAcceptHeader() + { + $response = new Response('foo'); + $request = Request::create('/'); + $request->headers->set('Accept', 'application/json'); + $response->prepare($request); + + $this->assertSame('text/html; charset=UTF-8', $response->headers->get('content-type')); + } + public function testPrepareSetContentType() { $response = new Response('foo'); @@ -533,7 +545,6 @@ public function testPrepareRemovesContentForInformationalResponse() $response->prepare($request); $this->assertEquals('', $response->getContent()); $this->assertFalse($response->headers->has('Content-Type')); - $this->assertFalse($response->headers->has('Content-Type')); $response->setContent('content'); $response->setStatusCode(304); @@ -601,25 +612,25 @@ public function testSetCache() $this->fail('->setCache() throws an InvalidArgumentException if an option is not supported'); } catch (\Exception $e) { $this->assertInstanceOf('InvalidArgumentException', $e, '->setCache() throws an InvalidArgumentException if an option is not supported'); - $this->assertContains('"wrong option"', $e->getMessage()); + $this->assertStringContainsString('"wrong option"', $e->getMessage()); } $options = ['etag' => '"whatever"']; $response->setCache($options); - $this->assertEquals($response->getEtag(), '"whatever"'); + $this->assertEquals('"whatever"', $response->getEtag()); $now = $this->createDateTimeNow(); $options = ['last_modified' => $now]; $response->setCache($options); - $this->assertEquals($response->getLastModified()->getTimestamp(), $now->getTimestamp()); + $this->assertEquals($now->getTimestamp(), $response->getLastModified()->getTimestamp()); $options = ['max_age' => 100]; $response->setCache($options); - $this->assertEquals($response->getMaxAge(), 100); + $this->assertEquals(100, $response->getMaxAge()); $options = ['s_maxage' => 200]; $response->setCache($options); - $this->assertEquals($response->getMaxAge(), 200); + $this->assertEquals(200, $response->getMaxAge()); $this->assertTrue($response->headers->hasCacheControlDirective('public')); $this->assertFalse($response->headers->hasCacheControlDirective('private')); @@ -654,7 +665,7 @@ public function testSendContent() ob_start(); $response->sendContent(); $string = ob_get_clean(); - $this->assertContains('test response rendering', $string); + $this->assertStringContainsString('test response rendering', $string); } public function testSetPublic() @@ -901,11 +912,11 @@ public function testSetContent($content) } /** - * @expectedException \UnexpectedValueException * @dataProvider invalidContentProvider */ public function testSetContentInvalid($content) { + $this->expectException('UnexpectedValueException'); $response = new Response(); $response->setContent($content); } @@ -990,11 +1001,11 @@ protected function provideResponse() } /** - * @see http://github.com/zendframework/zend-diactoros for the canonical source repository + * @see http://github.com/zendframework/zend-diactoros for the canonical source repository * - * @author Fábio Pacheco + * @author Fábio Pacheco * @copyright Copyright (c) 2015-2016 Zend Technologies USA Inc. (http://www.zend.com) - * @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License + * @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License */ public function ianaCodesReasonPhrasesProvider() { @@ -1054,7 +1065,7 @@ public function testReasonPhraseDefaultsAgainstIana($code, $reasonPhrase) class StringableObject { - public function __toString() + public function __toString(): string { return 'Foo'; } diff --git a/Tests/Session/Attribute/AttributeBagTest.php b/Tests/Session/Attribute/AttributeBagTest.php index 44c8174e3..6313967af 100644 --- a/Tests/Session/Attribute/AttributeBagTest.php +++ b/Tests/Session/Attribute/AttributeBagTest.php @@ -28,7 +28,7 @@ class AttributeBagTest extends TestCase */ private $bag; - protected function setUp() + protected function setUp(): void { $this->array = [ 'hello' => 'world', @@ -49,7 +49,7 @@ protected function setUp() $this->bag->initialize($this->array); } - protected function tearDown() + protected function tearDown(): void { $this->bag = null; $this->array = []; diff --git a/Tests/Session/Attribute/NamespacedAttributeBagTest.php b/Tests/Session/Attribute/NamespacedAttributeBagTest.php index 6b4bb17d6..3a3251d05 100644 --- a/Tests/Session/Attribute/NamespacedAttributeBagTest.php +++ b/Tests/Session/Attribute/NamespacedAttributeBagTest.php @@ -28,7 +28,7 @@ class NamespacedAttributeBagTest extends TestCase */ private $bag; - protected function setUp() + protected function setUp(): void { $this->array = [ 'hello' => 'world', @@ -49,7 +49,7 @@ protected function setUp() $this->bag->initialize($this->array); } - protected function tearDown() + protected function tearDown(): void { $this->bag = null; $this->array = []; diff --git a/Tests/Session/Flash/AutoExpireFlashBagTest.php b/Tests/Session/Flash/AutoExpireFlashBagTest.php index b4e2c3a5a..ba2687199 100644 --- a/Tests/Session/Flash/AutoExpireFlashBagTest.php +++ b/Tests/Session/Flash/AutoExpireFlashBagTest.php @@ -28,7 +28,7 @@ class AutoExpireFlashBagTest extends TestCase protected $array = []; - protected function setUp() + protected function setUp(): void { parent::setUp(); $this->bag = new FlashBag(); @@ -36,7 +36,7 @@ protected function setUp() $this->bag->initialize($this->array); } - protected function tearDown() + protected function tearDown(): void { $this->bag = null; parent::tearDown(); diff --git a/Tests/Session/Flash/FlashBagTest.php b/Tests/Session/Flash/FlashBagTest.php index 6d8619e07..24dbbfe98 100644 --- a/Tests/Session/Flash/FlashBagTest.php +++ b/Tests/Session/Flash/FlashBagTest.php @@ -28,7 +28,7 @@ class FlashBagTest extends TestCase protected $array = []; - protected function setUp() + protected function setUp(): void { parent::setUp(); $this->bag = new FlashBag(); @@ -36,7 +36,7 @@ protected function setUp() $this->bag->initialize($this->array); } - protected function tearDown() + protected function tearDown(): void { $this->bag = null; parent::tearDown(); diff --git a/Tests/Session/SessionTest.php b/Tests/Session/SessionTest.php index acb129984..e216bfc8c 100644 --- a/Tests/Session/SessionTest.php +++ b/Tests/Session/SessionTest.php @@ -37,13 +37,13 @@ class SessionTest extends TestCase */ protected $session; - protected function setUp() + protected function setUp(): void { $this->storage = new MockArraySessionStorage(); $this->session = new Session($this->storage, new AttributeBag(), new FlashBag()); } - protected function tearDown() + protected function tearDown(): void { $this->storage = null; $this->session = null; diff --git a/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php b/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php index c0651498f..3f3982ff4 100644 --- a/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php +++ b/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php @@ -37,13 +37,18 @@ abstract class AbstractRedisSessionHandlerTestCase extends TestCase */ abstract protected function createRedisClient(string $host); - protected function setUp() + protected function setUp(): void { parent::setUp(); if (!\extension_loaded('redis')) { self::markTestSkipped('Extension redis required.'); } + try { + (new \Redis())->connect(getenv('REDIS_HOST')); + } catch (\Exception $e) { + self::markTestSkipped($e->getMessage()); + } $host = getenv('REDIS_HOST') ?: 'localhost'; @@ -54,7 +59,7 @@ protected function setUp() ); } - protected function tearDown() + protected function tearDown(): void { $this->redisClient = null; $this->storage = null; @@ -139,7 +144,37 @@ public function getOptionFixtures(): array { return [ [['prefix' => 'session'], true], + [['ttl' => 1000], true], + [['prefix' => 'sfs', 'ttl' => 1000], true], [['prefix' => 'sfs', 'foo' => 'bar'], false], + [['ttl' => 'sfs', 'foo' => 'bar'], false], + ]; + } + + /** + * @dataProvider getTtlFixtures + */ + public function testUseTtlOption(int $ttl) + { + $options = [ + 'prefix' => self::PREFIX, + 'ttl' => $ttl, + ]; + + $handler = new RedisSessionHandler($this->redisClient, $options); + $handler->write('id', 'data'); + $redisTtl = $this->redisClient->ttl(self::PREFIX.'id'); + + $this->assertLessThan($redisTtl, $ttl - 5); + $this->assertGreaterThan($redisTtl, $ttl + 5); + } + + public function getTtlFixtures(): array + { + return [ + ['ttl' => 5000], + ['ttl' => 120], + ['ttl' => 60], ]; } } diff --git a/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php b/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php index f65e62b50..b25b68bbb 100644 --- a/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php @@ -17,7 +17,7 @@ class AbstractSessionHandlerTest extends TestCase { private static $server; - public static function setUpBeforeClass() + public static function setUpBeforeClass(): void { $spec = [ 1 => ['file', '/dev/null', 'w'], @@ -29,7 +29,7 @@ public static function setUpBeforeClass() sleep(1); } - public static function tearDownAfterClass() + public static function tearDownAfterClass(): void { if (self::$server) { proc_terminate(self::$server); diff --git a/Tests/Session/Storage/Handler/Fixtures/common.inc b/Tests/Session/Storage/Handler/Fixtures/common.inc index 7a064c7f3..a887f607e 100644 --- a/Tests/Session/Storage/Handler/Fixtures/common.inc +++ b/Tests/Session/Storage/Handler/Fixtures/common.inc @@ -60,14 +60,14 @@ class TestSessionHandler extends AbstractSessionHandler $this->data = $data; } - public function open($path, $name) + public function open($path, $name): bool { echo __FUNCTION__, "\n"; return parent::open($path, $name); } - public function validateId($sessionId) + public function validateId($sessionId): bool { echo __FUNCTION__, "\n"; @@ -77,7 +77,7 @@ class TestSessionHandler extends AbstractSessionHandler /** * {@inheritdoc} */ - public function read($sessionId) + public function read($sessionId): string { echo __FUNCTION__, "\n"; @@ -87,7 +87,7 @@ class TestSessionHandler extends AbstractSessionHandler /** * {@inheritdoc} */ - public function updateTimestamp($sessionId, $data) + public function updateTimestamp($sessionId, $data): bool { echo __FUNCTION__, "\n"; @@ -97,7 +97,7 @@ class TestSessionHandler extends AbstractSessionHandler /** * {@inheritdoc} */ - public function write($sessionId, $data) + public function write($sessionId, $data): bool { echo __FUNCTION__, "\n"; @@ -107,42 +107,42 @@ class TestSessionHandler extends AbstractSessionHandler /** * {@inheritdoc} */ - public function destroy($sessionId) + public function destroy($sessionId): bool { echo __FUNCTION__, "\n"; return parent::destroy($sessionId); } - public function close() + public function close(): bool { echo __FUNCTION__, "\n"; return true; } - public function gc($maxLifetime) + public function gc($maxLifetime): bool { echo __FUNCTION__, "\n"; return true; } - protected function doRead($sessionId) + protected function doRead($sessionId): string { echo __FUNCTION__.': ', $this->data, "\n"; return $this->data; } - protected function doWrite($sessionId, $data) + protected function doWrite($sessionId, $data): bool { echo __FUNCTION__.': ', $data, "\n"; return true; } - protected function doDestroy($sessionId) + protected function doDestroy($sessionId): bool { echo __FUNCTION__, "\n"; diff --git a/Tests/Session/Storage/Handler/Fixtures/regenerate.expected b/Tests/Session/Storage/Handler/Fixtures/regenerate.expected index baa5f2f6f..d825f44f7 100644 --- a/Tests/Session/Storage/Handler/Fixtures/regenerate.expected +++ b/Tests/Session/Storage/Handler/Fixtures/regenerate.expected @@ -11,6 +11,7 @@ validateId read doRead: abc|i:123; read +doRead: abc|i:123; write doWrite: abc|i:123; diff --git a/Tests/Session/Storage/Handler/Fixtures/storage.expected b/Tests/Session/Storage/Handler/Fixtures/storage.expected index 4533a10a1..05a5d5d0b 100644 --- a/Tests/Session/Storage/Handler/Fixtures/storage.expected +++ b/Tests/Session/Storage/Handler/Fixtures/storage.expected @@ -11,10 +11,11 @@ $_SESSION is not empty write destroy close -$_SESSION is not empty +$_SESSION is empty Array ( [0] => Content-Type: text/plain; charset=utf-8 [1] => Cache-Control: max-age=0, private, must-revalidate + [2] => Set-Cookie: sid=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly ) shutdown diff --git a/Tests/Session/Storage/Handler/Fixtures/with_cookie_and_session.expected b/Tests/Session/Storage/Handler/Fixtures/with_cookie_and_session.expected index 5de2d9e39..63078228d 100644 --- a/Tests/Session/Storage/Handler/Fixtures/with_cookie_and_session.expected +++ b/Tests/Session/Storage/Handler/Fixtures/with_cookie_and_session.expected @@ -20,5 +20,6 @@ Array [0] => Content-Type: text/plain; charset=utf-8 [1] => Cache-Control: max-age=10800, private, must-revalidate [2] => Set-Cookie: abc=def + [3] => Set-Cookie: sid=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly ) shutdown diff --git a/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php b/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php index 2393ddf18..e9c17703a 100644 --- a/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php @@ -30,7 +30,7 @@ class MemcachedSessionHandlerTest extends TestCase protected $memcached; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -45,7 +45,7 @@ protected function setUp() ); } - protected function tearDown() + protected function tearDown(): void { $this->memcached = null; $this->storage = null; diff --git a/Tests/Session/Storage/Handler/MigratingSessionHandlerTest.php b/Tests/Session/Storage/Handler/MigratingSessionHandlerTest.php index 6dc5b0cb5..01615e6b1 100644 --- a/Tests/Session/Storage/Handler/MigratingSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/MigratingSessionHandlerTest.php @@ -20,7 +20,7 @@ class MigratingSessionHandlerTest extends TestCase private $currentHandler; private $writeOnlyHandler; - protected function setUp() + protected function setUp(): void { $this->currentHandler = $this->createMock(\SessionHandlerInterface::class); $this->writeOnlyHandler = $this->createMock(\SessionHandlerInterface::class); diff --git a/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php b/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php index 5fcdad9b5..f0e2d4f50 100644 --- a/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler; @@ -22,13 +23,13 @@ class MongoDbSessionHandlerTest extends TestCase { /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ private $mongo; private $storage; public $options; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -52,11 +53,9 @@ protected function setUp() $this->storage = new MongoDbSessionHandler($this->mongo, $this->options); } - /** - * @expectedException \InvalidArgumentException - */ public function testConstructorShouldThrowExceptionForMissingOptions() { + $this->expectException('InvalidArgumentException'); new MongoDbSessionHandler($this->mongo, []); } @@ -87,7 +86,7 @@ public function testRead() ->method('findOne') ->willReturnCallback(function ($criteria) use ($testTimeout) { $this->assertArrayHasKey($this->options['id_field'], $criteria); - $this->assertEquals($criteria[$this->options['id_field']], 'foo'); + $this->assertEquals('foo', $criteria[$this->options['id_field']]); $this->assertArrayHasKey($this->options['expiry_field'], $criteria); $this->assertArrayHasKey('$gte', $criteria[$this->options['expiry_field']]); @@ -198,7 +197,7 @@ public function testGetConnection() $this->assertInstanceOf(\MongoDB\Client::class, $method->invoke($this->storage)); } - private function createMongoCollectionMock() + private function createMongoCollectionMock(): \MongoDB\Collection { $collection = $this->getMockBuilder(\MongoDB\Collection::class) ->disableOriginalConstructor() diff --git a/Tests/Session/Storage/Handler/NativeFileSessionHandlerTest.php b/Tests/Session/Storage/Handler/NativeFileSessionHandlerTest.php index e227bebf6..368af6a3e 100644 --- a/Tests/Session/Storage/Handler/NativeFileSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/NativeFileSessionHandlerTest.php @@ -27,7 +27,7 @@ class NativeFileSessionHandlerTest extends TestCase { public function testConstruct() { - $storage = new NativeSessionStorage(['name' => 'TESTING'], new NativeFileSessionHandler(sys_get_temp_dir())); + new NativeSessionStorage(['name' => 'TESTING'], new NativeFileSessionHandler(sys_get_temp_dir())); $this->assertEquals('user', ini_get('session.save_handler')); @@ -40,9 +40,9 @@ public function testConstruct() */ public function testConstructSavePath($savePath, $expectedSavePath, $path) { - $handler = new NativeFileSessionHandler($savePath); + new NativeFileSessionHandler($savePath); $this->assertEquals($expectedSavePath, ini_get('session.save_path')); - $this->assertTrue(is_dir(realpath($path))); + $this->assertDirectoryExists(realpath($path)); rmdir($path); } @@ -58,18 +58,16 @@ public function savePathDataProvider() ]; } - /** - * @expectedException \InvalidArgumentException - */ public function testConstructException() { - $handler = new NativeFileSessionHandler('something;invalid;with;too-many-args'); + $this->expectException('InvalidArgumentException'); + new NativeFileSessionHandler('something;invalid;with;too-many-args'); } public function testConstructDefault() { $path = ini_get('session.save_path'); - $storage = new NativeSessionStorage(['name' => 'TESTING'], new NativeFileSessionHandler()); + new NativeSessionStorage(['name' => 'TESTING'], new NativeFileSessionHandler()); $this->assertEquals($path, ini_get('session.save_path')); } diff --git a/Tests/Session/Storage/Handler/NullSessionHandlerTest.php b/Tests/Session/Storage/Handler/NullSessionHandlerTest.php index 0d246e1aa..f793db144 100644 --- a/Tests/Session/Storage/Handler/NullSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/NullSessionHandlerTest.php @@ -28,7 +28,7 @@ class NullSessionHandlerTest extends TestCase { public function testSaveHandlers() { - $storage = $this->getStorage(); + $this->getStorage(); $this->assertEquals('user', ini_get('session.save_handler')); } diff --git a/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php b/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php index 380b4d7da..03796e66e 100644 --- a/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php @@ -22,7 +22,7 @@ class PdoSessionHandlerTest extends TestCase { private $dbFile; - protected function tearDown() + protected function tearDown(): void { // make sure the temporary database file is deleted when it has been created (even when a test fails) if ($this->dbFile) { @@ -48,22 +48,18 @@ protected function getMemorySqlitePdo() return $pdo; } - /** - * @expectedException \InvalidArgumentException - */ public function testWrongPdoErrMode() { + $this->expectException('InvalidArgumentException'); $pdo = $this->getMemorySqlitePdo(); $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_SILENT); - $storage = new PdoSessionHandler($pdo); + new PdoSessionHandler($pdo); } - /** - * @expectedException \RuntimeException - */ public function testInexistentTable() { + $this->expectException('RuntimeException'); $storage = new PdoSessionHandler($this->getMemorySqlitePdo(), ['db_table' => 'inexistent_table']); $storage->open('', 'sid'); $storage->read('id'); @@ -71,11 +67,9 @@ public function testInexistentTable() $storage->close(); } - /** - * @expectedException \RuntimeException - */ public function testCreateTableTwice() { + $this->expectException('RuntimeException'); $storage = new PdoSessionHandler($this->getMemorySqlitePdo()); $storage->createTable(); } @@ -323,15 +317,15 @@ public function testGetConnectionConnectsIfNeeded() public function testUrlDsn($url, $expectedDsn, $expectedUser = null, $expectedPassword = null) { $storage = new PdoSessionHandler($url); - - $this->assertAttributeEquals($expectedDsn, 'dsn', $storage); - - if (null !== $expectedUser) { - $this->assertAttributeEquals($expectedUser, 'username', $storage); - } - - if (null !== $expectedPassword) { - $this->assertAttributeEquals($expectedPassword, 'password', $storage); + $reflection = new \ReflectionClass(PdoSessionHandler::class); + + foreach (['dsn' => $expectedDsn, 'username' => $expectedUser, 'password' => $expectedPassword] as $property => $expectedValue) { + if (!isset($expectedValue)) { + continue; + } + $property = $reflection->getProperty($property); + $property->setAccessible(true); + $this->assertSame($expectedValue, $property->getValue($storage)); } } @@ -352,6 +346,9 @@ public function provideUrlDsnPairs() yield ['mssql://localhost:56/test', 'sqlsrv:server=localhost,56;Database=test']; } + /** + * @return resource + */ private function createStream($content) { $stream = tmpfile(); @@ -368,7 +365,7 @@ class MockPdo extends \PDO private $driverName; private $errorMode; - public function __construct($driverName = null, $errorMode = null) + public function __construct(string $driverName = null, int $errorMode = null) { $this->driverName = $driverName; $this->errorMode = null !== $errorMode ?: \PDO::ERRMODE_EXCEPTION; diff --git a/Tests/Session/Storage/Handler/PredisClusterSessionHandlerTest.php b/Tests/Session/Storage/Handler/PredisClusterSessionHandlerTest.php index 622b42da1..8926fb1a9 100644 --- a/Tests/Session/Storage/Handler/PredisClusterSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/PredisClusterSessionHandlerTest.php @@ -13,6 +13,9 @@ use Predis\Client; +/** + * @group integration + */ class PredisClusterSessionHandlerTest extends AbstractRedisSessionHandlerTestCase { protected function createRedisClient(string $host): Client diff --git a/Tests/Session/Storage/Handler/PredisSessionHandlerTest.php b/Tests/Session/Storage/Handler/PredisSessionHandlerTest.php index 5ecab116f..bb33a3d9a 100644 --- a/Tests/Session/Storage/Handler/PredisSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/PredisSessionHandlerTest.php @@ -13,6 +13,9 @@ use Predis\Client; +/** + * @group integration + */ class PredisSessionHandlerTest extends AbstractRedisSessionHandlerTestCase { protected function createRedisClient(string $host): Client diff --git a/Tests/Session/Storage/Handler/RedisArraySessionHandlerTest.php b/Tests/Session/Storage/Handler/RedisArraySessionHandlerTest.php index b03a37236..c2647c35b 100644 --- a/Tests/Session/Storage/Handler/RedisArraySessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/RedisArraySessionHandlerTest.php @@ -11,9 +11,15 @@ namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler; +/** + * @group integration + */ class RedisArraySessionHandlerTest extends AbstractRedisSessionHandlerTestCase { - protected function createRedisClient(string $host): \RedisArray + /** + * @return \RedisArray|object + */ + protected function createRedisClient(string $host) { return new \RedisArray([$host]); } diff --git a/Tests/Session/Storage/Handler/RedisClusterSessionHandlerTest.php b/Tests/Session/Storage/Handler/RedisClusterSessionHandlerTest.php index 7d85a59ee..278b3c876 100644 --- a/Tests/Session/Storage/Handler/RedisClusterSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/RedisClusterSessionHandlerTest.php @@ -11,9 +11,12 @@ namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler; +/** + * @group integration + */ class RedisClusterSessionHandlerTest extends AbstractRedisSessionHandlerTestCase { - public static function setupBeforeClass() + public static function setUpBeforeClass(): void { if (!class_exists('RedisCluster')) { self::markTestSkipped('The RedisCluster class is required.'); @@ -24,7 +27,10 @@ public static function setupBeforeClass() } } - protected function createRedisClient(string $host): \RedisCluster + /** + * @return \RedisCluster|object + */ + protected function createRedisClient(string $host) { return new \RedisCluster(null, explode(' ', getenv('REDIS_CLUSTER_HOSTS'))); } diff --git a/Tests/Session/Storage/Handler/RedisSessionHandlerTest.php b/Tests/Session/Storage/Handler/RedisSessionHandlerTest.php index afdb6c503..e7fb1ca19 100644 --- a/Tests/Session/Storage/Handler/RedisSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/RedisSessionHandlerTest.php @@ -11,9 +11,15 @@ namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler; +/** + * @group integration + */ class RedisSessionHandlerTest extends AbstractRedisSessionHandlerTestCase { - protected function createRedisClient(string $host): \Redis + /** + * @return \Redis|object + */ + protected function createRedisClient(string $host) { $client = new \Redis(); $client->connect($host); diff --git a/Tests/Session/Storage/MetadataBagTest.php b/Tests/Session/Storage/MetadataBagTest.php index 2c4758b91..e040f4862 100644 --- a/Tests/Session/Storage/MetadataBagTest.php +++ b/Tests/Session/Storage/MetadataBagTest.php @@ -28,7 +28,7 @@ class MetadataBagTest extends TestCase protected $array = []; - protected function setUp() + protected function setUp(): void { parent::setUp(); $this->bag = new MetadataBag(); @@ -36,7 +36,7 @@ protected function setUp() $this->bag->initialize($this->array); } - protected function tearDown() + protected function tearDown(): void { $this->array = []; $this->bag = null; diff --git a/Tests/Session/Storage/MockArraySessionStorageTest.php b/Tests/Session/Storage/MockArraySessionStorageTest.php index 2e3024ef1..b99e71985 100644 --- a/Tests/Session/Storage/MockArraySessionStorageTest.php +++ b/Tests/Session/Storage/MockArraySessionStorageTest.php @@ -40,7 +40,7 @@ class MockArraySessionStorageTest extends TestCase private $data; - protected function setUp() + protected function setUp(): void { $this->attributes = new AttributeBag(); $this->flashes = new FlashBag(); @@ -56,7 +56,7 @@ protected function setUp() $this->storage->setSessionData($this->data); } - protected function tearDown() + protected function tearDown(): void { $this->data = null; $this->flashes = null; @@ -121,11 +121,9 @@ public function testClearWithNoBagsStartsSession() $this->assertTrue($storage->isStarted()); } - /** - * @expectedException \RuntimeException - */ public function testUnstartedSave() { + $this->expectException('RuntimeException'); $this->storage->save(); } } diff --git a/Tests/Session/Storage/MockFileSessionStorageTest.php b/Tests/Session/Storage/MockFileSessionStorageTest.php index 062769e28..9eb8e89b1 100644 --- a/Tests/Session/Storage/MockFileSessionStorageTest.php +++ b/Tests/Session/Storage/MockFileSessionStorageTest.php @@ -33,20 +33,20 @@ class MockFileSessionStorageTest extends TestCase */ protected $storage; - protected function setUp() + protected function setUp(): void { $this->sessionDir = sys_get_temp_dir().'/sftest'; $this->storage = $this->getStorage(); } - protected function tearDown() + protected function tearDown(): void { - $this->sessionDir = null; - $this->storage = null; - array_map('unlink', glob($this->sessionDir.'/*.session')); + array_map('unlink', glob($this->sessionDir.'/*')); if (is_dir($this->sessionDir)) { rmdir($this->sessionDir); } + $this->sessionDir = null; + $this->storage = null; } public function testStart() @@ -107,16 +107,14 @@ public function testMultipleInstances() $this->assertEquals('bar', $storage2->getBag('attributes')->get('foo'), 'values persist between instances'); } - /** - * @expectedException \RuntimeException - */ public function testSaveWithoutStart() { + $this->expectException('RuntimeException'); $storage1 = $this->getStorage(); $storage1->save(); } - private function getStorage() + private function getStorage(): MockFileSessionStorage { $storage = new MockFileSessionStorage($this->sessionDir); $storage->registerBag(new FlashBag()); diff --git a/Tests/Session/Storage/NativeSessionStorageTest.php b/Tests/Session/Storage/NativeSessionStorageTest.php index d97973e2e..4cb0b2dab 100644 --- a/Tests/Session/Storage/NativeSessionStorageTest.php +++ b/Tests/Session/Storage/NativeSessionStorageTest.php @@ -33,7 +33,7 @@ class NativeSessionStorageTest extends TestCase { private $savePath; - protected function setUp() + protected function setUp(): void { $this->iniSet('session.save_handler', 'files'); $this->iniSet('session.save_path', $this->savePath = sys_get_temp_dir().'/sftest'); @@ -42,7 +42,7 @@ protected function setUp() } } - protected function tearDown() + protected function tearDown(): void { session_write_close(); array_map('unlink', glob($this->savePath.'/*')); @@ -53,10 +53,7 @@ protected function tearDown() $this->savePath = null; } - /** - * @return NativeSessionStorage - */ - protected function getStorage(array $options = []) + protected function getStorage(array $options = []): NativeSessionStorage { $storage = new NativeSessionStorage($options); $storage->registerBag(new AttributeBag()); @@ -72,20 +69,16 @@ public function testBag() $this->assertSame($bag, $storage->getBag($bag->getName())); } - /** - * @expectedException \InvalidArgumentException - */ public function testRegisterBagException() { + $this->expectException('InvalidArgumentException'); $storage = $this->getStorage(); $storage->getBag('non_existing'); } - /** - * @expectedException \LogicException - */ public function testRegisterBagForAStartedSessionThrowsException() { + $this->expectException('LogicException'); $storage = $this->getStorage(); $storage->start(); $storage->registerBag(new AttributeBag()); @@ -98,7 +91,7 @@ public function testGetId() $storage->start(); $id = $storage->getId(); - $this->assertInternalType('string', $id); + $this->assertIsString($id); $this->assertNotSame('', $id); $storage->save(); @@ -127,6 +120,19 @@ public function testRegenerateDestroy() $this->assertEquals(11, $storage->getBag('attributes')->get('legs')); } + public function testRegenerateWithCustomLifetime() + { + $storage = $this->getStorage(); + $storage->start(); + $id = $storage->getId(); + $lifetime = 999999; + $storage->getBag('attributes')->set('legs', 11); + $storage->regenerate(false, $lifetime); + $this->assertNotEquals($id, $storage->getId()); + $this->assertEquals(11, $storage->getBag('attributes')->get('legs')); + $this->assertEquals($lifetime, ini_get('session.cookie_lifetime')); + } + public function testSessionGlobalIsUpToDateAfterIdRegeneration() { $storage = $this->getStorage(); @@ -149,7 +155,7 @@ public function testDefaultSessionCacheLimiter() { $this->iniSet('session.cache_limiter', 'nocache'); - $storage = new NativeSessionStorage(); + new NativeSessionStorage(); $this->assertEquals('', ini_get('session.cache_limiter')); } @@ -157,7 +163,7 @@ public function testExplicitSessionCacheLimiter() { $this->iniSet('session.cache_limiter', 'nocache'); - $storage = new NativeSessionStorage(['cache_limiter' => 'public']); + new NativeSessionStorage(['cache_limiter' => 'public']); $this->assertEquals('public', ini_get('session.cache_limiter')); } @@ -203,11 +209,9 @@ public function testSessionOptions() $this->assertSame('200', ini_get('session.cache_expire')); } - /** - * @expectedException \InvalidArgumentException - */ public function testSetSaveHandlerException() { + $this->expectException('InvalidArgumentException'); $storage = $this->getStorage(); $storage->setSaveHandler(new \stdClass()); } @@ -230,11 +234,9 @@ public function testSetSaveHandler() $this->assertInstanceOf('Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy', $storage->getSaveHandler()); } - /** - * @expectedException \RuntimeException - */ public function testStarted() { + $this->expectException('RuntimeException'); $storage = $this->getStorage(); $this->assertFalse($storage->getSaveHandler()->isActive()); diff --git a/Tests/Session/Storage/PhpBridgeSessionStorageTest.php b/Tests/Session/Storage/PhpBridgeSessionStorageTest.php index 433240024..b546b958e 100644 --- a/Tests/Session/Storage/PhpBridgeSessionStorageTest.php +++ b/Tests/Session/Storage/PhpBridgeSessionStorageTest.php @@ -29,7 +29,7 @@ class PhpBridgeSessionStorageTest extends TestCase { private $savePath; - protected function setUp() + protected function setUp(): void { $this->iniSet('session.save_handler', 'files'); $this->iniSet('session.save_path', $this->savePath = sys_get_temp_dir().'/sftest'); @@ -38,7 +38,7 @@ protected function setUp() } } - protected function tearDown() + protected function tearDown(): void { session_write_close(); array_map('unlink', glob($this->savePath.'/*')); @@ -49,10 +49,7 @@ protected function tearDown() $this->savePath = null; } - /** - * @return PhpBridgeSessionStorage - */ - protected function getStorage() + protected function getStorage(): PhpBridgeSessionStorage { $storage = new PhpBridgeSessionStorage(); $storage->registerBag(new AttributeBag()); @@ -64,12 +61,12 @@ public function testPhpSession() { $storage = $this->getStorage(); - $this->assertNotSame(\PHP_SESSION_ACTIVE, session_status()); + $this->assertNotSame(PHP_SESSION_ACTIVE, session_status()); $this->assertFalse($storage->isStarted()); session_start(); $this->assertTrue(isset($_SESSION)); - $this->assertSame(\PHP_SESSION_ACTIVE, session_status()); + $this->assertSame(PHP_SESSION_ACTIVE, session_status()); // PHP session might have started, but the storage driver has not, so false is correct here $this->assertFalse($storage->isStarted()); @@ -86,10 +83,10 @@ public function testClear() $_SESSION['drak'] = 'loves symfony'; $storage->getBag('attributes')->set('symfony', 'greatness'); $key = $storage->getBag('attributes')->getStorageKey(); - $this->assertEquals($_SESSION[$key], ['symfony' => 'greatness']); - $this->assertEquals($_SESSION['drak'], 'loves symfony'); + $this->assertEquals(['symfony' => 'greatness'], $_SESSION[$key]); + $this->assertEquals('loves symfony', $_SESSION['drak']); $storage->clear(); - $this->assertEquals($_SESSION[$key], []); - $this->assertEquals($_SESSION['drak'], 'loves symfony'); + $this->assertEquals([], $_SESSION[$key]); + $this->assertEquals('loves symfony', $_SESSION['drak']); } } diff --git a/Tests/Session/Storage/Proxy/AbstractProxyTest.php b/Tests/Session/Storage/Proxy/AbstractProxyTest.php index cbb291f19..4820a6593 100644 --- a/Tests/Session/Storage/Proxy/AbstractProxyTest.php +++ b/Tests/Session/Storage/Proxy/AbstractProxyTest.php @@ -27,12 +27,12 @@ class AbstractProxyTest extends TestCase */ protected $proxy; - protected function setUp() + protected function setUp(): void { $this->proxy = $this->getMockForAbstractClass(AbstractProxy::class); } - protected function tearDown() + protected function tearDown(): void { $this->proxy = null; } @@ -80,10 +80,10 @@ public function testName() /** * @runInSeparateProcess * @preserveGlobalState disabled - * @expectedException \LogicException */ public function testNameException() { + $this->expectException('LogicException'); session_start(); $this->proxy->setName('foo'); } @@ -103,10 +103,10 @@ public function testId() /** * @runInSeparateProcess * @preserveGlobalState disabled - * @expectedException \LogicException */ public function testIdException() { + $this->expectException('LogicException'); session_start(); $this->proxy->setId('foo'); } diff --git a/Tests/Session/Storage/Proxy/SessionHandlerProxyTest.php b/Tests/Session/Storage/Proxy/SessionHandlerProxyTest.php index b6e0da99d..81c90433d 100644 --- a/Tests/Session/Storage/Proxy/SessionHandlerProxyTest.php +++ b/Tests/Session/Storage/Proxy/SessionHandlerProxyTest.php @@ -25,7 +25,7 @@ class SessionHandlerProxyTest extends TestCase { /** - * @var \PHPUnit_Framework_MockObject_Matcher + * @var \PHPUnit\Framework\MockObject\Matcher */ private $mock; @@ -34,13 +34,13 @@ class SessionHandlerProxyTest extends TestCase */ private $proxy; - protected function setUp() + protected function setUp(): void { $this->mock = $this->getMockBuilder('SessionHandlerInterface')->getMock(); $this->proxy = new SessionHandlerProxy($this->mock); } - protected function tearDown() + protected function tearDown(): void { $this->mock = null; $this->proxy = null; @@ -127,7 +127,7 @@ public function testGc() */ public function testValidateId() { - $mock = $this->getMockBuilder(['SessionHandlerInterface', 'SessionUpdateTimestampHandlerInterface'])->getMock(); + $mock = $this->getMockBuilder(TestSessionHandler::class)->getMock(); $mock->expects($this->once()) ->method('validateId'); @@ -142,9 +142,10 @@ public function testValidateId() */ public function testUpdateTimestamp() { - $mock = $this->getMockBuilder(['SessionHandlerInterface', 'SessionUpdateTimestampHandlerInterface'])->getMock(); + $mock = $this->getMockBuilder(TestSessionHandler::class)->getMock(); $mock->expects($this->once()) - ->method('updateTimestamp'); + ->method('updateTimestamp') + ->willReturn(false); $proxy = new SessionHandlerProxy($mock); $proxy->updateTimestamp('id', 'data'); @@ -155,3 +156,7 @@ public function testUpdateTimestamp() $this->proxy->updateTimestamp('id', 'data'); } } + +abstract class TestSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface +{ +} diff --git a/Tests/StreamedResponseTest.php b/Tests/StreamedResponseTest.php index 62dfc9bc9..a084e917d 100644 --- a/Tests/StreamedResponseTest.php +++ b/Tests/StreamedResponseTest.php @@ -81,20 +81,16 @@ public function testSendContent() $this->assertEquals(1, $called); } - /** - * @expectedException \LogicException - */ public function testSendContentWithNonCallable() { + $this->expectException('LogicException'); $response = new StreamedResponse(null); $response->sendContent(); } - /** - * @expectedException \LogicException - */ public function testSetContent() { + $this->expectException('LogicException'); $response = new StreamedResponse(function () { echo 'foo'; }); $response->setContent('foo'); } diff --git a/Tests/Test/Constraint/ResponseIsRedirectedTest.php b/Tests/Test/Constraint/ResponseIsRedirectedTest.php index a3a460636..a8314c259 100644 --- a/Tests/Test/Constraint/ResponseIsRedirectedTest.php +++ b/Tests/Test/Constraint/ResponseIsRedirectedTest.php @@ -29,7 +29,7 @@ public function testConstraint(): void try { $constraint->evaluate(new Response()); } catch (ExpectationFailedException $e) { - $this->assertContains("Failed asserting that the Response is redirected.\nHTTP/1.0 200 OK", TestFailure::exceptionToString($e)); + $this->assertStringContainsString("Failed asserting that the Response is redirected.\nHTTP/1.0 200 OK", TestFailure::exceptionToString($e)); return; } diff --git a/Tests/Test/Constraint/ResponseIsSuccessfulTest.php b/Tests/Test/Constraint/ResponseIsSuccessfulTest.php index 0c99a5e48..b59daf8a3 100644 --- a/Tests/Test/Constraint/ResponseIsSuccessfulTest.php +++ b/Tests/Test/Constraint/ResponseIsSuccessfulTest.php @@ -29,7 +29,7 @@ public function testConstraint(): void try { $constraint->evaluate(new Response('', 404)); } catch (ExpectationFailedException $e) { - $this->assertContains("Failed asserting that the Response is successful.\nHTTP/1.0 404 Not Found", TestFailure::exceptionToString($e)); + $this->assertStringContainsString("Failed asserting that the Response is successful.\nHTTP/1.0 404 Not Found", TestFailure::exceptionToString($e)); return; } diff --git a/Tests/Test/Constraint/ResponseStatusCodeSameTest.php b/Tests/Test/Constraint/ResponseStatusCodeSameTest.php index 3e15e9067..53200fdd0 100644 --- a/Tests/Test/Constraint/ResponseStatusCodeSameTest.php +++ b/Tests/Test/Constraint/ResponseStatusCodeSameTest.php @@ -31,7 +31,7 @@ public function testConstraint(): void try { $constraint->evaluate(new Response('', 404)); } catch (ExpectationFailedException $e) { - $this->assertContains("Failed asserting that the Response status code is 200.\nHTTP/1.0 404 Not Found", TestFailure::exceptionToString($e)); + $this->assertStringContainsString("Failed asserting that the Response status code is 200.\nHTTP/1.0 404 Not Found", TestFailure::exceptionToString($e)); return; } diff --git a/composer.json b/composer.json index f30975114..a8c7c24c4 100644 --- a/composer.json +++ b/composer.json @@ -16,13 +16,13 @@ } ], "require": { - "php": "^7.1.3", - "symfony/mime": "^4.3", + "php": ">=7.1.3", + "symfony/mime": "^4.3|^5.0", "symfony/polyfill-mbstring": "~1.1" }, "require-dev": { "predis/predis": "~1.0", - "symfony/expression-language": "~3.4|~4.0" + "symfony/expression-language": "^3.4|^4.0|^5.0" }, "autoload": { "psr-4": { "Symfony\\Component\\HttpFoundation\\": "" }, @@ -33,7 +33,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.3-dev" + "dev-master": "4.4-dev" } } }