diff --git a/AcceptHeader.php b/AcceptHeader.php index 2aa91dc44..968b71f5d 100644 --- a/AcceptHeader.php +++ b/AcceptHeader.php @@ -24,7 +24,7 @@ class AcceptHeader /** * @var AcceptHeaderItem[] */ - private $items = array(); + private $items = []; /** * @var bool @@ -32,8 +32,6 @@ class AcceptHeader private $sorted = true; /** - * Constructor. - * * @param AcceptHeaderItem[] $items */ public function __construct(array $items) @@ -99,8 +97,6 @@ public function get($value) /** * Adds an item. * - * @param AcceptHeaderItem $item - * * @return $this */ public function add(AcceptHeaderItem $item) @@ -155,7 +151,7 @@ public function first() private function sort() { if (!$this->sorted) { - uasort($this->items, function ($a, $b) { + uasort($this->items, function (AcceptHeaderItem $a, AcceptHeaderItem $b) { $qA = $a->getQuality(); $qB = $b->getQuality(); diff --git a/AcceptHeaderItem.php b/AcceptHeaderItem.php index fb54b4935..96bb0c443 100644 --- a/AcceptHeaderItem.php +++ b/AcceptHeaderItem.php @@ -18,33 +18,15 @@ */ class AcceptHeaderItem { - /** - * @var string - */ private $value; - - /** - * @var float - */ private $quality = 1.0; - - /** - * @var int - */ private $index = 0; + private $attributes = []; /** - * @var array - */ - private $attributes = array(); - - /** - * Constructor. - * * @param string $value - * @param array $attributes */ - public function __construct($value, array $attributes = array()) + public function __construct($value, array $attributes = []) { $this->value = $value; foreach ($attributes as $name => $value) { @@ -63,33 +45,33 @@ public static function fromString($itemValue) { $bits = preg_split('/\s*(?:;*("[^"]+");*|;*(\'[^\']+\');*|;+)\s*/', $itemValue, 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); $value = array_shift($bits); - $attributes = array(); + $attributes = []; $lastNullAttribute = null; foreach ($bits as $bit) { - if (($start = substr($bit, 0, 1)) === ($end = substr($bit, -1)) && ($start === '"' || $start === '\'')) { + if (($start = substr($bit, 0, 1)) === ($end = substr($bit, -1)) && ('"' === $start || '\'' === $start)) { $attributes[$lastNullAttribute] = substr($bit, 1, -1); } elseif ('=' === $end) { $lastNullAttribute = $bit = substr($bit, 0, -1); $attributes[$bit] = null; } else { $parts = explode('=', $bit); - $attributes[$parts[0]] = isset($parts[1]) && strlen($parts[1]) > 0 ? $parts[1] : ''; + $attributes[$parts[0]] = isset($parts[1]) && \strlen($parts[1]) > 0 ? $parts[1] : ''; } } - return new self(($start = substr($value, 0, 1)) === ($end = substr($value, -1)) && ($start === '"' || $start === '\'') ? substr($value, 1, -1) : $value, $attributes); + return new self(($start = substr($value, 0, 1)) === ($end = substr($value, -1)) && ('"' === $start || '\'' === $start) ? substr($value, 1, -1) : $value, $attributes); } /** - * Returns header value's string representation. + * Returns header value's string representation. * * @return string */ public function __toString() { $string = $this->value.($this->quality < 1 ? ';q='.$this->quality : ''); - if (count($this->attributes) > 0) { + if (\count($this->attributes) > 0) { $string .= ';'.implode(';', array_map(function ($name, $value) { return sprintf(preg_match('/[,;=]/', $value) ? '%s="%s"' : '%s=%s', $name, $value); }, array_keys($this->attributes), $this->attributes)); diff --git a/ApacheRequest.php b/ApacheRequest.php index 84803ebae..4e99186dc 100644 --- a/ApacheRequest.php +++ b/ApacheRequest.php @@ -35,7 +35,7 @@ protected function prepareBaseUrl() if (false === strpos($this->server->get('REQUEST_URI'), $baseUrl)) { // assume mod_rewrite - return rtrim(dirname($baseUrl), '/\\'); + return rtrim(\dirname($baseUrl), '/\\'); } return $baseUrl; diff --git a/BinaryFileResponse.php b/BinaryFileResponse.php index dcf5dcfee..ea7ac8469 100644 --- a/BinaryFileResponse.php +++ b/BinaryFileResponse.php @@ -11,8 +11,8 @@ namespace Symfony\Component\HttpFoundation; -use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\HttpFoundation\File\Exception\FileException; +use Symfony\Component\HttpFoundation\File\File; /** * BinaryFileResponse represents an HTTP response delivering a file. @@ -31,22 +31,20 @@ class BinaryFileResponse extends Response * @var File */ protected $file; - protected $offset; - protected $maxlen; + protected $offset = 0; + protected $maxlen = -1; protected $deleteFileAfterSend = false; /** - * Constructor. - * * @param \SplFileInfo|string $file The file to stream * @param int $status The response status code * @param array $headers An array of response headers * @param bool $public Files are public by default - * @param null|string $contentDisposition The type of Content-Disposition to set automatically with the filename + * @param string|null $contentDisposition The type of Content-Disposition to set automatically with the filename * @param bool $autoEtag Whether the ETag header should be automatically set * @param bool $autoLastModified Whether the Last-Modified header should be automatically set */ - public function __construct($file, $status = 200, $headers = array(), $public = true, $contentDisposition = null, $autoEtag = false, $autoLastModified = true) + public function __construct($file, $status = 200, $headers = [], $public = true, $contentDisposition = null, $autoEtag = false, $autoLastModified = true) { parent::__construct(null, $status, $headers); @@ -62,13 +60,13 @@ public function __construct($file, $status = 200, $headers = array(), $public = * @param int $status The response status code * @param array $headers An array of response headers * @param bool $public Files are public by default - * @param null|string $contentDisposition The type of Content-Disposition to set automatically with the filename + * @param string|null $contentDisposition The type of Content-Disposition to set automatically with the filename * @param bool $autoEtag Whether the ETag header should be automatically set * @param bool $autoLastModified Whether the Last-Modified header should be automatically set * * @return static */ - public static function create($file = null, $status = 200, $headers = array(), $public = true, $contentDisposition = null, $autoEtag = false, $autoLastModified = true) + public static function create($file = null, $status = 200, $headers = [], $public = true, $contentDisposition = null, $autoEtag = false, $autoLastModified = true) { return new static($file, $status, $headers, $public, $contentDisposition, $autoEtag, $autoLastModified); } @@ -141,7 +139,7 @@ public function setAutoLastModified() */ public function setAutoEtag() { - $this->setEtag(sha1_file($this->file->getPathname())); + $this->setEtag(base64_encode(hash_file('sha256', $this->file->getPathname(), true))); return $this; } @@ -157,7 +155,7 @@ public function setAutoEtag() */ public function setContentDisposition($disposition, $filename = '', $filenameFallback = '') { - if ($filename === '') { + if ('' === $filename) { $filename = $this->file->getFilename(); } @@ -167,7 +165,7 @@ public function setContentDisposition($disposition, $filename = '', $filenameFal for ($i = 0, $filenameLength = mb_strlen($filename, $encoding); $i < $filenameLength; ++$i) { $char = mb_substr($filename, $i, 1, $encoding); - if ('%' === $char || ord($char) < 32 || ord($char) > 126) { + if ('%' === $char || \ord($char) < 32 || \ord($char) > 126) { $filenameFallback .= '_'; } else { $filenameFallback .= $char; @@ -217,31 +215,36 @@ public function prepare(Request $request) if (false === $path) { $path = $this->file->getPathname(); } - if (strtolower($type) === 'x-accel-redirect') { + if ('x-accel-redirect' === strtolower($type)) { // Do X-Accel-Mapping substitutions. // @link http://wiki.nginx.org/X-accel#X-Accel-Redirect foreach (explode(',', $request->headers->get('X-Accel-Mapping', '')) as $mapping) { $mapping = explode('=', $mapping, 2); - if (2 === count($mapping)) { + if (2 === \count($mapping)) { $pathPrefix = trim($mapping[0]); $location = trim($mapping[1]); - if (substr($path, 0, strlen($pathPrefix)) === $pathPrefix) { - $path = $location.substr($path, strlen($pathPrefix)); + if (substr($path, 0, \strlen($pathPrefix)) === $pathPrefix) { + $path = $location.substr($path, \strlen($pathPrefix)); + // Only set X-Accel-Redirect header if a valid URI can be produced + // as nginx does not serve arbitrary file paths. + $this->headers->set($type, $path); + $this->maxlen = 0; break; } } } + } else { + $this->headers->set($type, $path); + $this->maxlen = 0; } - $this->headers->set($type, $path); - $this->maxlen = 0; } elseif ($request->headers->has('Range')) { // Process the range headers. if (!$request->headers->has('If-Range') || $this->hasValidIfRangeHeader($request->headers->get('If-Range'))) { $range = $request->headers->get('Range'); - list($start, $end) = explode('-', substr($range, 6), 2) + array(0); + list($start, $end) = explode('-', substr($range, 6), 2) + [0]; $end = ('' === $end) ? $fileSize - 1 : (int) $end; @@ -256,7 +259,7 @@ public function prepare(Request $request) if ($start < 0 || $end > $fileSize - 1) { $this->setStatusCode(416); $this->headers->set('Content-Range', sprintf('bytes */%s', $fileSize)); - } elseif ($start !== 0 || $end !== $fileSize - 1) { + } elseif (0 !== $start || $end !== $fileSize - 1) { $this->maxlen = $end < $fileSize ? $end - $start + 1 : -1; $this->offset = $start; @@ -307,7 +310,7 @@ public function sendContent() fclose($out); fclose($file); - if ($this->deleteFileAfterSend) { + if ($this->deleteFileAfterSend && file_exists($this->file->getPathname())) { unlink($this->file->getPathname()); } @@ -324,12 +327,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() { @@ -345,7 +348,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 e1fdf77b9..c0d890167 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,27 @@ CHANGELOG ========= +3.4.14 +------ + + * [BC BREAK] Support for the IIS-only `X_ORIGINAL_URL` and `X_REWRITE_URL` + HTTP headers has been dropped for security reasons. + +3.4.0 +----- + + * implemented PHP 7.0's `SessionUpdateTimestampHandlerInterface` with a new + `AbstractSessionHandler` base class and a new `StrictSessionHandler` wrapper + * deprecated the `WriteCheckSessionHandler`, `NativeSessionHandler` and `NativeProxy` classes + * deprecated setting session save handlers that do not implement `\SessionHandlerInterface` in `NativeSessionStorage::setSaveHandler()` + * deprecated using `MongoDbSessionHandler` with the legacy mongo extension; use it with the mongodb/mongodb package and ext-mongodb instead + * deprecated `MemcacheSessionHandler`; use `MemcachedSessionHandler` instead + 3.3.0 ----- * the `Request::setTrustedProxies()` method takes a new `$trustedHeaderSet` argument, - see http://symfony.com/doc/current/components/http_foundation/trusting_proxies.html for more info, + see https://symfony.com/doc/current/deployment/proxies.html for more info, * deprecated the `Request::setTrustedHeaderName()` and `Request::getTrustedHeaderName()` methods, * added `File\Stream`, to be passed to `BinaryFileResponse` when the size of the served file is unknown, disabling `Range` and `Content-Length` handling, switching to chunked encoding instead @@ -128,10 +144,10 @@ CHANGELOG * Added `FlashBag`. Flashes expire when retrieved by `get()` or `all()`. This implementation is ESI compatible. * Added `AutoExpireFlashBag` (default) to replicate Symfony 2.0.x auto expire - behaviour of messages auto expiring after one page page load. Messages must + behavior of messages auto expiring after one page page load. Messages must be retrieved by `get()` or `all()`. * Added `Symfony\Component\HttpFoundation\Attribute\AttributeBag` to replicate - attributes storage behaviour from 2.0.x (default). + attributes storage behavior from 2.0.x (default). * Added `Symfony\Component\HttpFoundation\Attribute\NamespacedAttributeBag` for namespace session attributes. * Flash API can stores messages in an array so there may be multiple messages diff --git a/Cookie.php b/Cookie.php index a2139ff6b..98a5ef00a 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,11 +29,13 @@ class Cookie protected $path; protected $secure; protected $httpOnly; + private $raw; private $sameSite; - 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. @@ -41,7 +47,7 @@ class Cookie */ public static function fromString($cookie, $decode = false) { - $data = array( + $data = [ 'expires' => 0, 'path' => '/', 'domain' => null, @@ -49,7 +55,7 @@ public static function fromString($cookie, $decode = false) 'httponly' => false, 'raw' => !$decode, 'samesite' => null, - ); + ]; foreach (explode(';', $cookie) as $part) { if (false === strpos($part, '=')) { $key = trim($part); @@ -81,8 +87,6 @@ public static function fromString($cookie, $decode = false) } /** - * Constructor. - * * @param string $name The name of the cookie * @param string|null $value The value of the cookie * @param int|string|\DateTimeInterface $expire The time the cookie expires @@ -98,7 +102,7 @@ public static function fromString($cookie, $decode = false) public function __construct($name, $value = null, $expire = 0, $path = '/', $domain = null, $secure = false, $httpOnly = true, $raw = false, $sameSite = null) { // 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)); } @@ -130,7 +134,7 @@ public function __construct($name, $value = null, $expire = 0, $path = '/', $dom $sameSite = strtolower($sameSite); } - if (!in_array($sameSite, array(self::SAMESITE_LAX, self::SAMESITE_STRICT, null), true)) { + if (!\in_array($sameSite, [self::SAMESITE_LAX, self::SAMESITE_STRICT, self::SAMESITE_NONE, null], true)) { throw new \InvalidArgumentException('The "sameSite" parameter value is not valid.'); } @@ -144,15 +148,21 @@ public function __construct($name, $value = null, $expire = 0, $path = '/', $dom */ 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=-31536001'; + $str .= 'deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; Max-Age=0'; } else { $str .= $this->isRaw() ? $this->getValue() : rawurlencode($this->getValue()); if (0 !== $this->getExpiresTime()) { - $str .= '; expires='.gmdate('D, d-M-Y H:i:s T', $this->getExpiresTime()).'; max-age='.$this->getMaxAge(); + $str .= '; expires='.gmdate('D, d-M-Y H:i:s T', $this->getExpiresTime()).'; Max-Age='.$this->getMaxAge(); } } @@ -226,7 +236,9 @@ public function getExpiresTime() */ public function getMaxAge() { - return 0 !== $this->expire ? $this->expire - time() : 0; + $maxAge = $this->expire - time(); + + return 0 >= $maxAge ? 0 : $maxAge; } /** @@ -266,7 +278,7 @@ public function isHttpOnly() */ public function isCleared() { - return $this->expire < time(); + return 0 !== $this->expire && $this->expire < time(); } /** diff --git a/ExpressionRequestMatcher.php b/ExpressionRequestMatcher.php index e9c8441ce..26bed7d37 100644 --- a/ExpressionRequestMatcher.php +++ b/ExpressionRequestMatcher.php @@ -35,13 +35,13 @@ public function matches(Request $request) throw new \LogicException('Unable to match the request as the expression language is not available.'); } - return $this->language->evaluate($this->expression, array( + return $this->language->evaluate($this->expression, [ 'request' => $request, 'method' => $request->getMethod(), 'path' => rawurldecode($request->getPathInfo()), 'host' => $request->getHost(), 'ip' => $request->getClientIp(), 'attributes' => $request->attributes->all(), - )) && parent::matches($request); + ]) && parent::matches($request); } } diff --git a/File/Exception/AccessDeniedException.php b/File/Exception/AccessDeniedException.php index 41f7a4625..3b8e41d4a 100644 --- a/File/Exception/AccessDeniedException.php +++ b/File/Exception/AccessDeniedException.php @@ -19,8 +19,6 @@ class AccessDeniedException extends FileException { /** - * Constructor. - * * @param string $path The path to the accessed file */ public function __construct($path) diff --git a/File/Exception/FileNotFoundException.php b/File/Exception/FileNotFoundException.php index ac90d4035..bfcc37ec6 100644 --- a/File/Exception/FileNotFoundException.php +++ b/File/Exception/FileNotFoundException.php @@ -19,8 +19,6 @@ class FileNotFoundException extends FileException { /** - * Constructor. - * * @param string $path The path to the file that was not found */ public function __construct($path) diff --git a/File/Exception/UnexpectedTypeException.php b/File/Exception/UnexpectedTypeException.php index 0444b8778..62005d3b6 100644 --- a/File/Exception/UnexpectedTypeException.php +++ b/File/Exception/UnexpectedTypeException.php @@ -15,6 +15,6 @@ class UnexpectedTypeException extends FileException { public function __construct($value, $expectedType) { - parent::__construct(sprintf('Expected argument of type %s, %s given', $expectedType, is_object($value) ? get_class($value) : gettype($value))); + parent::__construct(sprintf('Expected argument of type %s, %s given', $expectedType, \is_object($value) ? \get_class($value) : \gettype($value))); } } diff --git a/File/File.php b/File/File.php index e2a67684f..34220588a 100644 --- a/File/File.php +++ b/File/File.php @@ -13,8 +13,8 @@ use Symfony\Component\HttpFoundation\File\Exception\FileException; use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; -use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesser; use Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser; +use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesser; /** * A file in the file system. @@ -93,9 +93,11 @@ public function move($directory, $name = null) { $target = $this->getTargetFile($directory, $name); - if (!@rename($this->getPathname(), $target)) { - $error = error_get_last(); - throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $this->getPathname(), $target, strip_tags($error['message']))); + set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; }); + $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))); } @chmod($target, 0666 & ~umask()); @@ -113,7 +115,7 @@ protected function getTargetFile($directory, $name = null) throw new FileException(sprintf('Unable to write in the "%s" directory', $directory)); } - $target = rtrim($directory, '/\\').DIRECTORY_SEPARATOR.(null === $name ? $this->getBasename() : $this->getName($name)); + $target = rtrim($directory, '/\\').\DIRECTORY_SEPARATOR.(null === $name ? $this->getBasename() : $this->getName($name)); return new self($target, false); } diff --git a/File/MimeType/ExtensionGuesser.php b/File/MimeType/ExtensionGuesser.php index 921751f6b..f9393df90 100644 --- a/File/MimeType/ExtensionGuesser.php +++ b/File/MimeType/ExtensionGuesser.php @@ -37,7 +37,7 @@ class ExtensionGuesser implements ExtensionGuesserInterface * * @var array */ - protected $guessers = array(); + protected $guessers = []; /** * Returns the singleton instance. @@ -65,8 +65,6 @@ private function __construct() * Registers a new extension guesser. * * When guessing, this guesser is preferred over previously registered ones. - * - * @param ExtensionGuesserInterface $guesser */ public function register(ExtensionGuesserInterface $guesser) { @@ -92,5 +90,7 @@ public function guess($mimeType) return $extension; } } + + return null; } } diff --git a/File/MimeType/FileBinaryMimeTypeGuesser.php b/File/MimeType/FileBinaryMimeTypeGuesser.php index f917a06d6..7045e94df 100644 --- a/File/MimeType/FileBinaryMimeTypeGuesser.php +++ b/File/MimeType/FileBinaryMimeTypeGuesser.php @@ -11,8 +11,8 @@ namespace Symfony\Component\HttpFoundation\File\MimeType; -use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException; +use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; /** * Guesses the mime type with the binary "file" (only available on *nix). @@ -24,8 +24,6 @@ class FileBinaryMimeTypeGuesser implements MimeTypeGuesserInterface private $cmd; /** - * Constructor. - * * The $cmd pattern must contain a "%s" string that will be replaced * with the file name to guess. * @@ -33,7 +31,7 @@ class FileBinaryMimeTypeGuesser implements MimeTypeGuesserInterface * * @param string $cmd The command to run to get the mime type of a file */ - public function __construct($cmd = 'file -b --mime %s 2>/dev/null') + public function __construct($cmd = 'file -b --mime -- %s 2>/dev/null') { $this->cmd = $cmd; } @@ -45,7 +43,21 @@ public function __construct($cmd = 'file -b --mime %s 2>/dev/null') */ public static function isSupported() { - return '\\' !== DIRECTORY_SEPARATOR && function_exists('passthru') && function_exists('escapeshellarg'); + static $supported = null; + + if (null !== $supported) { + return $supported; + } + + if ('\\' === \DIRECTORY_SEPARATOR || !\function_exists('passthru') || !\function_exists('escapeshellarg')) { + return $supported = false; + } + + ob_start(); + passthru('command -v file', $exitStatus); + $binPath = trim(ob_get_clean()); + + return $supported = 0 === $exitStatus && '' !== $binPath; } /** @@ -62,24 +74,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 6fee94798..fc4bc4502 100644 --- a/File/MimeType/FileinfoMimeTypeGuesser.php +++ b/File/MimeType/FileinfoMimeTypeGuesser.php @@ -11,8 +11,8 @@ namespace Symfony\Component\HttpFoundation\File\MimeType; -use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException; +use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; /** * Guesses the mime type using the PECL extension FileInfo. @@ -24,11 +24,9 @@ class FileinfoMimeTypeGuesser implements MimeTypeGuesserInterface private $magicFile; /** - * Constructor. - * * @param string $magicFile A magic file to use with the finfo instance * - * @see http://www.php.net/manual/en/function.finfo-open.php + * @see https://php.net/finfo-open */ public function __construct($magicFile = null) { @@ -42,7 +40,7 @@ public function __construct($magicFile = null) */ public static function isSupported() { - return function_exists('finfo_open'); + return \function_exists('finfo_open'); } /** @@ -59,11 +57,11 @@ public function guess($path) } if (!self::isSupported()) { - return; + return null; } if (!$finfo = new \finfo(FILEINFO_MIME_TYPE, $this->magicFile)) { - return; + return null; } return $finfo->file($path); diff --git a/File/MimeType/MimeTypeExtensionGuesser.php b/File/MimeType/MimeTypeExtensionGuesser.php index e327f834f..c0f9140c8 100644 --- a/File/MimeType/MimeTypeExtensionGuesser.php +++ b/File/MimeType/MimeTypeExtensionGuesser.php @@ -20,13 +20,11 @@ class MimeTypeExtensionGuesser implements ExtensionGuesserInterface * A map of mime types and their default extensions. * * This list has been placed under the public domain by the Apache HTTPD project. - * This list has been updated from upstream on 2013-04-23. + * This list has been updated from upstream on 2019-01-14. * - * @see http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types - * - * @var array + * @see https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types */ - protected $defaultExtensions = array( + protected $defaultExtensions = [ 'application/andrew-inset' => 'ez', 'application/applixware' => 'aw', 'application/atom+xml' => 'atom', @@ -601,6 +599,7 @@ class MimeTypeExtensionGuesser implements ExtensionGuesserInterface 'application/x-xliff+xml' => 'xlf', 'application/x-xpinstall' => 'xpi', 'application/x-xz' => 'xz', + 'application/x-zip-compressed' => 'zip', 'application/x-zmachine' => 'z1', 'application/xaml+xml' => 'xaml', 'application/xcap-diff+xml' => 'xdf', @@ -619,7 +618,7 @@ class MimeTypeExtensionGuesser implements ExtensionGuesserInterface 'audio/adpcm' => 'adp', 'audio/basic' => 'au', 'audio/midi' => 'mid', - 'audio/mp4' => 'mp4a', + 'audio/mp4' => 'm4a', 'audio/mpeg' => 'mpga', 'audio/ogg' => 'oga', 'audio/s3m' => 's3m', @@ -654,6 +653,11 @@ class MimeTypeExtensionGuesser implements ExtensionGuesserInterface 'chemical/x-cml' => 'cml', 'chemical/x-csml' => 'csml', 'chemical/x-xyz' => 'xyz', + 'font/collection' => 'ttc', + 'font/otf' => 'otf', + 'font/ttf' => 'ttf', + 'font/woff' => 'woff', + 'font/woff2' => 'woff2', 'image/bmp' => 'bmp', 'image/x-ms-bmp' => 'bmp', 'image/cgm' => 'cgm', @@ -670,8 +674,8 @@ class MimeTypeExtensionGuesser implements ExtensionGuesserInterface 'image/tiff' => 'tiff', 'image/vnd.adobe.photoshop' => 'psd', 'image/vnd.dece.graphic' => 'uvi', - 'image/vnd.dvb.subtitle' => 'sub', 'image/vnd.djvu' => 'djvu', + 'image/vnd.dvb.subtitle' => 'sub', 'image/vnd.dwg' => 'dwg', 'image/vnd.dxf' => 'dxf', 'image/vnd.fastbidsheet' => 'fbs', @@ -733,8 +737,8 @@ class MimeTypeExtensionGuesser implements ExtensionGuesserInterface 'text/vcard' => 'vcard', 'text/vnd.curl' => 'curl', 'text/vnd.curl.dcurl' => 'dcurl', - 'text/vnd.curl.scurl' => 'scurl', 'text/vnd.curl.mcurl' => 'mcurl', + 'text/vnd.curl.scurl' => 'scurl', 'text/vnd.dvb.subtitle' => 'sub', 'text/vnd.fly' => 'fly', 'text/vnd.fmi.flexstor' => 'flx', @@ -748,10 +752,10 @@ class MimeTypeExtensionGuesser implements ExtensionGuesserInterface 'text/x-asm' => 's', 'text/x-c' => 'c', 'text/x-fortran' => 'f', - 'text/x-pascal' => 'p', 'text/x-java-source' => 'java', - 'text/x-opml' => 'opml', 'text/x-nfo' => 'nfo', + 'text/x-opml' => 'opml', + 'text/x-pascal' => 'p', 'text/x-setext' => 'etx', 'text/x-sfv' => 'sfv', 'text/x-uuencode' => 'uu', @@ -797,13 +801,19 @@ class MimeTypeExtensionGuesser implements ExtensionGuesserInterface 'video/x-sgi-movie' => 'movie', 'video/x-smv' => 'smv', 'x-conference/x-cooltalk' => 'ice', - ); + ]; /** * {@inheritdoc} */ public function guess($mimeType) { - return isset($this->defaultExtensions[$mimeType]) ? $this->defaultExtensions[$mimeType] : null; + if (isset($this->defaultExtensions[$mimeType])) { + return $this->defaultExtensions[$mimeType]; + } + + $lcMimeType = strtolower($mimeType); + + return isset($this->defaultExtensions[$lcMimeType]) ? $this->defaultExtensions[$lcMimeType] : null; } } diff --git a/File/MimeType/MimeTypeGuesser.php b/File/MimeType/MimeTypeGuesser.php index 69c803b49..e05269fc8 100644 --- a/File/MimeType/MimeTypeGuesser.php +++ b/File/MimeType/MimeTypeGuesser.php @@ -11,8 +11,8 @@ namespace Symfony\Component\HttpFoundation\File\MimeType; -use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException; +use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; /** * A singleton mime type guesser. @@ -51,7 +51,7 @@ class MimeTypeGuesser implements MimeTypeGuesserInterface * * @var array */ - protected $guessers = array(); + protected $guessers = []; /** * Returns the singleton instance. @@ -80,21 +80,14 @@ public static function reset() */ private function __construct() { - if (FileBinaryMimeTypeGuesser::isSupported()) { - $this->register(new FileBinaryMimeTypeGuesser()); - } - - if (FileinfoMimeTypeGuesser::isSupported()) { - $this->register(new FileinfoMimeTypeGuesser()); - } + $this->register(new FileBinaryMimeTypeGuesser()); + $this->register(new FileinfoMimeTypeGuesser()); } /** * Registers a new mime type guesser. * * When guessing, this guesser is preferred over previously registered ones. - * - * @param MimeTypeGuesserInterface $guesser */ public function register(MimeTypeGuesserInterface $guesser) { @@ -127,18 +120,16 @@ public function guess($path) throw new AccessDeniedException($path); } - if (!$this->guessers) { - $msg = 'Unable to guess the mime type as no guessers are available'; - if (!FileinfoMimeTypeGuesser::isSupported()) { - $msg .= ' (Did you enable the php_fileinfo extension?)'; - } - throw new \LogicException($msg); - } - foreach ($this->guessers as $guesser) { if (null !== $mimeType = $guesser->guess($path)) { return $mimeType; } } + + 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?)'); + } + + return null; } } diff --git a/File/MimeType/MimeTypeGuesserInterface.php b/File/MimeType/MimeTypeGuesserInterface.php index f8c3ad228..e46e78eef 100644 --- a/File/MimeType/MimeTypeGuesserInterface.php +++ b/File/MimeType/MimeTypeGuesserInterface.php @@ -11,8 +11,8 @@ namespace Symfony\Component\HttpFoundation\File\MimeType; -use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException; +use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; /** * Guesses the mime type of a file. @@ -26,7 +26,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 10837726c..86153ed49 100644 --- a/File/UploadedFile.php +++ b/File/UploadedFile.php @@ -24,41 +24,10 @@ */ class UploadedFile extends File { - /** - * Whether the test mode is activated. - * - * Local files are used in test mode hence the code should not enforce HTTP uploads. - * - * @var bool - */ private $test = false; - - /** - * The original name of the uploaded file. - * - * @var string - */ private $originalName; - - /** - * The mime type provided by the uploader. - * - * @var string - */ private $mimeType; - - /** - * The file size provided by the uploader. - * - * @var int|null - */ private $size; - - /** - * The UPLOAD_ERR_XXX constant provided by the uploader. - * - * @var int - */ private $error; /** @@ -76,11 +45,12 @@ class UploadedFile extends File * Calling any other method on an non-valid instance will cause an unpredictable result. * * @param string $path The full temporary path to the file - * @param string $originalName The original file name + * @param string $originalName The original file name of the uploaded file * @param string|null $mimeType The type of the file as provided by PHP; null defaults to application/octet-stream - * @param int|null $size The file size + * @param int|null $size The file size provided by the uploader * @param int|null $error The error constant of the upload (one of PHP's UPLOAD_ERR_XXX constants); null defaults to UPLOAD_ERR_OK * @param bool $test Whether the test mode is active + * Local files are used in test mode hence the code should not enforce HTTP uploads * * @throws FileException If file_uploads is disabled * @throws FileNotFoundException If the file does not exist @@ -198,7 +168,7 @@ public function getError() */ public function isValid() { - $isOk = $this->error === UPLOAD_ERR_OK; + $isOk = UPLOAD_ERR_OK === $this->error; return $this->test ? $isOk : $isOk && is_uploaded_file($this->getPathname()); } @@ -222,9 +192,11 @@ public function move($directory, $name = null) $target = $this->getTargetFile($directory, $name); - if (!@move_uploaded_file($this->getPathname(), $target)) { - $error = error_get_last(); - throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $this->getPathname(), $target, strip_tags($error['message']))); + set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; }); + $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))); } @chmod($target, 0666 & ~umask()); @@ -242,25 +214,41 @@ 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. + * + * @return int The given size in bytes + */ + private static function parseFilesize($size) + { + if ('' === $size) { + return 0; } - $max = ltrim($iniMax, '+'); + $size = strtolower($size); + + $max = ltrim($size, '+'); if (0 === strpos($max, '0x')) { - $max = intval($max, 16); + $max = \intval($max, 16); } elseif (0 === strpos($max, '0')) { - $max = intval($max, 8); + $max = \intval($max, 8); } else { $max = (int) $max; } - switch (substr($iniMax, -1)) { + switch (substr($size, -1)) { case 't': $max *= 1024; + // no break case 'g': $max *= 1024; + // no break case 'm': $max *= 1024; + // no break case 'k': $max *= 1024; } @@ -274,7 +262,7 @@ public static function getMaxFilesize() */ public function getErrorMessage() { - static $errors = array( + static $errors = [ UPLOAD_ERR_INI_SIZE => 'The file "%s" exceeds your upload_max_filesize ini directive (limit is %d KiB).', UPLOAD_ERR_FORM_SIZE => 'The file "%s" exceeds the upload limit defined in your form.', UPLOAD_ERR_PARTIAL => 'The file "%s" was only partially uploaded.', @@ -282,10 +270,10 @@ public function getErrorMessage() UPLOAD_ERR_CANT_WRITE => 'The file "%s" could not be written on disk.', UPLOAD_ERR_NO_TMP_DIR => 'File could not be uploaded: missing temporary directory.', UPLOAD_ERR_EXTENSION => 'File upload was stopped by a PHP extension.', - ); + ]; $errorCode = $this->error; - $maxFilesize = $errorCode === UPLOAD_ERR_INI_SIZE ? self::getMaxFilesize() / 1024 : 0; + $maxFilesize = UPLOAD_ERR_INI_SIZE === $errorCode ? self::getMaxFilesize() / 1024 : 0; $message = isset($errors[$errorCode]) ? $errors[$errorCode] : 'The file "%s" was not uploaded due to an unknown error.'; return sprintf($message, $this->getClientOriginalName(), $maxFilesize); diff --git a/FileBag.php b/FileBag.php index e17a9057b..024fadf20 100644 --- a/FileBag.php +++ b/FileBag.php @@ -21,14 +21,12 @@ */ class FileBag extends ParameterBag { - private static $fileKeys = array('error', 'name', 'size', 'tmp_name', 'type'); + private static $fileKeys = ['error', 'name', 'size', 'tmp_name', 'type']; /** - * Constructor. - * * @param array $parameters An array of HTTP files */ - public function __construct(array $parameters = array()) + public function __construct(array $parameters = []) { $this->replace($parameters); } @@ -36,9 +34,9 @@ public function __construct(array $parameters = array()) /** * {@inheritdoc} */ - public function replace(array $files = array()) + public function replace(array $files = []) { - $this->parameters = array(); + $this->parameters = []; $this->add($files); } @@ -47,7 +45,7 @@ public function replace(array $files = array()) */ public function set($key, $value) { - if (!is_array($value) && !$value instanceof UploadedFile) { + if (!\is_array($value) && !$value instanceof UploadedFile) { throw new \InvalidArgumentException('An uploaded file must be an array or an instance of UploadedFile.'); } @@ -57,7 +55,7 @@ public function set($key, $value) /** * {@inheritdoc} */ - public function add(array $files = array()) + public function add(array $files = []) { foreach ($files as $key => $file) { $this->set($key, $file); @@ -69,7 +67,7 @@ public function add(array $files = array()) * * @param array|UploadedFile $file A (multi-dimensional) array of uploaded file information * - * @return UploadedFile|UploadedFile[] A (multi-dimensional) array of UploadedFile instances + * @return UploadedFile[]|UploadedFile|null A (multi-dimensional) array of UploadedFile instances */ protected function convertFileInformation($file) { @@ -77,8 +75,8 @@ protected function convertFileInformation($file) return $file; } - $file = $this->fixPhpFilesArray($file); - if (is_array($file)) { + if (\is_array($file)) { + $file = $this->fixPhpFilesArray($file); $keys = array_keys($file); sort($keys); @@ -89,7 +87,10 @@ protected function convertFileInformation($file) $file = new UploadedFile($file['tmp_name'], $file['name'], $file['type'], $file['size'], $file['error']); } } else { - $file = array_map(array($this, 'convertFileInformation'), $file); + $file = array_map([$this, 'convertFileInformation'], $file); + if (array_keys($keys) === $keys) { + $file = array_filter($file); + } } } @@ -114,14 +115,10 @@ protected function convertFileInformation($file) */ protected function fixPhpFilesArray($data) { - if (!is_array($data)) { - return $data; - } - $keys = array_keys($data); sort($keys); - if (self::$fileKeys != $keys || !isset($data['name']) || !is_array($data['name'])) { + if (self::$fileKeys != $keys || !isset($data['name']) || !\is_array($data['name'])) { return $data; } @@ -131,13 +128,13 @@ protected function fixPhpFilesArray($data) } foreach ($data['name'] as $key => $name) { - $files[$key] = $this->fixPhpFilesArray(array( + $files[$key] = $this->fixPhpFilesArray([ 'error' => $data['error'][$key], 'name' => $name, 'type' => $data['type'][$key], 'tmp_name' => $data['tmp_name'][$key], 'size' => $data['size'][$key], - )); + ]); } return $files; diff --git a/HeaderBag.php b/HeaderBag.php index 3cc9e7024..35bd6ad8f 100644 --- a/HeaderBag.php +++ b/HeaderBag.php @@ -18,15 +18,13 @@ */ class HeaderBag implements \IteratorAggregate, \Countable { - protected $headers = array(); - protected $cacheControl = array(); + protected $headers = []; + protected $cacheControl = []; /** - * Constructor. - * * @param array $headers An array of HTTP headers */ - public function __construct(array $headers = array()) + public function __construct(array $headers = []) { foreach ($headers as $key => $values) { $this->set($key, $values); @@ -82,9 +80,9 @@ public function keys() * * @param array $headers An array of HTTP headers */ - public function replace(array $headers = array()) + public function replace(array $headers = []) { - $this->headers = array(); + $this->headers = []; $this->add($headers); } @@ -103,27 +101,35 @@ public function add(array $headers) /** * Returns a header value by name. * - * @param string $key The header name - * @param mixed $default The default value - * @param bool $first Whether to return the first value or all header values + * @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|array The first header value if $first is true, an array of values otherwise + * @return string|string[]|null The first header value or default value if $first is true, an array of values otherwise */ public function get($key, $default = null, $first = true) { $key = str_replace('_', '-', strtolower($key)); $headers = $this->all(); - if (!array_key_exists($key, $headers)) { + if (!\array_key_exists($key, $headers)) { if (null === $default) { - return $first ? null : array(); + return $first ? null : []; } - return $first ? $default : array($default); + return $first ? $default : [$default]; } if ($first) { - return count($headers[$key]) ? $headers[$key][0] : $default; + if (!$headers[$key]) { + return $default; + } + + if (null === $headers[$key][0]) { + return null; + } + + return (string) $headers[$key][0]; } return $headers[$key]; @@ -132,24 +138,32 @@ public function get($key, $default = null, $first = true) /** * Sets a header by name. * - * @param string $key The key - * @param string|array $values The value or an array of values - * @param bool $replace Whether to replace the actual value or not (true by default) + * @param string $key The key + * @param string|string[] $values The value or an array of values + * @param bool $replace Whether to replace the actual value or not (true by default) */ public function set($key, $values, $replace = true) { $key = str_replace('_', '-', strtolower($key)); - $values = array_values((array) $values); + if (\is_array($values)) { + $values = array_values($values); - if (true === $replace || !isset($this->headers[$key])) { - $this->headers[$key] = $values; + if (true === $replace || !isset($this->headers[$key])) { + $this->headers[$key] = $values; + } else { + $this->headers[$key] = array_merge($this->headers[$key], $values); + } } else { - $this->headers[$key] = array_merge($this->headers[$key], $values); + if (true === $replace || !isset($this->headers[$key])) { + $this->headers[$key] = [$values]; + } else { + $this->headers[$key][] = $values; + } } if ('cache-control' === $key) { - $this->cacheControl = $this->parseCacheControl($values[0]); + $this->cacheControl = $this->parseCacheControl(implode(', ', $this->headers[$key])); } } @@ -162,7 +176,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(str_replace('_', '-', strtolower($key)), $this->all()); } /** @@ -175,7 +189,7 @@ public function has($key) */ public function contains($key, $value) { - return in_array($value, $this->get($key, null, false)); + return \in_array($value, $this->get($key, null, false)); } /** @@ -190,7 +204,7 @@ public function remove($key) unset($this->headers[$key]); if ('cache-control' === $key) { - $this->cacheControl = array(); + $this->cacheControl = []; } } @@ -200,7 +214,7 @@ public function remove($key) * @param string $key The parameter key * @param \DateTime $default The default value * - * @return null|\DateTime The parsed DateTime or the default value if the header does not exist + * @return \DateTime|null The parsed DateTime or the default value if the header does not exist * * @throws \RuntimeException When the HTTP header is not parseable */ @@ -239,7 +253,7 @@ public function addCacheControlDirective($key, $value = true) */ public function hasCacheControlDirective($key) { - return array_key_exists($key, $this->cacheControl); + return \array_key_exists($key, $this->cacheControl); } /** @@ -251,7 +265,7 @@ public function hasCacheControlDirective($key) */ public function getCacheControlDirective($key) { - return array_key_exists($key, $this->cacheControl) ? $this->cacheControl[$key] : null; + return \array_key_exists($key, $this->cacheControl) ? $this->cacheControl[$key] : null; } /** @@ -283,12 +297,12 @@ public function getIterator() */ public function count() { - return count($this->headers); + return \count($this->headers); } protected function getCacheControlHeader() { - $parts = array(); + $parts = []; ksort($this->cacheControl); foreach ($this->cacheControl as $key => $value) { if (true === $value) { @@ -314,7 +328,7 @@ protected function getCacheControlHeader() */ protected function parseCacheControl($header) { - $cacheControl = array(); + $cacheControl = []; preg_match_all('#([a-zA-Z][a-zA-Z_-]*)\s*(?:=(?:"([^"]*)"|([^ \t",;]*)))?#', $header, $matches, PREG_SET_ORDER); foreach ($matches as $match) { $cacheControl[strtolower($match[1])] = isset($match[3]) ? $match[3] : (isset($match[2]) ? $match[2] : true); diff --git a/IpUtils.php b/IpUtils.php index eba603b15..67d13e57a 100644 --- a/IpUtils.php +++ b/IpUtils.php @@ -18,7 +18,7 @@ */ class IpUtils { - private static $checkedIps = array(); + private static $checkedIps = []; /** * This class should not be instantiated. @@ -37,8 +37,8 @@ private function __construct() */ public static function checkIp($requestIp, $ips) { - if (!is_array($ips)) { - $ips = array($ips); + if (!\is_array($ips)) { + $ips = [$ips]; } $method = substr_count($requestIp, ':') > 1 ? 'checkIp6' : 'checkIp4'; @@ -75,7 +75,7 @@ public static function checkIp4($requestIp, $ip) if (false !== strpos($ip, '/')) { list($address, $netmask) = explode('/', $ip, 2); - if ($netmask === '0') { + if ('0' === $netmask) { return self::$checkedIps[$cacheKey] = filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); } @@ -87,6 +87,10 @@ public static function checkIp4($requestIp, $ip) $netmask = 32; } + if (false === ip2long($address)) { + return self::$checkedIps[$cacheKey] = false; + } + return self::$checkedIps[$cacheKey] = 0 === substr_compare(sprintf('%032b', ip2long($requestIp)), sprintf('%032b', ip2long($address)), 0, $netmask); } @@ -112,13 +116,17 @@ public static function checkIp6($requestIp, $ip) return self::$checkedIps[$cacheKey]; } - if (!((extension_loaded('sockets') && defined('AF_INET6')) || @inet_pton('::1'))) { + if (!((\extension_loaded('sockets') && \defined('AF_INET6')) || @inet_pton('::1'))) { throw new \RuntimeException('Unable to check Ipv6. Check that PHP was not compiled with option "disable-ipv6".'); } if (false !== strpos($ip, '/')) { list($address, $netmask) = explode('/', $ip, 2); + if ('0' === $netmask) { + return (bool) unpack('n*', @inet_pton($address)); + } + if ($netmask < 1 || $netmask > 128) { return self::$checkedIps[$cacheKey] = false; } diff --git a/JsonResponse.php b/JsonResponse.php index cf1a11ea2..b0e765167 100644 --- a/JsonResponse.php +++ b/JsonResponse.php @@ -18,7 +18,7 @@ * object. It is however recommended that you do return an object as it * protects yourself against XSSI and JSON-JavaScript Hijacking. * - * @see https://www.owasp.org/index.php/OWASP_AJAX_Security_Guidelines#Always_return_JSON_with_an_Object_on_the_outside + * @see https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/AJAX_Security_Cheat_Sheet.md#always-return-json-with-an-object-on-the-outside * * @author Igor Wiedler */ @@ -39,7 +39,7 @@ class JsonResponse extends Response * @param array $headers An array of response headers * @param bool $json If the data is already a JSON string */ - public function __construct($data = null, $status = 200, $headers = array(), $json = false) + public function __construct($data = null, $status = 200, $headers = [], $json = false) { parent::__construct('', $status, $headers); @@ -55,24 +55,35 @@ public function __construct($data = null, $status = 200, $headers = array(), $js * * Example: * - * return JsonResponse::create($data, 200) + * return JsonResponse::create(['key' => 'value']) * ->setSharedMaxAge(300); * - * @param mixed $data The json response data + * @param mixed $data The JSON response data * @param int $status The response status code * @param array $headers An array of response headers * * @return static */ - public static function create($data = null, $status = 200, $headers = array()) + public static function create($data = null, $status = 200, $headers = []) { return new static($data, $status, $headers); } /** - * Make easier the creation of JsonResponse from raw json. + * Factory method for chainability. + * + * Example: + * + * return JsonResponse::fromJsonString('{"key": "value"}') + * ->setSharedMaxAge(300); + * + * @param string|null $data The JSON response string + * @param int $status The response status code + * @param array $headers An array of response headers + * + * @return static */ - public static function fromJsonString($data = null, $status = 200, $headers = array()) + public static function fromJsonString($data = null, $status = 200, $headers = []) { return new static($data, $status, $headers, true); } @@ -89,19 +100,19 @@ public static function fromJsonString($data = null, $status = 200, $headers = ar 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 $pattern = '/^[$_\p{L}][$_\p{L}\p{Mn}\p{Mc}\p{Nd}\p{Pc}\x{200C}\x{200D}]*(?:\[(?:"(?:\\\.|[^"\\\])*"|\'(?:\\\.|[^\'\\\])*\'|\d+)\])*?$/u'; - $reserved = array( + $reserved = [ 'break', 'do', 'instanceof', 'typeof', 'case', 'else', 'new', 'var', 'catch', 'finally', 'return', 'void', 'continue', 'for', 'switch', 'while', 'debugger', 'function', 'this', 'with', 'default', 'if', 'throw', 'delete', 'in', 'try', 'class', 'enum', 'extends', 'super', 'const', 'export', 'import', 'implements', 'let', 'private', 'public', 'yield', 'interface', 'package', 'protected', 'static', 'null', 'true', 'false', - ); + ]; $parts = explode('.', $callback); foreach ($parts as $part) { - if (!preg_match($pattern, $part) || in_array($part, $reserved, true)) { + if (!preg_match($pattern, $part) || \in_array($part, $reserved, true)) { throw new \InvalidArgumentException('The callback name is not valid.'); } } @@ -137,24 +148,34 @@ public function setJson($json) * * @throws \InvalidArgumentException */ - public function setData($data = array()) + public function setData($data = []) { - if (defined('HHVM_VERSION')) { + if (\defined('HHVM_VERSION')) { // HHVM does not trigger any warnings and let exceptions // thrown from a JsonSerializable object pass through. // If only PHP did the same... $data = json_encode($data, $this->encodingOptions); } else { - try { - // PHP 5.4 and up wrap exceptions thrown by JsonSerializable - // objects in a new exception that needs to be removed. - // Fortunately, PHP 5.5 and up do not trigger any warning anymore. - $data = json_encode($data, $this->encodingOptions); - } catch (\Exception $e) { - if ('Exception' === get_class($e) && 0 === strpos($e->getMessage(), 'Failed calling ')) { - throw $e->getPrevious() ?: $e; + if (!interface_exists('JsonSerializable', false)) { + set_error_handler(function () { return false; }); + try { + $data = @json_encode($data, $this->encodingOptions); + } finally { + restore_error_handler(); + } + } else { + try { + $data = json_encode($data, $this->encodingOptions); + } catch (\Exception $e) { + if ('Exception' === \get_class($e) && 0 === strpos($e->getMessage(), 'Failed calling ')) { + throw $e->getPrevious() ?: $e; + } + throw $e; + } + + if (\PHP_VERSION_ID >= 70300 && (JSON_THROW_ON_ERROR & $this->encodingOptions)) { + return $this->setJson($data); } - throw $e; } } diff --git a/LICENSE b/LICENSE index 17d16a133..a677f4376 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2017 Fabien Potencier +Copyright (c) 2004-2019 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 c0b36479f..194ba2c6c 100644 --- a/ParameterBag.php +++ b/ParameterBag.php @@ -20,17 +20,13 @@ class ParameterBag implements \IteratorAggregate, \Countable { /** * Parameter storage. - * - * @var array */ protected $parameters; /** - * Constructor. - * * @param array $parameters An array of parameters */ - public function __construct(array $parameters = array()) + public function __construct(array $parameters = []) { $this->parameters = $parameters; } @@ -60,7 +56,7 @@ public function keys() * * @param array $parameters An array of parameters */ - public function replace(array $parameters = array()) + public function replace(array $parameters = []) { $this->parameters = $parameters; } @@ -70,7 +66,7 @@ public function replace(array $parameters = array()) * * @param array $parameters An array of parameters */ - public function add(array $parameters = array()) + public function add(array $parameters = []) { $this->parameters = array_replace($this->parameters, $parameters); } @@ -85,7 +81,7 @@ public function add(array $parameters = array()) */ public function get($key, $default = null) { - return array_key_exists($key, $this->parameters) ? $this->parameters[$key] : $default; + return \array_key_exists($key, $this->parameters) ? $this->parameters[$key] : $default; } /** @@ -108,7 +104,7 @@ public function set($key, $value) */ public function has($key) { - return array_key_exists($key, $this->parameters); + return \array_key_exists($key, $this->parameters); } /** @@ -158,7 +154,7 @@ public function getAlnum($key, $default = '') public function getDigits($key, $default = '') { // we need to remove - and + because they're allowed in the filter - return str_replace(array('-', '+'), '', $this->filter($key, $default, FILTER_SANITIZE_NUMBER_INT)); + return str_replace(['-', '+'], '', $this->filter($key, $default, FILTER_SANITIZE_NUMBER_INT)); } /** @@ -178,7 +174,7 @@ public function getInt($key, $default = 0) * Returns the parameter value converted to boolean. * * @param string $key The parameter key - * @param mixed $default The default value if the parameter key does not exist + * @param bool $default The default value if the parameter key does not exist * * @return bool The filtered value */ @@ -195,21 +191,21 @@ 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 */ - public function filter($key, $default = null, $filter = FILTER_DEFAULT, $options = array()) + public function filter($key, $default = null, $filter = FILTER_DEFAULT, $options = []) { $value = $this->get($key, $default); // Always turn $options into an array - this allows filter_var option shortcuts. - if (!is_array($options) && $options) { - $options = array('flags' => $options); + if (!\is_array($options) && $options) { + $options = ['flags' => $options]; } // Add a convenience check for arrays. - if (is_array($value) && !isset($options['flags'])) { + if (\is_array($value) && !isset($options['flags'])) { $options['flags'] = FILTER_REQUIRE_ARRAY; } @@ -233,6 +229,6 @@ public function getIterator() */ public function count() { - return count($this->parameters); + return \count($this->parameters); } } diff --git a/RedirectResponse.php b/RedirectResponse.php index cb1c58e89..a19efba3e 100644 --- a/RedirectResponse.php +++ b/RedirectResponse.php @@ -30,9 +30,9 @@ class RedirectResponse extends Response * * @throws \InvalidArgumentException * - * @see http://tools.ietf.org/html/rfc2616#section-10.3 + * @see https://tools.ietf.org/html/rfc2616#section-10.3 */ - public function __construct($url, $status = 302, $headers = array()) + public function __construct($url, $status = 302, $headers = []) { parent::__construct('', $status, $headers); @@ -42,15 +42,21 @@ public function __construct($url, $status = 302, $headers = array()) throw new \InvalidArgumentException(sprintf('The HTTP status code is not a redirect ("%s" given).', $status)); } - if (301 == $status && !array_key_exists('cache-control', $headers)) { + if (301 == $status && !\array_key_exists('cache-control', array_change_key_case($headers, \CASE_LOWER))) { $this->headers->remove('cache-control'); } } /** - * {@inheritdoc} + * Factory method for chainability. + * + * @param string $url The url to redirect to + * @param int $status The response status code + * @param array $headers An array of response headers + * + * @return static */ - public static function create($url = '', $status = 302, $headers = array()) + public static function create($url = '', $status = 302, $headers = []) { return new static($url, $status, $headers); } @@ -87,7 +93,7 @@ public function setTargetUrl($url) - + Redirecting to %1$s diff --git a/Request.php b/Request.php index 6fd0707b0..3fc7b71e6 100644 --- a/Request.php +++ b/Request.php @@ -61,17 +61,17 @@ class Request /** * @var string[] */ - protected static $trustedProxies = array(); + protected static $trustedProxies = []; /** * @var string[] */ - protected static $trustedHostPatterns = array(); + protected static $trustedHostPatterns = []; /** * @var string[] */ - protected static $trustedHosts = array(); + protected static $trustedHosts = []; /** * Names for headers that can be trusted when @@ -84,67 +84,67 @@ class Request * * @deprecated since version 3.3, to be removed in 4.0 */ - protected static $trustedHeaders = array( + protected static $trustedHeaders = [ self::HEADER_FORWARDED => 'FORWARDED', self::HEADER_CLIENT_IP => 'X_FORWARDED_FOR', self::HEADER_CLIENT_HOST => 'X_FORWARDED_HOST', self::HEADER_CLIENT_PROTO => 'X_FORWARDED_PROTO', self::HEADER_CLIENT_PORT => 'X_FORWARDED_PORT', - ); + ]; protected static $httpMethodParameterOverride = false; /** * 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; /** - * @var string + * @var string|resource|false|null */ protected $content; @@ -199,7 +199,7 @@ class Request protected $format; /** - * @var \Symfony\Component\HttpFoundation\Session\SessionInterface + * @var SessionInterface */ protected $session; @@ -221,39 +221,36 @@ class Request protected static $requestFactory; private $isHostValid = true; - private $isClientIpsValid = true; private $isForwardedValid = true; private static $trustedHeaderSet = -1; /** @deprecated since version 3.3, to be removed in 4.0 */ - private static $trustedHeaderNames = array( + private static $trustedHeaderNames = [ self::HEADER_FORWARDED => 'FORWARDED', self::HEADER_CLIENT_IP => 'X_FORWARDED_FOR', self::HEADER_CLIENT_HOST => 'X_FORWARDED_HOST', self::HEADER_CLIENT_PROTO => 'X_FORWARDED_PROTO', self::HEADER_CLIENT_PORT => 'X_FORWARDED_PORT', - ); + ]; - private static $forwardedParams = array( + private static $forwardedParams = [ self::HEADER_X_FORWARDED_FOR => 'for', self::HEADER_X_FORWARDED_HOST => 'host', self::HEADER_X_FORWARDED_PROTO => 'proto', self::HEADER_X_FORWARDED_PORT => 'host', - ); + ]; /** - * Constructor. - * - * @param array $query The GET parameters - * @param array $request The POST parameters - * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...) - * @param array $cookies The COOKIE parameters - * @param array $files The FILES parameters - * @param array $server The SERVER parameters - * @param string|resource $content The raw body data + * @param array $query The GET parameters + * @param array $request The POST parameters + * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...) + * @param array $cookies The COOKIE parameters + * @param array $files The FILES parameters + * @param array $server The SERVER parameters + * @param string|resource|null $content The raw body data */ - public function __construct(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null) + public function __construct(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null) { $this->initialize($query, $request, $attributes, $cookies, $files, $server, $content); } @@ -263,15 +260,15 @@ public function __construct(array $query = array(), array $request = array(), ar * * This method also re-initializes all properties. * - * @param array $query The GET parameters - * @param array $request The POST parameters - * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...) - * @param array $cookies The COOKIE parameters - * @param array $files The FILES parameters - * @param array $server The SERVER parameters - * @param string|resource $content The raw body data + * @param array $query The GET parameters + * @param array $request The POST parameters + * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...) + * @param array $cookies The COOKIE parameters + * @param array $files The FILES parameters + * @param array $server The SERVER parameters + * @param string|resource|null $content The raw body data */ - public function initialize(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null) + public function initialize(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null) { $this->request = new ParameterBag($request); $this->query = new ParameterBag($query); @@ -305,19 +302,19 @@ public static function createFromGlobals() // stores the Content-Type and Content-Length header values in // HTTP_CONTENT_TYPE and HTTP_CONTENT_LENGTH fields. $server = $_SERVER; - if ('cli-server' === PHP_SAPI) { - if (array_key_exists('HTTP_CONTENT_LENGTH', $_SERVER)) { + if ('cli-server' === \PHP_SAPI) { + if (\array_key_exists('HTTP_CONTENT_LENGTH', $_SERVER)) { $server['CONTENT_LENGTH'] = $_SERVER['HTTP_CONTENT_LENGTH']; } - if (array_key_exists('HTTP_CONTENT_TYPE', $_SERVER)) { + if (\array_key_exists('HTTP_CONTENT_TYPE', $_SERVER)) { $server['CONTENT_TYPE'] = $_SERVER['HTTP_CONTENT_TYPE']; } } - $request = self::createRequestFromFactory($_GET, $_POST, array(), $_COOKIE, $_FILES, $server); + $request = self::createRequestFromFactory($_GET, $_POST, [], $_COOKIE, $_FILES, $server); if (0 === strpos($request->headers->get('CONTENT_TYPE'), 'application/x-www-form-urlencoded') - && in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), array('PUT', 'DELETE', 'PATCH')) + && \in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), ['PUT', 'DELETE', 'PATCH']) ) { parse_str($request->getContent(), $data); $request->request = new ParameterBag($data); @@ -332,19 +329,19 @@ public static function createFromGlobals() * The information contained in the URI always take precedence * over the other information (server and parameters). * - * @param string $uri The URI - * @param string $method The HTTP method - * @param array $parameters The query (GET) or request (POST) parameters - * @param array $cookies The request cookies ($_COOKIE) - * @param array $files The request files ($_FILES) - * @param array $server The server parameters ($_SERVER) - * @param string $content The raw body data + * @param string $uri The URI + * @param string $method The HTTP method + * @param array $parameters The query (GET) or request (POST) parameters + * @param array $cookies The request cookies ($_COOKIE) + * @param array $files The request files ($_FILES) + * @param array $server The server parameters ($_SERVER) + * @param string|resource|null $content The raw body data * * @return static */ - public static function create($uri, $method = 'GET', $parameters = array(), $cookies = array(), $files = array(), $server = array(), $content = null) + public static function create($uri, $method = 'GET', $parameters = [], $cookies = [], $files = [], $server = [], $content = null) { - $server = array_replace(array( + $server = array_replace([ 'SERVER_NAME' => 'localhost', 'SERVER_PORT' => 80, 'HTTP_HOST' => 'localhost', @@ -357,7 +354,7 @@ public static function create($uri, $method = 'GET', $parameters = array(), $coo 'SCRIPT_FILENAME' => '', 'SERVER_PROTOCOL' => 'HTTP/1.1', 'REQUEST_TIME' => time(), - ), $server); + ], $server); $server['PATH_INFO'] = ''; $server['REQUEST_METHOD'] = strtoupper($method); @@ -380,7 +377,7 @@ public static function create($uri, $method = 'GET', $parameters = array(), $coo if (isset($components['port'])) { $server['SERVER_PORT'] = $components['port']; - $server['HTTP_HOST'] = $server['HTTP_HOST'].':'.$components['port']; + $server['HTTP_HOST'] .= ':'.$components['port']; } if (isset($components['user'])) { @@ -405,10 +402,10 @@ public static function create($uri, $method = 'GET', $parameters = array(), $coo // no break case 'PATCH': $request = $parameters; - $query = array(); + $query = []; break; default: - $request = array(); + $request = []; $query = $parameters; break; } @@ -431,7 +428,7 @@ public static function create($uri, $method = 'GET', $parameters = array(), $coo $server['REQUEST_URI'] = $components['path'].('' !== $queryString ? '?'.$queryString : ''); $server['QUERY_STRING'] = $queryString; - return self::createRequestFromFactory($query, $request, array(), $cookies, $files, $server, $content); + return self::createRequestFromFactory($query, $request, [], $cookies, $files, $server, $content); } /** @@ -463,22 +460,22 @@ public static function setFactory($callable) public function duplicate(array $query = null, array $request = null, array $attributes = null, array $cookies = null, array $files = null, array $server = null) { $dup = clone $this; - if ($query !== null) { + if (null !== $query) { $dup->query = new ParameterBag($query); } - if ($request !== null) { + if (null !== $request) { $dup->request = new ParameterBag($request); } - if ($attributes !== null) { + if (null !== $attributes) { $dup->attributes = new ParameterBag($attributes); } - if ($cookies !== null) { + if (null !== $cookies) { $dup->cookies = new ParameterBag($cookies); } - if ($files !== null) { + if (null !== $files) { $dup->files = new FileBag($files); } - if ($server !== null) { + if (null !== $server) { $dup->server = new ServerBag($server); $dup->headers = new HeaderBag($dup->server->getHeaders()); } @@ -531,12 +528,28 @@ public function __toString() try { $content = $this->getContent(); } catch (\LogicException $e) { + if (\PHP_VERSION_ID >= 70400) { + throw $e; + } + return trigger_error($e, E_USER_ERROR); } + $cookieHeader = ''; + $cookies = []; + + foreach ($this->cookies as $k => $v) { + $cookies[] = $k.'='.$v; + } + + if (!empty($cookies)) { + $cookieHeader = 'Cookie: '.implode('; ', $cookies)."\r\n"; + } + return sprintf('%s %s %s', $this->getMethod(), $this->getRequestUri(), $this->server->get('SERVER_PROTOCOL'))."\r\n". - $this->headers."\r\n". + $this->headers. + $cookieHeader."\r\n". $content; } @@ -548,7 +561,7 @@ public function __toString() */ public function overrideGlobals() { - $this->server->set('QUERY_STRING', static::normalizeQueryString(http_build_query($this->query->all(), null, '&'))); + $this->server->set('QUERY_STRING', static::normalizeQueryString(http_build_query($this->query->all(), '', '&'))); $_GET = $this->query->all(); $_POST = $this->request->all(); @@ -557,19 +570,19 @@ public function overrideGlobals() foreach ($this->headers->all() as $key => $value) { $key = strtoupper(str_replace('-', '_', $key)); - if (in_array($key, array('CONTENT_TYPE', 'CONTENT_LENGTH'))) { + if (\in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH'])) { $_SERVER[$key] = implode(', ', $value); } else { $_SERVER['HTTP_'.$key] = implode(', ', $value); } } - $request = array('g' => $_GET, 'p' => $_POST, 'c' => $_COOKIE); + $request = ['g' => $_GET, 'p' => $_POST, 'c' => $_COOKIE]; $requestOrder = ini_get('request_order') ?: ini_get('variables_order'); $requestOrder = preg_replace('#[^cgp]#', '', strtolower($requestOrder)) ?: 'gp'; - $_REQUEST = array(); + $_REQUEST = []; foreach (str_split($requestOrder) as $order) { $_REQUEST = array_merge($_REQUEST, $request[$order]); } @@ -589,8 +602,8 @@ public static function setTrustedProxies(array $proxies/*, int $trustedHeaderSet { self::$trustedProxies = $proxies; - if (2 > func_num_args()) { - @trigger_error(sprintf('The %s() method expects a bit field of Request::HEADER_* as second argument since version 3.3. Defining it will be required in 4.0. ', __METHOD__), E_USER_DEPRECATED); + if (2 > \func_num_args()) { + @trigger_error(sprintf('The %s() method expects a bit field of Request::HEADER_* as second argument since Symfony 3.3. Defining it will be required in 4.0. ', __METHOD__), E_USER_DEPRECATED); return; } @@ -632,10 +645,10 @@ public static function getTrustedHeaderSet() public static function setTrustedHosts(array $hostPatterns) { self::$trustedHostPatterns = array_map(function ($hostPattern) { - return sprintf('#%s#i', $hostPattern); + return sprintf('{%s}i', $hostPattern); }, $hostPatterns); // we need to reset trusted hosts on trusted host patterns change - self::$trustedHosts = array(); + self::$trustedHosts = []; } /** @@ -670,7 +683,7 @@ public static function getTrustedHosts() */ public static function setTrustedHeaderName($key, $value) { - @trigger_error(sprintf('The "%s()" method is deprecated since version 3.3 and will be removed in 4.0. Use the $trustedHeaderSet argument of the Request::setTrustedProxies() method instead.', __METHOD__), E_USER_DEPRECATED); + @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 3.3 and will be removed in 4.0. Use the $trustedHeaderSet argument of the Request::setTrustedProxies() method instead.', __METHOD__), E_USER_DEPRECATED); if ('forwarded' === $key) { $key = self::HEADER_FORWARDED; @@ -682,7 +695,7 @@ public static function setTrustedHeaderName($key, $value) $key = self::HEADER_CLIENT_PROTO; } elseif ('client_port' === $key) { $key = self::HEADER_CLIENT_PORT; - } elseif (!array_key_exists($key, self::$trustedHeaders)) { + } elseif (!\array_key_exists($key, self::$trustedHeaders)) { throw new \InvalidArgumentException(sprintf('Unable to set the trusted header name for key "%s".', $key)); } @@ -709,11 +722,11 @@ public static function setTrustedHeaderName($key, $value) */ public static function getTrustedHeaderName($key) { - if (2 > func_num_args() || func_get_arg(1)) { - @trigger_error(sprintf('The "%s()" method is deprecated since version 3.3 and will be removed in 4.0. Use the Request::getTrustedHeaderSet() method instead.', __METHOD__), E_USER_DEPRECATED); + if (2 > \func_num_args() || func_get_arg(1)) { + @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 3.3 and will be removed in 4.0. Use the Request::getTrustedHeaderSet() method instead.', __METHOD__), E_USER_DEPRECATED); } - if (!array_key_exists($key, self::$trustedHeaders)) { + if (!\array_key_exists($key, self::$trustedHeaders)) { throw new \InvalidArgumentException(sprintf('Unable to get the trusted header name for key "%s".', $key)); } @@ -736,8 +749,8 @@ public static function normalizeQueryString($qs) return ''; } - $parts = array(); - $order = array(); + $parts = []; + $order = []; foreach (explode('&', $qs) as $param) { if ('' === $param || '=' === $param[0]) { @@ -798,8 +811,8 @@ public static function getHttpMethodParameterOverride() * * Order of precedence: PATH (routing placeholders or custom attributes), GET, BODY * - * @param string $key the key - * @param mixed $default the default value if the parameter key does not exist + * @param string $key The key + * @param mixed $default The default value if the parameter key does not exist * * @return mixed */ @@ -884,10 +897,10 @@ public function getClientIps() $ip = $this->server->get('REMOTE_ADDR'); if (!$this->isFromTrustedProxy()) { - return array($ip); + return [$ip]; } - return $this->getTrustedValues(self::HEADER_CLIENT_IP, $ip) ?: array($ip); + return $this->getTrustedValues(self::HEADER_CLIENT_IP, $ip) ?: [$ip]; } /** @@ -906,7 +919,7 @@ public function getClientIps() * @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() { @@ -1022,14 +1035,14 @@ public function getPort() return $this->server->get('SERVER_PORT'); } - if ($host[0] === '[') { + if ('[' === $host[0]) { $pos = strpos($host, ':', strrpos($host, ']')); } else { $pos = strrpos($host, ':'); } - if (false !== $pos) { - return (int) substr($host, $pos + 1); + if (false !== $pos && $port = substr($host, $pos + 1)) { + return (int) $port; } return 'https' === $this->getScheme() ? 443 : 80; @@ -1084,7 +1097,7 @@ public function getHttpHost() $scheme = $this->getScheme(); $port = $this->getPort(); - if (('http' == $scheme && $port == 80) || ('https' == $scheme && $port == 443)) { + if (('http' == $scheme && 80 == $port) || ('https' == $scheme && 443 == $port)) { return $this->getHost(); } @@ -1177,7 +1190,7 @@ public function getRelativeUriForPath($path) } $sourceDirs = explode('/', isset($basePath[0]) && '/' === $basePath[0] ? substr($basePath, 1) : $basePath); - $targetDirs = explode('/', isset($path[0]) && '/' === $path[0] ? substr($path, 1) : $path); + $targetDirs = explode('/', substr($path, 1)); array_pop($sourceDirs); $targetFile = array_pop($targetDirs); @@ -1190,12 +1203,12 @@ public function getRelativeUriForPath($path) } $targetDirs[] = $targetFile; - $path = str_repeat('../', count($sourceDirs)).implode('/', $targetDirs); + $path = str_repeat('../', \count($sourceDirs)).implode('/', $targetDirs); // 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; @@ -1233,7 +1246,7 @@ public function getQueryString() public function isSecure() { if ($this->isFromTrustedProxy() && $proto = $this->getTrustedValues(self::HEADER_CLIENT_PROTO)) { - return in_array(strtolower($proto[0]), array('https', 'on', 'ssl', '1'), true); + return \in_array(strtolower($proto[0]), ['https', 'on', 'ssl', '1'], true); } $https = $this->server->get('HTTPS'); @@ -1283,10 +1296,10 @@ public function getHost() throw new SuspiciousOperationException(sprintf('Invalid Host "%s".', $host)); } - if (count(self::$trustedHostPatterns) > 0) { + if (\count(self::$trustedHostPatterns) > 0) { // to avoid host header injection attacks, you should provide a list of trusted host patterns - if (in_array($host, self::$trustedHosts)) { + if (\in_array($host, self::$trustedHosts)) { return $host; } @@ -1337,19 +1350,37 @@ public function setMethod($method) */ public function getMethod() { - if (null === $this->method) { - $this->method = strtoupper($this->server->get('REQUEST_METHOD', 'GET')); + if (null !== $this->method) { + return $this->method; + } - if ('POST' === $this->method) { - if ($method = $this->headers->get('X-HTTP-METHOD-OVERRIDE')) { - $this->method = strtoupper($method); - } elseif (self::$httpMethodParameterOverride) { - $this->method = strtoupper($this->request->get('_method', $this->query->get('_method', 'POST'))); - } - } + $this->method = strtoupper($this->server->get('REQUEST_METHOD', 'GET')); + + if ('POST' !== $this->method) { + return $this->method; + } + + $method = $this->headers->get('X-HTTP-METHOD-OVERRIDE'); + + if (!$method && self::$httpMethodParameterOverride) { + $method = $this->request->get('_method', $this->query->get('_method', 'POST')); } - return $this->method; + if (!\is_string($method)) { + return $this->method; + } + + $method = strtoupper($method); + + if (\in_array($method, ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'PATCH', 'PURGE', 'TRACE'], true)) { + return $this->method = $method; + } + + if (!preg_match('/^[A-Z]++$/D', $method)) { + throw new SuspiciousOperationException(sprintf('Invalid method override "%s".', $method)); + } + + return $this->method = $method; } /** @@ -1369,7 +1400,7 @@ public function getRealMethod() * * @param string $format The format * - * @return string The associated mime type (null if not found) + * @return string|null The associated mime type (null if not found) */ public function getMimeType($format) { @@ -1393,7 +1424,7 @@ public static function getMimeTypes($format) static::initializeFormats(); } - return isset(static::$formats[$format]) ? static::$formats[$format] : array(); + return isset(static::$formats[$format]) ? static::$formats[$format] : []; } /** @@ -1407,7 +1438,7 @@ public function getFormat($mimeType) { $canonicalMimeType = null; if (false !== $pos = strpos($mimeType, ';')) { - $canonicalMimeType = substr($mimeType, 0, $pos); + $canonicalMimeType = trim(substr($mimeType, 0, $pos)); } if (null === static::$formats) { @@ -1415,13 +1446,15 @@ public function getFormat($mimeType) } foreach (static::$formats as $format => $mimeTypes) { - if (in_array($mimeType, (array) $mimeTypes)) { + if (\in_array($mimeType, (array) $mimeTypes)) { return $format; } - if (null !== $canonicalMimeType && in_array($canonicalMimeType, (array) $mimeTypes)) { + if (null !== $canonicalMimeType && \in_array($canonicalMimeType, (array) $mimeTypes)) { return $format; } } + + return null; } /** @@ -1436,7 +1469,7 @@ public function setFormat($format, $mimeTypes) static::initializeFormats(); } - static::$formats[$format] = is_array($mimeTypes) ? $mimeTypes : array($mimeTypes); + static::$formats[$format] = \is_array($mimeTypes) ? $mimeTypes : [$mimeTypes]; } /** @@ -1448,9 +1481,9 @@ public function setFormat($format, $mimeTypes) * * _format request attribute * * $default * - * @param string $default The default format + * @param string|null $default The default format * - * @return string The request format + * @return string|null The request format */ public function getRequestFormat($default = 'html') { @@ -1548,15 +1581,15 @@ public function isMethod($method) */ public function isMethodSafe(/* $andCacheable = true */) { - if (!func_num_args() || func_get_arg(0)) { + if (!\func_num_args() || func_get_arg(0)) { // This deprecation should be turned into a BadMethodCallException in 4.0 (without adding the argument in the signature) // then setting $andCacheable to false should be deprecated in 4.1 - @trigger_error('Checking only for cacheable HTTP methods with Symfony\Component\HttpFoundation\Request::isMethodSafe() is deprecated since version 3.2 and will throw an exception in 4.0. Disable checking only for cacheable methods by calling the method with `false` as first argument or use the Request::isMethodCacheable() instead.', E_USER_DEPRECATED); + @trigger_error('Checking only for cacheable HTTP methods with Symfony\Component\HttpFoundation\Request::isMethodSafe() is deprecated since Symfony 3.2 and will throw an exception in 4.0. Disable checking only for cacheable methods by calling the method with `false` as first argument or use the Request::isMethodCacheable() instead.', E_USER_DEPRECATED); - return in_array($this->getMethod(), array('GET', 'HEAD')); + return \in_array($this->getMethod(), ['GET', 'HEAD']); } - return in_array($this->getMethod(), array('GET', 'HEAD', 'OPTIONS', 'TRACE')); + return \in_array($this->getMethod(), ['GET', 'HEAD', 'OPTIONS', 'TRACE']); } /** @@ -1566,7 +1599,7 @@ public function isMethodSafe(/* $andCacheable = true */) */ public function isMethodIdempotent() { - return in_array($this->getMethod(), array('HEAD', 'GET', 'PUT', 'DELETE', 'TRACE', 'OPTIONS', 'PURGE')); + return \in_array($this->getMethod(), ['HEAD', 'GET', 'PUT', 'DELETE', 'TRACE', 'OPTIONS', 'PURGE']); } /** @@ -1574,11 +1607,35 @@ public function isMethodIdempotent() * * @see https://tools.ietf.org/html/rfc7231#section-4.2.3 * - * @return bool + * @return bool True for GET and HEAD, false otherwise */ public function isMethodCacheable() { - return in_array($this->getMethod(), array('GET', 'HEAD')); + return \in_array($this->getMethod(), ['GET', 'HEAD']); + } + + /** + * Returns the protocol version. + * + * If the application is behind a proxy, the protocol version used in the + * requests between the client and the proxy and between the proxy and the + * server might be different. This returns the former (from the "Via" header) + * if the proxy is trusted (see "setTrustedProxies()"), otherwise it returns + * the latter (from the "SERVER_PROTOCOL" server parameter). + * + * @return string + */ + public function getProtocolVersion() + { + if ($this->isFromTrustedProxy()) { + preg_match('~^(HTTP/)?([1-9]\.[0-9]) ~', $this->headers->get('Via'), $matches); + + if ($matches) { + return 'HTTP/'.$matches[2]; + } + } + + return $this->server->get('SERVER_PROTOCOL'); } /** @@ -1592,7 +1649,7 @@ public function isMethodCacheable() */ public function getContent($asResource = false) { - $currentContentIsResource = is_resource($this->content); + $currentContentIsResource = \is_resource($this->content); if (\PHP_VERSION_ID < 50600 && false === $this->content) { throw new \LogicException('getContent() can only be called once when using the resource return type and PHP below 5.6.'); } @@ -1605,7 +1662,7 @@ public function getContent($asResource = false) } // Content passed in parameter (test) - if (is_string($this->content)) { + if (\is_string($this->content)) { $resource = fopen('php://temp', 'r+'); fwrite($resource, $this->content); rewind($resource); @@ -1668,12 +1725,12 @@ public function getPreferredLanguage(array $locales = null) return $locales[0]; } - $extendedPreferredLanguages = array(); + $extendedPreferredLanguages = []; foreach ($preferredLanguages as $language) { $extendedPreferredLanguages[] = $language; if (false !== $position = strpos($language, '_')) { $superLanguage = substr($language, 0, $position); - if (!in_array($superLanguage, $preferredLanguages)) { + if (!\in_array($superLanguage, $preferredLanguages)) { $extendedPreferredLanguages[] = $superLanguage; } } @@ -1696,7 +1753,7 @@ public function getLanguages() } $languages = AcceptHeader::fromString($this->headers->get('Accept-Language'))->all(); - $this->languages = array(); + $this->languages = []; foreach ($languages as $lang => $acceptHeaderItem) { if (false !== strpos($lang, '-')) { $codes = explode('-', $lang); @@ -1704,12 +1761,12 @@ public function getLanguages() // Language not listed in ISO 639 that are not variants // of any listed language, which can be registered with the // i-prefix, such as i-cherokee - if (count($codes) > 1) { + if (\count($codes) > 1) { $lang = $codes[1]; } } else { - for ($i = 0, $max = count($codes); $i < $max; ++$i) { - if ($i === 0) { + for ($i = 0, $max = \count($codes); $i < $max; ++$i) { + if (0 === $i) { $lang = strtolower($codes[0]); } else { $lang .= '_'.strtoupper($codes[$i]); @@ -1772,7 +1829,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 */ @@ -1784,37 +1841,40 @@ 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() { $requestUri = ''; - if ($this->headers->has('X_ORIGINAL_URL')) { - // IIS with Microsoft Rewrite Module - $requestUri = $this->headers->get('X_ORIGINAL_URL'); - $this->headers->remove('X_ORIGINAL_URL'); - $this->server->remove('HTTP_X_ORIGINAL_URL'); - $this->server->remove('UNENCODED_URL'); - $this->server->remove('IIS_WasUrlRewritten'); - } elseif ($this->headers->has('X_REWRITE_URL')) { - // IIS with ISAPI_Rewrite - $requestUri = $this->headers->get('X_REWRITE_URL'); - $this->headers->remove('X_REWRITE_URL'); - } elseif ($this->server->get('IIS_WasUrlRewritten') == '1' && $this->server->get('UNENCODED_URL') != '') { + if ('1' == $this->server->get('IIS_WasUrlRewritten') && '' != $this->server->get('UNENCODED_URL')) { // IIS7 with URL Rewrite: make sure we get the unencoded URL (double slash problem) $requestUri = $this->server->get('UNENCODED_URL'); $this->server->remove('UNENCODED_URL'); $this->server->remove('IIS_WasUrlRewritten'); } elseif ($this->server->has('REQUEST_URI')) { $requestUri = $this->server->get('REQUEST_URI'); - // HTTP proxy reqs setup request URI with scheme and host [and port] + the URL path, only use URL path - $schemeAndHttpHost = $this->getSchemeAndHttpHost(); - if (strpos($requestUri, $schemeAndHttpHost) === 0) { - $requestUri = substr($requestUri, strlen($schemeAndHttpHost)); + + if ('' !== $requestUri && '/' === $requestUri[0]) { + // To only use path and query remove the fragment. + if (false !== $pos = strpos($requestUri, '#')) { + $requestUri = substr($requestUri, 0, $pos); + } + } else { + // HTTP proxy reqs setup request URI with scheme and host [and port] + the URL path, + // only use URL path. + $uriComponents = parse_url($requestUri); + + if (isset($uriComponents['path'])) { + $requestUri = $uriComponents['path']; + } + + if (isset($uriComponents['query'])) { + $requestUri .= '?'.$uriComponents['query']; + } } } elseif ($this->server->has('ORIG_PATH_INFO')) { // IIS 5.0, PHP as CGI @@ -1854,7 +1914,7 @@ protected function prepareBaseUrl() $segs = explode('/', trim($file, '/')); $segs = array_reverse($segs); $index = 0; - $last = count($segs); + $last = \count($segs); $baseUrl = ''; do { $seg = $segs[$index]; @@ -1865,15 +1925,18 @@ protected function prepareBaseUrl() // Does the baseUrl have anything in common with the request_uri? $requestUri = $this->getRequestUri(); + if ('' !== $requestUri && '/' !== $requestUri[0]) { + $requestUri = '/'.$requestUri; + } if ($baseUrl && false !== $prefix = $this->getUrlencodedPrefix($requestUri, $baseUrl)) { // full $baseUrl matches return $prefix; } - if ($baseUrl && false !== $prefix = $this->getUrlencodedPrefix($requestUri, rtrim(dirname($baseUrl), '/'.DIRECTORY_SEPARATOR).'/')) { + if ($baseUrl && false !== $prefix = $this->getUrlencodedPrefix($requestUri, rtrim(\dirname($baseUrl), '/'.\DIRECTORY_SEPARATOR).'/')) { // directory portion of $baseUrl matches - return rtrim($prefix, '/'.DIRECTORY_SEPARATOR); + return rtrim($prefix, '/'.\DIRECTORY_SEPARATOR); } $truncatedRequestUri = $requestUri; @@ -1890,11 +1953,11 @@ protected function prepareBaseUrl() // If using mod_rewrite or ISAPI_Rewrite strip the script filename // out of baseUrl. $pos !== 0 makes sure it is not matching a value // from PATH_INFO or QUERY_STRING - if (strlen($requestUri) >= strlen($baseUrl) && (false !== $pos = strpos($requestUri, $baseUrl)) && $pos !== 0) { - $baseUrl = substr($requestUri, 0, $pos + strlen($baseUrl)); + if (\strlen($requestUri) >= \strlen($baseUrl) && (false !== $pos = strpos($requestUri, $baseUrl)) && 0 !== $pos) { + $baseUrl = substr($requestUri, 0, $pos + \strlen($baseUrl)); } - return rtrim($baseUrl, '/'.DIRECTORY_SEPARATOR); + return rtrim($baseUrl, '/'.\DIRECTORY_SEPARATOR); } /** @@ -1904,19 +1967,19 @@ protected function prepareBaseUrl() */ protected function prepareBasePath() { - $filename = basename($this->server->get('SCRIPT_FILENAME')); $baseUrl = $this->getBaseUrl(); if (empty($baseUrl)) { return ''; } + $filename = basename($this->server->get('SCRIPT_FILENAME')); if (basename($baseUrl) === $filename) { - $basePath = dirname($baseUrl); + $basePath = \dirname($baseUrl); } else { $basePath = $baseUrl; } - if ('\\' === DIRECTORY_SEPARATOR) { + if ('\\' === \DIRECTORY_SEPARATOR) { $basePath = str_replace('\\', '/', $basePath); } @@ -1930,23 +1993,26 @@ protected function prepareBasePath() */ protected function preparePathInfo() { - $baseUrl = $this->getBaseUrl(); - if (null === ($requestUri = $this->getRequestUri())) { return '/'; } // Remove the query string from REQUEST_URI - if ($pos = strpos($requestUri, '?')) { + if (false !== $pos = strpos($requestUri, '?')) { $requestUri = substr($requestUri, 0, $pos); } + if ('' !== $requestUri && '/' !== $requestUri[0]) { + $requestUri = '/'.$requestUri; + } + + if (null === ($baseUrl = $this->getBaseUrl())) { + return $requestUri; + } - $pathInfo = substr($requestUri, strlen($baseUrl)); - if (null !== $baseUrl && (false === $pathInfo || '' === $pathInfo)) { + $pathInfo = substr($requestUri, \strlen($baseUrl)); + if (false === $pathInfo || '' === $pathInfo) { // If substr() returns false then PATH_INFO is set to an empty string return '/'; - } elseif (null === $baseUrl) { - return $requestUri; } return (string) $pathInfo; @@ -1957,18 +2023,19 @@ protected function preparePathInfo() */ protected static function initializeFormats() { - static::$formats = array( - 'html' => array('text/html', 'application/xhtml+xml'), - 'txt' => array('text/plain'), - 'js' => array('application/javascript', 'application/x-javascript', 'text/javascript'), - 'css' => array('text/css'), - 'json' => array('application/json', 'application/x-json'), - 'xml' => array('text/xml', 'application/xml', 'application/x-xml'), - 'rdf' => array('application/rdf+xml'), - 'atom' => array('application/atom+xml'), - 'rss' => array('application/rss+xml'), - 'form' => array('application/x-www-form-urlencoded'), - ); + static::$formats = [ + 'html' => ['text/html', 'application/xhtml+xml'], + 'txt' => ['text/plain'], + 'js' => ['application/javascript', 'application/x-javascript', 'text/javascript'], + 'css' => ['text/css'], + 'json' => ['application/json', 'application/x-json'], + 'jsonld' => ['application/ld+json'], + 'xml' => ['text/xml', 'application/xml', 'application/x-xml'], + 'rdf' => ['application/rdf+xml'], + 'atom' => ['application/atom+xml'], + 'rss' => ['application/rss+xml'], + 'form' => ['application/x-www-form-urlencoded'], + ]; } /** @@ -1989,7 +2056,7 @@ private function setPhpDefaultLocale($locale) } } - /* + /** * Returns the prefix as encoded in the string when the string starts with * the given prefix, false otherwise. * @@ -2004,7 +2071,7 @@ private function getUrlencodedPrefix($string, $prefix) return false; } - $len = strlen($prefix); + $len = \strlen($prefix); if (preg_match(sprintf('#^(%%[[:xdigit:]]{2}|.){%d}#', $len), $string, $match)) { return $match[0]; @@ -2013,10 +2080,10 @@ private function getUrlencodedPrefix($string, $prefix) return false; } - private static function createRequestFromFactory(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null) + private static function createRequestFromFactory(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null) { if (self::$requestFactory) { - $request = call_user_func(self::$requestFactory, $query, $request, $attributes, $cookies, $files, $server, $content); + $request = \call_user_func(self::$requestFactory, $query, $request, $attributes, $cookies, $files, $server, $content); if (!$request instanceof self) { throw new \LogicException('The Request factory must return an instance of Symfony\Component\HttpFoundation\Request.'); @@ -2043,8 +2110,8 @@ public function isFromTrustedProxy() private function getTrustedValues($type, $ip = null) { - $clientValues = array(); - $forwardedValues = array(); + $clientValues = []; + $forwardedValues = []; if (self::$trustedHeaders[$type] && $this->headers->has(self::$trustedHeaders[$type])) { foreach (explode(',', $this->headers->get(self::$trustedHeaders[$type])) as $v) { @@ -2054,7 +2121,15 @@ private function getTrustedValues($type, $ip = null) if (self::$trustedHeaders[self::HEADER_FORWARDED] && $this->headers->has(self::$trustedHeaders[self::HEADER_FORWARDED])) { $forwardedValues = $this->headers->get(self::$trustedHeaders[self::HEADER_FORWARDED]); - $forwardedValues = preg_match_all(sprintf('{(?:%s)=(?:"?\[?)([a-zA-Z0-9\.:_\-/]*+)}', self::$forwardedParams[$type]), $forwardedValues, $matches) ? $matches[1] : array(); + $forwardedValues = preg_match_all(sprintf('{(?:%s)="?([a-zA-Z0-9\.:_\-/\[\]]*+)}', self::$forwardedParams[$type]), $forwardedValues, $matches) ? $matches[1] : []; + if (self::HEADER_CLIENT_PORT === $type) { + foreach ($forwardedValues as $k => $v) { + if (']' === substr($v, -1) || false === $v = strrchr($v, ':')) { + $v = $this->isSecure() ? ':443' : ':80'; + } + $forwardedValues[$k] = '0.0.0.0'.$v; + } + } } if (null !== $ip) { @@ -2071,7 +2146,7 @@ private function getTrustedValues($type, $ip = null) } if (!$this->isForwardedValid) { - return null !== $ip ? array('0.0.0.0', $ip) : array(); + return null !== $ip ? ['0.0.0.0', $ip] : []; } $this->isForwardedValid = false; @@ -2081,15 +2156,23 @@ private function getTrustedValues($type, $ip = null) private function normalizeAndFilterClientIps(array $clientIps, $ip) { if (!$clientIps) { - return array(); + return []; } $clientIps[] = $ip; // Complete the IP chain with the IP the request actually came from $firstTrustedIp = null; foreach ($clientIps as $key => $clientIp) { - // Remove port (unfortunately, it does happen) - if (preg_match('{((?:\d+\.){3}\d+)\:\d+}', $clientIp, $match)) { - $clientIps[$key] = $clientIp = $match[1]; + if (strpos($clientIp, '.')) { + // Strip :port from IPv4 addresses. This is allowed in Forwarded + // and may occur in X-Forwarded-For. + $i = strpos($clientIp, ':'); + if ($i) { + $clientIps[$key] = $clientIp = substr($clientIp, 0, $i); + } + } elseif (0 === strpos($clientIp, '[')) { + // Strip brackets and :port from IPv6 addresses. + $i = strpos($clientIp, ']', 1); + $clientIps[$key] = $clientIp = substr($clientIp, 1, $i - 1); } if (!filter_var($clientIp, FILTER_VALIDATE_IP)) { @@ -2109,6 +2192,6 @@ private function normalizeAndFilterClientIps(array $clientIps, $ip) } // Now the IP chain contains only untrusted proxies and the client IP - return $clientIps ? array_reverse($clientIps) : array($firstTrustedIp); + return $clientIps ? array_reverse($clientIps) : [$firstTrustedIp]; } } diff --git a/RequestMatcher.php b/RequestMatcher.php index aa4f67b58..3f5149579 100644 --- a/RequestMatcher.php +++ b/RequestMatcher.php @@ -31,32 +31,31 @@ class RequestMatcher implements RequestMatcherInterface /** * @var string[] */ - private $methods = array(); + private $methods = []; /** * @var string[] */ - private $ips = array(); + private $ips = []; /** * @var array */ - private $attributes = array(); + private $attributes = []; /** * @var string[] */ - private $schemes = array(); + 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($path = null, $host = null, $methods = null, $ips = null, array $attributes = array(), $schemes = null) + public function __construct($path = null, $host = null, $methods = null, $ips = null, array $attributes = [], $schemes = null) { $this->matchPath($path); $this->matchHost($host); @@ -76,7 +75,7 @@ public function __construct($path = null, $host = null, $methods = null, $ips = */ public function matchScheme($scheme) { - $this->schemes = null !== $scheme ? array_map('strtolower', (array) $scheme) : array(); + $this->schemes = null !== $scheme ? array_map('strtolower', (array) $scheme) : []; } /** @@ -116,7 +115,7 @@ public function matchIp($ip) */ public function matchIps($ips) { - $this->ips = null !== $ips ? (array) $ips : array(); + $this->ips = null !== $ips ? (array) $ips : []; } /** @@ -126,7 +125,7 @@ public function matchIps($ips) */ public function matchMethod($method) { - $this->methods = null !== $method ? array_map('strtoupper', (array) $method) : array(); + $this->methods = null !== $method ? array_map('strtoupper', (array) $method) : []; } /** @@ -145,11 +144,11 @@ public function matchAttribute($key, $regexp) */ public function matches(Request $request) { - if ($this->schemes && !in_array($request->getScheme(), $this->schemes, true)) { + if ($this->schemes && !\in_array($request->getScheme(), $this->schemes, true)) { return false; } - if ($this->methods && !in_array($request->getMethod(), $this->methods, true)) { + if ($this->methods && !\in_array($request->getMethod(), $this->methods, true)) { return false; } @@ -173,6 +172,6 @@ public function matches(Request $request) // Note to future implementors: add additional checks above the // foreach above or else your check might not be run! - return count($this->ips) === 0; + return 0 === \count($this->ips); } } diff --git a/RequestMatcherInterface.php b/RequestMatcherInterface.php index 066e7e8bf..c26db3e6f 100644 --- a/RequestMatcherInterface.php +++ b/RequestMatcherInterface.php @@ -21,8 +21,6 @@ interface RequestMatcherInterface /** * Decides whether the rule(s) implemented by the strategy matches the supplied request. * - * @param Request $request The request to check for a match - * * @return bool true if the request matches, false otherwise */ public function matches(Request $request); diff --git a/RequestStack.php b/RequestStack.php index 3d9cfd0c6..244a77d63 100644 --- a/RequestStack.php +++ b/RequestStack.php @@ -21,7 +21,7 @@ class RequestStack /** * @var Request[] */ - private $requests = array(); + private $requests = []; /** * Pushes a Request on the stack. @@ -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]; @@ -92,10 +92,10 @@ public function getMasterRequest() */ public function getParentRequest() { - $pos = count($this->requests) - 2; + $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 4af1e0bae..26e3a3378 100644 --- a/Response.php +++ b/Response.php @@ -21,6 +21,7 @@ class Response const HTTP_CONTINUE = 100; const HTTP_SWITCHING_PROTOCOLS = 101; const HTTP_PROCESSING = 102; // RFC2518 + const HTTP_EARLY_HINTS = 103; // RFC8297 const HTTP_OK = 200; const HTTP_CREATED = 201; const HTTP_ACCEPTED = 202; @@ -63,7 +64,12 @@ class Response const HTTP_UNPROCESSABLE_ENTITY = 422; // RFC4918 const HTTP_LOCKED = 423; // RFC4918 const HTTP_FAILED_DEPENDENCY = 424; // RFC4918 + + /** + * @deprecated + */ const HTTP_RESERVED_FOR_WEBDAV_ADVANCED_COLLECTIONS_EXPIRED_PROPOSAL = 425; // RFC2817 + const HTTP_TOO_EARLY = 425; // RFC-ietf-httpbis-replay-04 const HTTP_UPGRADE_REQUIRED = 426; // RFC2817 const HTTP_PRECONDITION_REQUIRED = 428; // RFC6585 const HTTP_TOO_MANY_REQUESTS = 429; // RFC6585 @@ -82,7 +88,7 @@ class Response const HTTP_NETWORK_AUTHENTICATION_REQUIRED = 511; // RFC6585 /** - * @var \Symfony\Component\HttpFoundation\ResponseHeaderBag + * @var ResponseHeaderBag */ public $headers; @@ -115,17 +121,18 @@ 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. * * @var array */ - public static $statusTexts = array( + public static $statusTexts = [ 100 => 'Continue', 101 => 'Switching Protocols', 102 => 'Processing', // RFC2518 + 103 => 'Early Hints', 200 => 'OK', 201 => 'Created', 202 => 'Accepted', @@ -167,7 +174,7 @@ class Response 422 => 'Unprocessable Entity', // RFC4918 423 => 'Locked', // RFC4918 424 => 'Failed Dependency', // RFC4918 - 425 => 'Reserved for WebDAV advanced collections expired proposal', // RFC2817 + 425 => 'Too Early', // RFC-ietf-httpbis-replay-04 426 => 'Upgrade Required', // RFC2817 428 => 'Precondition Required', // RFC6585 429 => 'Too Many Requests', // RFC6585 @@ -184,28 +191,21 @@ class Response 508 => 'Loop Detected', // RFC5842 510 => 'Not Extended', // RFC2774 511 => 'Network Authentication Required', // RFC6585 - ); + ]; /** - * Constructor. - * * @param mixed $content The response content, see setContent() * @param int $status The response status code * @param array $headers An array of response headers * * @throws \InvalidArgumentException When the HTTP status code is not valid */ - public function __construct($content = '', $status = 200, $headers = array()) + public function __construct($content = '', $status = 200, $headers = []) { $this->headers = new ResponseHeaderBag($headers); $this->setContent($content); $this->setStatusCode($status); $this->setProtocolVersion('1.0'); - - /* RFC2616 - 14.18 says all Responses need to have a Date */ - if (!$this->headers->has('Date')) { - $this->setDate(\DateTime::createFromFormat('U', time())); - } } /** @@ -222,7 +222,7 @@ public function __construct($content = '', $status = 200, $headers = array()) * * @return static */ - public static function create($content = '', $status = 200, $headers = array()) + public static function create($content = '', $status = 200, $headers = []) { return new static($content, $status, $headers); } @@ -261,8 +261,6 @@ public function __clone() * compliant with RFC 2616. Most of the changes are based on * the Request that is "associated" with this Response. * - * @param Request $request A Request instance - * * @return $this */ public function prepare(Request $request) @@ -312,9 +310,9 @@ public function prepare(Request $request) } // Check if we need to send extra expire info headers - if ('1.0' == $this->getProtocolVersion() && false !== strpos($this->headers->get('Cache-Control'), 'no-cache')) { - $this->headers->set('pragma', 'no-cache'); - $this->headers->set('expires', -1); + if ('1.0' == $this->getProtocolVersion() && false !== strpos($headers->get('Cache-Control'), 'no-cache')) { + $headers->set('pragma', 'no-cache'); + $headers->set('expires', -1); } $this->ensureIEOverSSLCompatibility($request); @@ -334,30 +332,22 @@ public function sendHeaders() return $this; } - /* RFC2616 - 14.18 says all Responses need to have a Date */ - if (!$this->headers->has('Date')) { - $this->setDate(\DateTime::createFromFormat('U', time())); - } - // headers foreach ($this->headers->allPreserveCaseWithoutCookies() as $name => $values) { + $replace = 0 === strcasecmp($name, 'Content-Type'); foreach ($values as $value) { - header($name.': '.$value, false, $this->statusCode); + header($name.': '.$value, $replace, $this->statusCode); } } - // status - header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode); - // cookies foreach ($this->headers->getCookies() as $cookie) { - if ($cookie->isRaw()) { - setrawcookie($cookie->getName(), $cookie->getValue(), $cookie->getExpiresTime(), $cookie->getPath(), $cookie->getDomain(), $cookie->isSecure(), $cookie->isHttpOnly()); - } else { - setcookie($cookie->getName(), $cookie->getValue(), $cookie->getExpiresTime(), $cookie->getPath(), $cookie->getDomain(), $cookie->isSecure(), $cookie->isHttpOnly()); - } + header('Set-Cookie: '.$cookie, false, $this->statusCode); } + // status + header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode); + return $this; } @@ -383,9 +373,9 @@ public function send() $this->sendHeaders(); $this->sendContent(); - if (function_exists('fastcgi_finish_request')) { + if (\function_exists('fastcgi_finish_request')) { fastcgi_finish_request(); - } elseif ('cli' !== PHP_SAPI) { + } elseif (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) { static::closeOutputBuffers(0, true); } @@ -405,8 +395,8 @@ public function send() */ public function setContent($content) { - if (null !== $content && !is_string($content) && !is_numeric($content) && !is_callable(array($content, '__toString'))) { - throw new \UnexpectedValueException(sprintf('The Response content must be a string or object implementing __toString(), "%s" given.', gettype($content))); + if (null !== $content && !\is_string($content) && !is_numeric($content) && !\is_callable([$content, '__toString'])) { + throw new \UnexpectedValueException(sprintf('The Response content must be a string or object implementing __toString(), "%s" given.', \gettype($content))); } $this->content = (string) $content; @@ -417,7 +407,7 @@ public function setContent($content) /** * Gets the current response content. * - * @return string Content + * @return string|false */ public function getContent() { @@ -455,12 +445,12 @@ public function getProtocolVersion() /** * Sets the response status code. * - * @param int $code HTTP status code - * @param mixed $text HTTP status text - * * If the status text is null it will be automatically populated for the known * status codes and left empty otherwise. * + * @param int $code HTTP status code + * @param mixed $text HTTP status text + * * @return $this * * @throws \InvalidArgumentException When the HTTP status code is not valid @@ -532,13 +522,19 @@ public function getCharset() } /** - * Returns true if the response is worth caching under any circumstance. + * Returns true if the response may safely be kept in a shared (surrogate) cache. * * Responses marked "private" with an explicit Cache-Control directive are * considered uncacheable. * * Responses with neither a freshness lifetime (Expires, max-age) nor cache - * validator (Last-Modified, ETag) are considered uncacheable. + * validator (Last-Modified, ETag) are considered uncacheable because there is + * no way to tell when or how to remove them from the cache. + * + * Note that RFC 7231 and RFC 7234 possibly allow for a more permissive implementation, + * for example "status codes that are defined as cacheable by default [...] + * can be reused by a cache with heuristic expiration unless otherwise indicated" + * (https://tools.ietf.org/html/rfc7231#section-6.1) * * @return bool true if the response is worth caching, false otherwise * @@ -546,7 +542,7 @@ public function getCharset() */ public function isCacheable() { - if (!in_array($this->statusCode, array(200, 203, 300, 301, 302, 404, 410))) { + if (!\in_array($this->statusCode, [200, 203, 300, 301, 302, 404, 410])) { return false; } @@ -620,6 +616,38 @@ public function setPublic() return $this; } + /** + * Marks the response as "immutable". + * + * @param bool $immutable enables or disables the immutable directive + * + * @return $this + * + * @final + */ + public function setImmutable($immutable = true) + { + if ($immutable) { + $this->headers->addCacheControlDirective('immutable'); + } else { + $this->headers->removeCacheControlDirective('immutable'); + } + + return $this; + } + + /** + * Returns true if the response is marked as "immutable". + * + * @return bool returns true if the response is marked as "immutable"; otherwise false + * + * @final + */ + public function isImmutable() + { + return $this->headers->hasCacheControlDirective('immutable'); + } + /** * Returns true if the response must be revalidated by caches. * @@ -648,23 +676,12 @@ public function mustRevalidate() */ public function getDate() { - /* - RFC2616 - 14.18 says all Responses need to have a Date. - Make sure we provide one even if it the header - has been removed in the meantime. - */ - if (!$this->headers->has('Date')) { - $this->setDate(\DateTime::createFromFormat('U', time())); - } - return $this->headers->getDate('Date'); } /** * Sets the Date header. * - * @param \DateTime $date A \DateTime instance - * * @return $this * * @final since version 3.2 @@ -690,7 +707,7 @@ public function getAge() return (int) $age; } - return max(time() - $this->getDate()->format('U'), 0); + return max(time() - (int) $this->getDate()->format('U'), 0); } /** @@ -702,6 +719,7 @@ public function expire() { if ($this->isFresh()) { $this->headers->set('Age', $this->getMaxAge()); + $this->headers->remove('Expires'); } return $this; @@ -770,8 +788,10 @@ public function getMaxAge() } if (null !== $this->getExpires()) { - return $this->getExpires()->format('U') - $this->getDate()->format('U'); + return (int) $this->getExpires()->format('U') - (int) $this->getDate()->format('U'); } + + return null; } /** @@ -828,6 +848,8 @@ public function getTtl() if (null !== $maxAge = $this->getMaxAge()) { return $maxAge - $this->getAge(); } + + return null; } /** @@ -944,7 +966,7 @@ public function setEtag($etag = null, $weak = false) /** * Sets the response's cache headers (validation and/or expiration). * - * Available options are: etag, last_modified, max_age, s_maxage, private, and public. + * Available options are: etag, last_modified, max_age, s_maxage, private, public and immutable. * * @param array $options An array of cache options * @@ -956,8 +978,8 @@ public function setEtag($etag = null, $weak = false) */ public function setCache(array $options) { - if ($diff = array_diff(array_keys($options), array('etag', 'last_modified', 'max_age', 's_maxage', 'private', 'public'))) { - throw new \InvalidArgumentException(sprintf('Response does not support the following options: "%s".', implode('", "', array_values($diff)))); + if ($diff = array_diff(array_keys($options), ['etag', 'last_modified', 'max_age', 's_maxage', 'private', 'public', 'immutable'])) { + throw new \InvalidArgumentException(sprintf('Response does not support the following options: "%s".', implode('", "', $diff))); } if (isset($options['etag'])) { @@ -992,6 +1014,10 @@ public function setCache(array $options) } } + if (isset($options['immutable'])) { + $this->setImmutable((bool) $options['immutable']); + } + return $this; } @@ -1003,7 +1029,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 since version 3.3 */ @@ -1013,7 +1039,7 @@ public function setNotModified() $this->setContent(null); // remove headers that MUST NOT be included with 304 Not Modified responses - foreach (array('Allow', 'Content-Encoding', 'Content-Language', 'Content-Length', 'Content-MD5', 'Content-Type', 'Last-Modified') as $header) { + foreach (['Allow', 'Content-Encoding', 'Content-Language', 'Content-Length', 'Content-MD5', 'Content-Type', 'Last-Modified'] as $header) { $this->headers->remove($header); } @@ -1042,10 +1068,10 @@ public function hasVary() public function getVary() { if (!$vary = $this->headers->get('Vary', null, false)) { - return array(); + return []; } - $ret = array(); + $ret = []; foreach ($vary as $item) { $ret = array_merge($ret, preg_split('/[\s,]+/', $item)); } @@ -1077,8 +1103,6 @@ public function setVary($headers, $replace = true) * If the Response is not modified, it sets the status code to 304 and * removes the actual content by calling the setNotModified() method. * - * @param Request $request A Request instance - * * @return bool true if the Response validators match the Request, false otherwise * * @final since version 3.3 @@ -1094,7 +1118,7 @@ public function isNotModified(Request $request) $modifiedSince = $request->headers->get('If-Modified-Since'); if ($etags = $request->getETags()) { - $notModified = in_array($this->getEtag(), $etags) || in_array('*', $etags); + $notModified = \in_array($this->getEtag(), $etags) || \in_array('*', $etags); } if ($modifiedSince && $lastModified) { @@ -1113,7 +1137,7 @@ public function isNotModified(Request $request) * * @return bool * - * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html + * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html * * @final since version 3.2 */ @@ -1229,7 +1253,7 @@ public function isNotFound() */ public function isRedirect($location = null) { - return in_array($this->statusCode, array(201, 301, 302, 303, 307, 308)) && (null === $location ?: $location == $this->headers->get('Location')); + return \in_array($this->statusCode, [201, 301, 302, 303, 307, 308]) && (null === $location ?: $location == $this->headers->get('Location')); } /** @@ -1241,7 +1265,7 @@ public function isRedirect($location = null) */ public function isEmpty() { - return in_array($this->statusCode, array(204, 304)); + return \in_array($this->statusCode, [204, 304]); } /** @@ -1257,11 +1281,11 @@ public function isEmpty() public static function closeOutputBuffers($targetLevel, $flush) { $status = ob_get_status(true); - $level = count($status); + $level = \count($status); // PHP_OUTPUT_HANDLER_* are not defined on HHVM 3.3 - $flags = defined('PHP_OUTPUT_HANDLER_REMOVABLE') ? PHP_OUTPUT_HANDLER_REMOVABLE | ($flush ? PHP_OUTPUT_HANDLER_FLUSHABLE : PHP_OUTPUT_HANDLER_CLEANABLE) : -1; + $flags = \defined('PHP_OUTPUT_HANDLER_REMOVABLE') ? PHP_OUTPUT_HANDLER_REMOVABLE | ($flush ? PHP_OUTPUT_HANDLER_FLUSHABLE : PHP_OUTPUT_HANDLER_CLEANABLE) : -1; - while ($level-- > $targetLevel && ($s = $status[$level]) && (!isset($s['del']) ? !isset($s['flags']) || $flags === ($s['flags'] & $flags) : $s['del'])) { + while ($level-- > $targetLevel && ($s = $status[$level]) && (!isset($s['del']) ? !isset($s['flags']) || ($s['flags'] & $flags) === $flags : $s['del'])) { if ($flush) { ob_end_flush(); } else { @@ -1279,7 +1303,7 @@ public static function closeOutputBuffers($targetLevel, $flush) */ protected function ensureIEOverSSLCompatibility(Request $request) { - if (false !== stripos($this->headers->get('Content-Disposition'), 'attachment') && preg_match('/MSIE (.*?);/i', $request->server->get('HTTP_USER_AGENT'), $match) == 1 && true === $request->isSecure()) { + if (false !== stripos($this->headers->get('Content-Disposition'), 'attachment') && 1 == preg_match('/MSIE (.*?);/i', $request->server->get('HTTP_USER_AGENT'), $match) && true === $request->isSecure()) { if ((int) preg_replace('/(MSIE )(.*?);/', '$2', $match[0]) < 9) { $this->headers->remove('Cache-Control'); } diff --git a/ResponseHeaderBag.php b/ResponseHeaderBag.php index df2931be0..1dc8dc2c5 100644 --- a/ResponseHeaderBag.php +++ b/ResponseHeaderBag.php @@ -24,33 +24,22 @@ class ResponseHeaderBag extends HeaderBag const DISPOSITION_ATTACHMENT = 'attachment'; const DISPOSITION_INLINE = 'inline'; - /** - * @var array - */ - protected $computedCacheControl = array(); - - /** - * @var array - */ - protected $cookies = array(); - - /** - * @var array - */ - protected $headerNames = array(); + protected $computedCacheControl = []; + protected $cookies = []; + protected $headerNames = []; - /** - * Constructor. - * - * @param array $headers An array of HTTP headers - */ - public function __construct(array $headers = array()) + public function __construct(array $headers = []) { parent::__construct($headers); if (!isset($this->headers['cache-control'])) { $this->set('Cache-Control', ''); } + + /* RFC2616 - 14.18 says all Responses need to have a Date */ + if (!isset($this->headers['date'])) { + $this->initDate(); + } } /** @@ -60,7 +49,7 @@ public function __construct(array $headers = array()) */ public function allPreserveCase() { - $headers = array(); + $headers = []; foreach ($this->all() as $name => $value) { $headers[isset($this->headerNames[$name]) ? $this->headerNames[$name] : $name] = $value; } @@ -81,15 +70,19 @@ public function allPreserveCaseWithoutCookies() /** * {@inheritdoc} */ - public function replace(array $headers = array()) + public function replace(array $headers = []) { - $this->headerNames = array(); + $this->headerNames = []; parent::replace($headers); if (!isset($this->headers['cache-control'])) { $this->set('Cache-Control', ''); } + + if (!isset($this->headers['date'])) { + $this->initDate(); + } } /** @@ -114,7 +107,7 @@ public function set($key, $values, $replace = true) if ('set-cookie' === $uniqueKey) { if ($replace) { - $this->cookies = array(); + $this->cookies = []; } foreach ((array) $values as $cookie) { $this->setCookie(Cookie::fromString($cookie)); @@ -129,9 +122,9 @@ public function set($key, $values, $replace = true) parent::set($key, $values, $replace); // ensure the cache-control header has sensible defaults - if (in_array($uniqueKey, array('cache-control', 'etag', 'last-modified', 'expires'))) { + if (\in_array($uniqueKey, ['cache-control', 'etag', 'last-modified', 'expires'], true)) { $computed = $this->computeCacheControlValue(); - $this->headers['cache-control'] = array($computed); + $this->headers['cache-control'] = [$computed]; $this->headerNames['cache-control'] = 'Cache-Control'; $this->computedCacheControl = $this->parseCacheControl($computed); } @@ -146,7 +139,7 @@ public function remove($key) unset($this->headerNames[$uniqueKey]); if ('set-cookie' === $uniqueKey) { - $this->cookies = array(); + $this->cookies = []; return; } @@ -154,7 +147,11 @@ public function remove($key) parent::remove($key); if ('cache-control' === $uniqueKey) { - $this->computedCacheControl = array(); + $this->computedCacheControl = []; + } + + if ('date' === $uniqueKey) { + $this->initDate(); } } @@ -163,7 +160,7 @@ public function remove($key) */ public function hasCacheControlDirective($key) { - return array_key_exists($key, $this->computedCacheControl); + return \array_key_exists($key, $this->computedCacheControl); } /** @@ -171,14 +168,9 @@ public function hasCacheControlDirective($key) */ public function getCacheControlDirective($key) { - return array_key_exists($key, $this->computedCacheControl) ? $this->computedCacheControl[$key] : null; + return \array_key_exists($key, $this->computedCacheControl) ? $this->computedCacheControl[$key] : null; } - /** - * Sets a cookie. - * - * @param Cookie $cookie - */ public function setCookie(Cookie $cookie) { $this->cookies[$cookie->getDomain()][$cookie->getPath()][$cookie->getName()] = $cookie; @@ -218,21 +210,21 @@ public function removeCookie($name, $path = '/', $domain = null) * * @param string $format * - * @return array + * @return Cookie[] * * @throws \InvalidArgumentException When the $format is invalid */ public function getCookies($format = self::COOKIES_FLAT) { - if (!in_array($format, array(self::COOKIES_FLAT, self::COOKIES_ARRAY))) { - throw new \InvalidArgumentException(sprintf('Format "%s" invalid (%s).', $format, implode(', ', array(self::COOKIES_FLAT, self::COOKIES_ARRAY)))); + if (!\in_array($format, [self::COOKIES_FLAT, self::COOKIES_ARRAY])) { + throw new \InvalidArgumentException(sprintf('Format "%s" invalid (%s).', $format, implode(', ', [self::COOKIES_FLAT, self::COOKIES_ARRAY]))); } if (self::COOKIES_ARRAY === $format) { return $this->cookies; } - $flattenedCookies = array(); + $flattenedCookies = []; foreach ($this->cookies as $path) { foreach ($path as $cookies) { foreach ($cookies as $cookie) { @@ -275,7 +267,7 @@ public function clearCookie($name, $path = '/', $domain = null, $secure = false, */ public function makeDisposition($disposition, $filename, $filenameFallback = '') { - if (!in_array($disposition, array(self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE))) { + if (!\in_array($disposition, [self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE])) { throw new \InvalidArgumentException(sprintf('The disposition must be either "%s" or "%s".', self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE)); } @@ -338,4 +330,11 @@ protected function computeCacheControlValue() return $header; } + + private function initDate() + { + $now = \DateTime::createFromFormat('U', time()); + $now->setTimezone(new \DateTimeZone('UTC')); + $this->set('Date', $now->format('D, d M Y H:i:s').' GMT'); + } } diff --git a/ServerBag.php b/ServerBag.php index 0d38c08ac..f3b640234 100644 --- a/ServerBag.php +++ b/ServerBag.php @@ -27,8 +27,8 @@ class ServerBag extends ParameterBag */ public function getHeaders() { - $headers = array(); - $contentHeaders = array('CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true); + $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; @@ -46,13 +46,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] */ @@ -68,7 +68,7 @@ public function getHeaders() if (0 === stripos($authorizationHeader, 'basic ')) { // Decode AUTHORIZATION header into PHP_AUTH_USER and PHP_AUTH_PW when authorization header is basic $exploded = explode(':', base64_decode(substr($authorizationHeader, 6)), 2); - if (count($exploded) == 2) { + if (2 == \count($exploded)) { list($headers['PHP_AUTH_USER'], $headers['PHP_AUTH_PW']) = $exploded; } } elseif (empty($this->parameters['PHP_AUTH_DIGEST']) && (0 === stripos($authorizationHeader, 'digest '))) { @@ -79,7 +79,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/AttributeBag.php b/Session/Attribute/AttributeBag.php index af292e37a..07118e891 100644 --- a/Session/Attribute/AttributeBag.php +++ b/Session/Attribute/AttributeBag.php @@ -17,20 +17,11 @@ class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Countable { private $name = 'attributes'; - - /** - * @var string - */ private $storageKey; - /** - * @var array - */ - protected $attributes = array(); + protected $attributes = []; /** - * Constructor. - * * @param string $storageKey The key used to store attributes in the session */ public function __construct($storageKey = '_sf2_attributes') @@ -72,7 +63,7 @@ public function getStorageKey() */ public function has($name) { - return array_key_exists($name, $this->attributes); + return \array_key_exists($name, $this->attributes); } /** @@ -80,7 +71,7 @@ public function has($name) */ public function get($name, $default = null) { - return array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default; + return \array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default; } /** @@ -104,7 +95,7 @@ public function all() */ public function replace(array $attributes) { - $this->attributes = array(); + $this->attributes = []; foreach ($attributes as $key => $value) { $this->set($key, $value); } @@ -116,7 +107,7 @@ public function replace(array $attributes) public function remove($name) { $retval = null; - if (array_key_exists($name, $this->attributes)) { + if (\array_key_exists($name, $this->attributes)) { $retval = $this->attributes[$name]; unset($this->attributes[$name]); } @@ -130,7 +121,7 @@ public function remove($name) public function clear() { $return = $this->attributes; - $this->attributes = array(); + $this->attributes = []; return $return; } @@ -152,6 +143,6 @@ public function getIterator() */ public function count() { - return count($this->attributes); + return \count($this->attributes); } } diff --git a/Session/Attribute/NamespacedAttributeBag.php b/Session/Attribute/NamespacedAttributeBag.php index d797a6f23..07885e7fb 100644 --- a/Session/Attribute/NamespacedAttributeBag.php +++ b/Session/Attribute/NamespacedAttributeBag.php @@ -19,16 +19,9 @@ */ class NamespacedAttributeBag extends AttributeBag { - /** - * Namespace character. - * - * @var string - */ private $namespaceCharacter; /** - * Constructor. - * * @param string $storageKey Session storage key * @param string $namespaceCharacter Namespace character to use in keys */ @@ -51,7 +44,7 @@ public function has($name) return false; } - return array_key_exists($name, $attributes); + return \array_key_exists($name, $attributes); } /** @@ -67,7 +60,7 @@ public function get($name, $default = null) return $default; } - return array_key_exists($name, $attributes) ? $attributes[$name] : $default; + return \array_key_exists($name, $attributes) ? $attributes[$name] : $default; } /** @@ -88,7 +81,7 @@ public function remove($name) $retval = null; $attributes = &$this->resolveAttributePath($name); $name = $this->resolveKey($name); - if (null !== $attributes && array_key_exists($name, $attributes)) { + if (null !== $attributes && \array_key_exists($name, $attributes)) { $retval = $attributes[$name]; unset($attributes[$name]); } @@ -104,12 +97,12 @@ 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) { $array = &$this->attributes; - $name = (strpos($name, $this->namespaceCharacter) === 0) ? substr($name, 1) : $name; + $name = (0 === strpos($name, $this->namespaceCharacter)) ? substr($name, 1) : $name; // Check if there is anything to do, else return if (!$name) { @@ -117,21 +110,27 @@ protected function &resolveAttributePath($name, $writeContext = false) } $parts = explode($this->namespaceCharacter, $name); - if (count($parts) < 2) { + if (\count($parts) < 2) { if (!$writeContext) { return $array; } - $array[$parts[0]] = array(); + $array[$parts[0]] = []; return $array; } - unset($parts[count($parts) - 1]); + unset($parts[\count($parts) - 1]); foreach ($parts as $part) { - if (null !== $array && !array_key_exists($part, $array)) { - $array[$part] = $writeContext ? array() : null; + if (null !== $array && !\array_key_exists($part, $array)) { + if (!$writeContext) { + $null = null; + + return $null; + } + + $array[$part] = []; } $array = &$array[$part]; diff --git a/Session/Flash/AutoExpireFlashBag.php b/Session/Flash/AutoExpireFlashBag.php index ddd603fdd..451c4a5a1 100644 --- a/Session/Flash/AutoExpireFlashBag.php +++ b/Session/Flash/AutoExpireFlashBag.php @@ -19,27 +19,13 @@ class AutoExpireFlashBag implements FlashBagInterface { private $name = 'flashes'; - - /** - * Flash messages. - * - * @var array - */ - private $flashes = array('display' => array(), 'new' => array()); - - /** - * The storage key for flashes in the session. - * - * @var string - */ + private $flashes = ['display' => [], 'new' => []]; private $storageKey; /** - * Constructor. - * * @param string $storageKey The key used to store flashes in the session */ - public function __construct($storageKey = '_sf2_flashes') + public function __construct($storageKey = '_symfony_flashes') { $this->storageKey = $storageKey; } @@ -67,8 +53,8 @@ public function initialize(array &$flashes) // The logic: messages from the last request will be stored in new, so we move them to previous // This request we will show what is in 'display'. What is placed into 'new' this time round will // be moved to display next time round. - $this->flashes['display'] = array_key_exists('new', $this->flashes) ? $this->flashes['new'] : array(); - $this->flashes['new'] = array(); + $this->flashes['display'] = \array_key_exists('new', $this->flashes) ? $this->flashes['new'] : []; + $this->flashes['new'] = []; } /** @@ -82,7 +68,7 @@ public function add($type, $message) /** * {@inheritdoc} */ - public function peek($type, array $default = array()) + public function peek($type, array $default = []) { return $this->has($type) ? $this->flashes['display'][$type] : $default; } @@ -92,13 +78,13 @@ public function peek($type, array $default = array()) */ public function peekAll() { - return array_key_exists('display', $this->flashes) ? (array) $this->flashes['display'] : array(); + return \array_key_exists('display', $this->flashes) ? (array) $this->flashes['display'] : []; } /** * {@inheritdoc} */ - public function get($type, array $default = array()) + public function get($type, array $default = []) { $return = $default; @@ -120,7 +106,7 @@ public function get($type, array $default = array()) public function all() { $return = $this->flashes['display']; - $this->flashes = array('new' => array(), 'display' => array()); + $this->flashes['display'] = []; return $return; } @@ -146,7 +132,7 @@ public function set($type, $messages) */ public function has($type) { - return array_key_exists($type, $this->flashes['display']) && $this->flashes['display'][$type]; + return \array_key_exists($type, $this->flashes['display']) && $this->flashes['display'][$type]; } /** diff --git a/Session/Flash/FlashBag.php b/Session/Flash/FlashBag.php index 85b4f00b0..f5d984af0 100644 --- a/Session/Flash/FlashBag.php +++ b/Session/Flash/FlashBag.php @@ -19,27 +19,13 @@ class FlashBag implements FlashBagInterface { private $name = 'flashes'; - - /** - * Flash messages. - * - * @var array - */ - private $flashes = array(); - - /** - * The storage key for flashes in the session. - * - * @var string - */ + private $flashes = []; private $storageKey; /** - * Constructor. - * * @param string $storageKey The key used to store flashes in the session */ - public function __construct($storageKey = '_sf2_flashes') + public function __construct($storageKey = '_symfony_flashes') { $this->storageKey = $storageKey; } @@ -76,7 +62,7 @@ public function add($type, $message) /** * {@inheritdoc} */ - public function peek($type, array $default = array()) + public function peek($type, array $default = []) { return $this->has($type) ? $this->flashes[$type] : $default; } @@ -92,7 +78,7 @@ public function peekAll() /** * {@inheritdoc} */ - public function get($type, array $default = array()) + public function get($type, array $default = []) { if (!$this->has($type)) { return $default; @@ -111,7 +97,7 @@ public function get($type, array $default = array()) public function all() { $return = $this->peekAll(); - $this->flashes = array(); + $this->flashes = []; return $return; } @@ -137,7 +123,7 @@ public function setAll(array $messages) */ public function has($type) { - return array_key_exists($type, $this->flashes) && $this->flashes[$type]; + return \array_key_exists($type, $this->flashes) && $this->flashes[$type]; } /** diff --git a/Session/Flash/FlashBagInterface.php b/Session/Flash/FlashBagInterface.php index 25f3d57b5..99e807421 100644 --- a/Session/Flash/FlashBagInterface.php +++ b/Session/Flash/FlashBagInterface.php @@ -21,20 +21,20 @@ interface FlashBagInterface extends SessionBagInterface { /** - * Adds a flash message for type. + * Adds a flash message for the given type. * * @param string $type - * @param string $message + * @param mixed $message */ public function add($type, $message); /** - * Registers a message for a given type. + * Registers one or more messages for a given type. * * @param string $type - * @param string|array $message + * @param string|array $messages */ - public function set($type, $message); + public function set($type, $messages); /** * Gets flash messages for a given type. @@ -44,7 +44,7 @@ public function set($type, $message); * * @return array */ - public function peek($type, array $default = array()); + public function peek($type, array $default = []); /** * Gets all flash messages. @@ -61,7 +61,7 @@ public function peekAll(); * * @return array */ - public function get($type, array $default = array()); + public function get($type, array $default = []); /** * Gets and clears flashes from the stack. @@ -72,8 +72,6 @@ public function all(); /** * Sets all flash messages. - * - * @param array $messages */ public function setAll(array $messages); diff --git a/Session/Session.php b/Session/Session.php index 70bcf3e09..db0b9aeb0 100644 --- a/Session/Session.php +++ b/Session/Session.php @@ -11,41 +11,27 @@ namespace Symfony\Component\HttpFoundation\Session; -use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface; use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag; use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface; use Symfony\Component\HttpFoundation\Session\Flash\FlashBag; use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface; use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; +use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface; /** - * Session. - * * @author Fabien Potencier * @author Drak */ class Session implements SessionInterface, \IteratorAggregate, \Countable { - /** - * Storage driver. - * - * @var SessionStorageInterface - */ protected $storage; - /** - * @var string - */ private $flashName; - - /** - * @var string - */ private $attributeName; + private $data = []; + private $usageIndex = 0; /** - * Constructor. - * * @param SessionStorageInterface $storage A SessionStorageInterface instance * @param AttributeBagInterface $attributes An AttributeBagInterface instance, (defaults null for default AttributeBag) * @param FlashBagInterface $flashes A FlashBagInterface instance (defaults null for default FlashBag) @@ -124,7 +110,7 @@ public function remove($name) */ public function clear() { - $this->storage->getBag($this->attributeName)->clear(); + $this->getAttributeBag()->clear(); } /** @@ -152,7 +138,36 @@ public function getIterator() */ public function count() { - return count($this->getAttributeBag()->all()); + return \count($this->getAttributeBag()->all()); + } + + /** + * @return int + * + * @internal + */ + public function getUsageIndex() + { + return $this->usageIndex; + } + + /** + * @return bool + * + * @internal + */ + public function isEmpty() + { + if ($this->isStarted()) { + ++$this->usageIndex; + } + foreach ($this->data as &$data) { + if (!empty($data)) { + return false; + } + } + + return true; } /** @@ -194,7 +209,9 @@ public function getId() */ public function setId($id) { - $this->storage->setId($id); + if ($this->storage->getId() !== $id) { + $this->storage->setId($id); + } } /** @@ -218,6 +235,8 @@ public function setName($name) */ public function getMetadataBag() { + ++$this->usageIndex; + return $this->storage->getMetadataBag(); } @@ -226,7 +245,7 @@ public function getMetadataBag() */ public function registerBag(SessionBagInterface $bag) { - $this->storage->registerBag($bag); + $this->storage->registerBag(new SessionBagProxy($bag, $this->data, $this->usageIndex)); } /** @@ -234,7 +253,9 @@ public function registerBag(SessionBagInterface $bag) */ public function getBag($name) { - return $this->storage->getBag($name); + $bag = $this->storage->getBag($name); + + return method_exists($bag, 'getBag') ? $bag->getBag() : $bag; } /** @@ -256,6 +277,6 @@ public function getFlashBag() */ private function getAttributeBag() { - return $this->storage->getBag($this->attributeName); + return $this->getBag($this->attributeName); } } diff --git a/Session/SessionBagInterface.php b/Session/SessionBagInterface.php index aca18aacb..8e37d06d6 100644 --- a/Session/SessionBagInterface.php +++ b/Session/SessionBagInterface.php @@ -27,8 +27,6 @@ public function getName(); /** * Initializes the Bag. - * - * @param array $array */ public function initialize(array &$array); diff --git a/Session/SessionBagProxy.php b/Session/SessionBagProxy.php new file mode 100644 index 000000000..3504bdfe7 --- /dev/null +++ b/Session/SessionBagProxy.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session; + +/** + * @author Nicolas Grekas + * + * @internal + */ +final class SessionBagProxy implements SessionBagInterface +{ + private $bag; + private $data; + private $usageIndex; + + public function __construct(SessionBagInterface $bag, array &$data, &$usageIndex) + { + $this->bag = $bag; + $this->data = &$data; + $this->usageIndex = &$usageIndex; + } + + /** + * @return SessionBagInterface + */ + public function getBag() + { + ++$this->usageIndex; + + return $this->bag; + } + + /** + * @return bool + */ + public function isEmpty() + { + if (!isset($this->data[$this->bag->getStorageKey()])) { + return true; + } + ++$this->usageIndex; + + return empty($this->data[$this->bag->getStorageKey()]); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return $this->bag->getName(); + } + + /** + * {@inheritdoc} + */ + public function initialize(array &$array) + { + ++$this->usageIndex; + $this->data[$this->bag->getStorageKey()] = &$array; + + $this->bag->initialize($array); + } + + /** + * {@inheritdoc} + */ + public function getStorageKey() + { + return $this->bag->getStorageKey(); + } + + /** + * {@inheritdoc} + */ + public function clear() + { + return $this->bag->clear(); + } +} diff --git a/Session/SessionInterface.php b/Session/SessionInterface.php index d3fcd2eec..95fca857e 100644 --- a/Session/SessionInterface.php +++ b/Session/SessionInterface.php @@ -25,7 +25,7 @@ interface SessionInterface * * @return bool True if session started * - * @throws \RuntimeException If session fails to start. + * @throws \RuntimeException if session fails to start */ public function start(); @@ -159,8 +159,6 @@ public function isStarted(); /** * Registers a SessionBagInterface with the session. - * - * @param SessionBagInterface $bag */ public function registerBag(SessionBagInterface $bag); diff --git a/Session/Storage/Handler/AbstractSessionHandler.php b/Session/Storage/Handler/AbstractSessionHandler.php new file mode 100644 index 000000000..eb09c0b54 --- /dev/null +++ b/Session/Storage/Handler/AbstractSessionHandler.php @@ -0,0 +1,168 @@ + + * + * 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; + +/** + * This abstract session handler provides a generic implementation + * of the PHP 7.0 SessionUpdateTimestampHandlerInterface, + * enabling strict and lazy session handling. + * + * @author Nicolas Grekas + */ +abstract class AbstractSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface +{ + private $sessionName; + private $prefetchId; + private $prefetchData; + private $newSessionId; + private $igbinaryEmptyData; + + /** + * {@inheritdoc} + */ + public function open($savePath, $sessionName) + { + $this->sessionName = $sessionName; + if (!headers_sent() && !ini_get('session.cache_limiter') && '0' !== ini_get('session.cache_limiter')) { + header(sprintf('Cache-Control: max-age=%d, private, must-revalidate', 60 * (int) ini_get('session.cache_expire'))); + } + + return true; + } + + /** + * @param string $sessionId + * + * @return string + */ + abstract protected function doRead($sessionId); + + /** + * @param string $sessionId + * @param string $data + * + * @return bool + */ + abstract protected function doWrite($sessionId, $data); + + /** + * @param string $sessionId + * + * @return bool + */ + abstract protected function doDestroy($sessionId); + + /** + * {@inheritdoc} + */ + public function validateId($sessionId) + { + $this->prefetchData = $this->read($sessionId); + $this->prefetchId = $sessionId; + + return '' !== $this->prefetchData; + } + + /** + * {@inheritdoc} + */ + public function read($sessionId) + { + if (null !== $this->prefetchId) { + $prefetchId = $this->prefetchId; + $prefetchData = $this->prefetchData; + $this->prefetchId = $this->prefetchData = null; + + if ($prefetchId === $sessionId || '' === $prefetchData) { + $this->newSessionId = '' === $prefetchData ? $sessionId : null; + + return $prefetchData; + } + } + + $data = $this->doRead($sessionId); + $this->newSessionId = '' === $data ? $sessionId : null; + if (\PHP_VERSION_ID < 70000) { + $this->prefetchData = $data; + } + + return $data; + } + + /** + * {@inheritdoc} + */ + public function write($sessionId, $data) + { + if (\PHP_VERSION_ID < 70000 && $this->prefetchData) { + $readData = $this->prefetchData; + $this->prefetchData = null; + + if ($readData === $data) { + return $this->updateTimestamp($sessionId, $data); + } + } + if (null === $this->igbinaryEmptyData) { + // see https://github.com/igbinary/igbinary/issues/146 + $this->igbinaryEmptyData = \function_exists('igbinary_serialize') ? igbinary_serialize([]) : ''; + } + if ('' === $data || $this->igbinaryEmptyData === $data) { + return $this->destroy($sessionId); + } + $this->newSessionId = null; + + return $this->doWrite($sessionId, $data); + } + + /** + * {@inheritdoc} + */ + public function destroy($sessionId) + { + if (\PHP_VERSION_ID < 70000) { + $this->prefetchData = null; + } + 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))); + } + $sessionCookie = sprintf(' %s=', urlencode($this->sessionName)); + $sessionCookieWithId = sprintf('%s%s;', $sessionCookie, urlencode($sessionId)); + $sessionCookieFound = false; + $otherCookies = []; + foreach (headers_list() as $h) { + if (0 !== stripos($h, 'Set-Cookie:')) { + continue; + } + if (11 === strpos($h, $sessionCookie, 11)) { + $sessionCookieFound = true; + + if (11 !== strpos($h, $sessionCookieWithId, 11)) { + $otherCookies[] = $h; + } + } else { + $otherCookies[] = $h; + } + } + if ($sessionCookieFound) { + header_remove('Set-Cookie'); + foreach ($otherCookies as $h) { + header($h, false); + } + } else { + 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)); + } + } + + return $this->newSessionId === $sessionId || $this->doDestroy($sessionId); + } +} diff --git a/Session/Storage/Handler/MemcacheSessionHandler.php b/Session/Storage/Handler/MemcacheSessionHandler.php index 4e490a05d..3abc33caa 100644 --- a/Session/Storage/Handler/MemcacheSessionHandler.php +++ b/Session/Storage/Handler/MemcacheSessionHandler.php @@ -11,16 +11,15 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; +@trigger_error(sprintf('The class %s is deprecated since Symfony 3.4 and will be removed in 4.0. Use Symfony\Component\HttpFoundation\Session\Storage\Handler\MemcachedSessionHandler instead.', MemcacheSessionHandler::class), E_USER_DEPRECATED); + /** - * MemcacheSessionHandler. - * * @author Drak + * + * @deprecated since version 3.4, to be removed in 4.0. Use Symfony\Component\HttpFoundation\Session\Storage\Handler\MemcachedSessionHandler instead. */ class MemcacheSessionHandler implements \SessionHandlerInterface { - /** - * @var \Memcache Memcache driver - */ private $memcache; /** @@ -45,12 +44,10 @@ class MemcacheSessionHandler implements \SessionHandlerInterface * * @throws \InvalidArgumentException When unsupported options are passed */ - public function __construct(\Memcache $memcache, array $options = array()) + public function __construct(\Memcache $memcache, array $options = []) { - if ($diff = array_diff(array_keys($options), array('prefix', 'expiretime'))) { - throw new \InvalidArgumentException(sprintf( - 'The following options are not supported "%s"', implode(', ', $diff) - )); + if ($diff = array_diff(array_keys($options), ['prefix', 'expiretime'])) { + throw new \InvalidArgumentException(sprintf('The following options are not supported "%s"', implode(', ', $diff))); } $this->memcache = $memcache; diff --git a/Session/Storage/Handler/MemcachedSessionHandler.php b/Session/Storage/Handler/MemcachedSessionHandler.php index 67a49ad6f..a399be5fd 100644 --- a/Session/Storage/Handler/MemcachedSessionHandler.php +++ b/Session/Storage/Handler/MemcachedSessionHandler.php @@ -12,20 +12,15 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; /** - * MemcachedSessionHandler. - * * Memcached based session storage handler based on the Memcached class * provided by the PHP memcached extension. * - * @see http://php.net/memcached + * @see https://php.net/memcached * * @author Drak */ -class MemcachedSessionHandler implements \SessionHandlerInterface +class MemcachedSessionHandler extends AbstractSessionHandler { - /** - * @var \Memcached Memcached driver - */ private $memcached; /** @@ -43,21 +38,16 @@ class MemcachedSessionHandler implements \SessionHandlerInterface * * List of available options: * * prefix: The prefix to use for the memcached keys in order to avoid collision - * * expiretime: The time to live in seconds - * - * @param \Memcached $memcached A \Memcached instance - * @param array $options An associative array of Memcached options + * * expiretime: The time to live in seconds. * * @throws \InvalidArgumentException When unsupported options are passed */ - public function __construct(\Memcached $memcached, array $options = array()) + public function __construct(\Memcached $memcached, array $options = []) { $this->memcached = $memcached; - if ($diff = array_diff(array_keys($options), array('prefix', 'expiretime'))) { - throw new \InvalidArgumentException(sprintf( - 'The following options are not supported "%s"', implode(', ', $diff) - )); + if ($diff = array_diff(array_keys($options), ['prefix', 'expiretime'])) { + throw new \InvalidArgumentException(sprintf('The following options are not supported "%s"', implode(', ', $diff))); } $this->ttl = isset($options['expiretime']) ? (int) $options['expiretime'] : 86400; @@ -65,33 +55,35 @@ public function __construct(\Memcached $memcached, array $options = array()) } /** - * {@inheritdoc} + * @return bool */ - public function open($savePath, $sessionName) + public function close() { - return true; + return $this->memcached->quit(); } /** * {@inheritdoc} */ - public function close() + protected function doRead($sessionId) { - return true; + return $this->memcached->get($this->prefix.$sessionId) ?: ''; } /** - * {@inheritdoc} + * @return bool */ - public function read($sessionId) + public function updateTimestamp($sessionId, $data) { - return $this->memcached->get($this->prefix.$sessionId) ?: ''; + $this->memcached->touch($this->prefix.$sessionId, time() + $this->ttl); + + return true; } /** * {@inheritdoc} */ - public function write($sessionId, $data) + protected function doWrite($sessionId, $data) { return $this->memcached->set($this->prefix.$sessionId, $data, time() + $this->ttl); } @@ -99,15 +91,15 @@ public function write($sessionId, $data) /** * {@inheritdoc} */ - public function destroy($sessionId) + protected function doDestroy($sessionId) { $result = $this->memcached->delete($this->prefix.$sessionId); - return $result || $this->memcached->getResultCode() == \Memcached::RES_NOTFOUND; + return $result || \Memcached::RES_NOTFOUND == $this->memcached->getResultCode(); } /** - * {@inheritdoc} + * @return bool */ public function gc($maxlifetime) { diff --git a/Session/Storage/Handler/MongoDbSessionHandler.php b/Session/Storage/Handler/MongoDbSessionHandler.php index 8408f000c..1dd724066 100644 --- a/Session/Storage/Handler/MongoDbSessionHandler.php +++ b/Session/Storage/Handler/MongoDbSessionHandler.php @@ -12,15 +12,15 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; /** - * MongoDB session handler. + * Session handler using the mongodb/mongodb package and MongoDB driver extension. * * @author Markus Bachmann + * + * @see https://packagist.org/packages/mongodb/mongodb + * @see https://php.net/mongodb */ -class MongoDbSessionHandler implements \SessionHandlerInterface +class MongoDbSessionHandler extends AbstractSessionHandler { - /** - * @var \Mongo|\MongoClient|\MongoDB\Client - */ private $mongo; /** @@ -42,7 +42,7 @@ class MongoDbSessionHandler implements \SessionHandlerInterface * * id_field: The field name for storing the session id [default: _id] * * data_field: The field name for storing the session data [default: data] * * time_field: The field name for storing the timestamp [default: time] - * * expiry_field: The field name for storing the expiry-timestamp [default: expires_at] + * * expiry_field: The field name for storing the expiry-timestamp [default: expires_at]. * * It is strongly recommended to put an index on the `expiry_field` for * garbage-collection. Alternatively it's possible to automatically expire @@ -56,19 +56,23 @@ class MongoDbSessionHandler implements \SessionHandlerInterface * { "expireAfterSeconds": 0 } * ) * - * More details on: http://docs.mongodb.org/manual/tutorial/expire-data/ + * More details on: https://docs.mongodb.org/manual/tutorial/expire-data/ * * If you use such an index, you can drop `gc_probability` to 0 since * no garbage-collection is required. * - * @param \Mongo|\MongoClient|\MongoDB\Client $mongo A MongoDB\Client, MongoClient or Mongo instance - * @param array $options An associative array of field options + * @param \MongoDB\Client $mongo A MongoDB\Client instance + * @param array $options An associative array of field options * * @throws \InvalidArgumentException When MongoClient or Mongo instance not provided * @throws \InvalidArgumentException When "database" or "collection" not provided */ public function __construct($mongo, array $options) { + if ($mongo instanceof \MongoClient || $mongo instanceof \Mongo) { + @trigger_error(sprintf('Using %s with the legacy mongo extension is deprecated as of 3.4 and will be removed in 4.0. Use it with the mongodb/mongodb package and ext-mongodb instead.', __CLASS__), E_USER_DEPRECATED); + } + if (!($mongo instanceof \MongoDB\Client || $mongo instanceof \MongoClient || $mongo instanceof \Mongo)) { throw new \InvalidArgumentException('MongoClient or Mongo instance required'); } @@ -79,20 +83,12 @@ public function __construct($mongo, array $options) $this->mongo = $mongo; - $this->options = array_merge(array( + $this->options = array_merge([ 'id_field' => '_id', 'data_field' => 'data', 'time_field' => 'time', 'expiry_field' => 'expires_at', - ), $options); - } - - /** - * {@inheritdoc} - */ - public function open($savePath, $sessionName) - { - return true; + ], $options); } /** @@ -106,13 +102,13 @@ public function close() /** * {@inheritdoc} */ - public function destroy($sessionId) + protected function doDestroy($sessionId) { $methodName = $this->mongo instanceof \MongoDB\Client ? 'deleteOne' : 'remove'; - $this->getCollection()->$methodName(array( + $this->getCollection()->$methodName([ $this->options['id_field'] => $sessionId, - )); + ]); return true; } @@ -122,11 +118,11 @@ public function destroy($sessionId) */ public function gc($maxlifetime) { - $methodName = $this->mongo instanceof \MongoDB\Client ? 'deleteOne' : 'remove'; + $methodName = $this->mongo instanceof \MongoDB\Client ? 'deleteMany' : 'remove'; - $this->getCollection()->$methodName(array( - $this->options['expiry_field'] => array('$lt' => $this->createDateTime()), - )); + $this->getCollection()->$methodName([ + $this->options['expiry_field'] => ['$lt' => $this->createDateTime()], + ]); return true; } @@ -134,16 +130,16 @@ public function gc($maxlifetime) /** * {@inheritdoc} */ - public function write($sessionId, $data) + protected function doWrite($sessionId, $data) { $expiry = $this->createDateTime(time() + (int) ini_get('session.gc_maxlifetime')); - $fields = array( + $fields = [ $this->options['time_field'] => $this->createDateTime(), $this->options['expiry_field'] => $expiry, - ); + ]; - $options = array('upsert' => true); + $options = ['upsert' => true]; if ($this->mongo instanceof \MongoDB\Client) { $fields[$this->options['data_field']] = new \MongoDB\BSON\Binary($data, \MongoDB\BSON\Binary::TYPE_OLD_BINARY); @@ -155,8 +151,35 @@ public function write($sessionId, $data) $methodName = $this->mongo instanceof \MongoDB\Client ? 'updateOne' : 'update'; $this->getCollection()->$methodName( - array($this->options['id_field'] => $sessionId), - array('$set' => $fields), + [$this->options['id_field'] => $sessionId], + ['$set' => $fields], + $options + ); + + return true; + } + + /** + * {@inheritdoc} + */ + public function updateTimestamp($sessionId, $data) + { + $expiry = $this->createDateTime(time() + (int) ini_get('session.gc_maxlifetime')); + + if ($this->mongo instanceof \MongoDB\Client) { + $methodName = 'updateOne'; + $options = []; + } else { + $methodName = 'update'; + $options = ['multiple' => false]; + } + + $this->getCollection()->$methodName( + [$this->options['id_field'] => $sessionId], + ['$set' => [ + $this->options['time_field'] => $this->createDateTime(), + $this->options['expiry_field'] => $expiry, + ]], $options ); @@ -166,12 +189,12 @@ public function write($sessionId, $data) /** * {@inheritdoc} */ - public function read($sessionId) + protected function doRead($sessionId) { - $dbData = $this->getCollection()->findOne(array( + $dbData = $this->getCollection()->findOne([ $this->options['id_field'] => $sessionId, - $this->options['expiry_field'] => array('$gte' => $this->createDateTime()), - )); + $this->options['expiry_field'] => ['$gte' => $this->createDateTime()], + ]); if (null === $dbData) { return ''; diff --git a/Session/Storage/Handler/NativeFileSessionHandler.php b/Session/Storage/Handler/NativeFileSessionHandler.php index 1be0a3983..8b7615ec1 100644 --- a/Session/Storage/Handler/NativeFileSessionHandler.php +++ b/Session/Storage/Handler/NativeFileSessionHandler.php @@ -12,8 +12,6 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; /** - * NativeFileSessionHandler. - * * Native session handler using PHP's built in file storage. * * @author Drak @@ -21,15 +19,14 @@ class NativeFileSessionHandler extends NativeSessionHandler { /** - * Constructor. - * * @param string $savePath Path of directory to save session files * Default null will leave setting as defined by PHP. * '/path', 'N;/path', or 'N;octal-mode;/path * - * @see http://php.net/session.configuration.php#ini.session.save-path for further details. + * @see https://php.net/session.configuration#ini.session.save-path for further details. * * @throws \InvalidArgumentException On invalid $savePath + * @throws \RuntimeException When failing to create the save directory */ public function __construct($savePath = null) { diff --git a/Session/Storage/Handler/NativeSessionHandler.php b/Session/Storage/Handler/NativeSessionHandler.php index 4ae410f9b..5159b1e35 100644 --- a/Session/Storage/Handler/NativeSessionHandler.php +++ b/Session/Storage/Handler/NativeSessionHandler.php @@ -12,10 +12,13 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; /** - * Adds SessionHandler functionality if available. - * - * @see http://php.net/sessionhandler + * @deprecated since version 3.4, to be removed in 4.0. Use \SessionHandler instead. + * @see https://php.net/sessionhandler */ class NativeSessionHandler extends \SessionHandler { + public function __construct() + { + @trigger_error('The '.__NAMESPACE__.'\NativeSessionHandler class is deprecated since Symfony 3.4 and will be removed in 4.0. Use the \SessionHandler class instead.', E_USER_DEPRECATED); + } } diff --git a/Session/Storage/Handler/NullSessionHandler.php b/Session/Storage/Handler/NullSessionHandler.php index 1516d4314..3ba9378ca 100644 --- a/Session/Storage/Handler/NullSessionHandler.php +++ b/Session/Storage/Handler/NullSessionHandler.php @@ -12,18 +12,16 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; /** - * NullSessionHandler. - * * Can be used in unit testing or in a situations where persisted sessions are not desired. * * @author Drak */ -class NullSessionHandler implements \SessionHandlerInterface +class NullSessionHandler extends AbstractSessionHandler { /** * {@inheritdoc} */ - public function open($savePath, $sessionName) + public function close() { return true; } @@ -31,7 +29,7 @@ public function open($savePath, $sessionName) /** * {@inheritdoc} */ - public function close() + public function validateId($sessionId) { return true; } @@ -39,7 +37,7 @@ public function close() /** * {@inheritdoc} */ - public function read($sessionId) + protected function doRead($sessionId) { return ''; } @@ -47,7 +45,7 @@ public function read($sessionId) /** * {@inheritdoc} */ - public function write($sessionId, $data) + public function updateTimestamp($sessionId, $data) { return true; } @@ -55,7 +53,7 @@ public function write($sessionId, $data) /** * {@inheritdoc} */ - public function destroy($sessionId) + protected function doWrite($sessionId, $data) { return true; } @@ -63,6 +61,14 @@ public function destroy($sessionId) /** * {@inheritdoc} */ + protected function doDestroy($sessionId) + { + return true; + } + + /** + * @return bool + */ public function gc($maxlifetime) { return true; diff --git a/Session/Storage/Handler/PdoSessionHandler.php b/Session/Storage/Handler/PdoSessionHandler.php index 8909a5f40..c9d47b6ed 100644 --- a/Session/Storage/Handler/PdoSessionHandler.php +++ b/Session/Storage/Handler/PdoSessionHandler.php @@ -32,13 +32,13 @@ * 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 * @author Tobias Schultze */ -class PdoSessionHandler implements \SessionHandlerInterface +class PdoSessionHandler extends AbstractSessionHandler { /** * No locking is done. This means sessions are prone to loss of data due to @@ -71,7 +71,7 @@ class PdoSessionHandler implements \SessionHandlerInterface private $pdo; /** - * @var string|null|false DSN string or null for session.save_path or false when lazy connection disabled + * @var string|false|null DSN string or null for session.save_path or false when lazy connection disabled */ private $dsn = false; @@ -118,7 +118,7 @@ class PdoSessionHandler implements \SessionHandlerInterface /** * @var array Connection options when lazy-connect */ - private $connectionOptions = array(); + private $connectionOptions = []; /** * @var int The strategy for locking, see constants @@ -130,7 +130,7 @@ class PdoSessionHandler implements \SessionHandlerInterface * * @var \PDOStatement[] An array of statements to release advisory locks */ - private $unlockStatements = array(); + private $unlockStatements = []; /** * @var bool True when the current session exists but expired according to session.gc_maxlifetime @@ -148,8 +148,6 @@ class PdoSessionHandler implements \SessionHandlerInterface private $gcCalled = false; /** - * Constructor. - * * You can either pass an existing database connection as PDO instance or * pass a DSN string that will be used to lazy-connect to the database * when the session is actually used. Furthermore it's possible to pass null @@ -163,15 +161,15 @@ class PdoSessionHandler implements \SessionHandlerInterface * * db_time_col: The column where to store the timestamp [default: sess_time] * * db_username: The username when lazy-connect [default: ''] * * db_password: The password when lazy-connect [default: ''] - * * db_connection_options: An array of driver-specific connection options [default: array()] + * * db_connection_options: An array of driver-specific connection options [default: []] * * lock_mode: The strategy for locking, see constants [default: LOCK_TRANSACTIONAL] * - * @param \PDO|string|null $pdoOrDsn A \PDO instance or DSN string or null + * @param \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 */ - public function __construct($pdoOrDsn = null, array $options = array()) + public function __construct($pdoOrDsn = null, array $options = []) { if ($pdoOrDsn instanceof \PDO) { if (\PDO::ERRMODE_EXCEPTION !== $pdoOrDsn->getAttribute(\PDO::ATTR_ERRMODE)) { @@ -180,6 +178,8 @@ public function __construct($pdoOrDsn = null, array $options = array()) $this->pdo = $pdoOrDsn; $this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); + } elseif (\is_string($pdoOrDsn) && false !== strpos($pdoOrDsn, '://')) { + $this->dsn = $this->buildDsnFromUrl($pdoOrDsn); } else { $this->dsn = $pdoOrDsn; } @@ -218,7 +218,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 utf8_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)"; @@ -262,11 +262,13 @@ public function isSessionExpired() */ public function open($savePath, $sessionName) { + $this->sessionExpired = false; + if (null === $this->pdo) { $this->connect($this->dsn ?: $savePath); } - return true; + return parent::open($savePath, $sessionName); } /** @@ -275,7 +277,7 @@ public function open($savePath, $sessionName) public function read($sessionId) { try { - return $this->doRead($sessionId); + return parent::read($sessionId); } catch (\PDOException $e) { $this->rollback(); @@ -284,7 +286,7 @@ public function read($sessionId) } /** - * {@inheritdoc} + * @return bool */ public function gc($maxlifetime) { @@ -298,7 +300,7 @@ public function gc($maxlifetime) /** * {@inheritdoc} */ - public function destroy($sessionId) + protected function doDestroy($sessionId) { // delete the record associated with this id $sql = "DELETE FROM $this->table WHERE $this->idCol = :id"; @@ -319,7 +321,7 @@ public function destroy($sessionId) /** * {@inheritdoc} */ - public function write($sessionId, $data) + protected function doWrite($sessionId, $data) { $maxlifetime = (int) ini_get('session.gc_maxlifetime'); @@ -332,13 +334,7 @@ public function write($sessionId, $data) return true; } - $updateStmt = $this->pdo->prepare( - "UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time WHERE $this->idCol = :id" - ); - $updateStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); - $updateStmt->bindParam(':data', $data, \PDO::PARAM_LOB); - $updateStmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT); - $updateStmt->bindValue(':time', time(), \PDO::PARAM_INT); + $updateStmt = $this->getUpdateStatement($sessionId, $data, $maxlifetime); $updateStmt->execute(); // When MERGE is not supported, like in Postgres < 9.5, we have to use this approach that can result in @@ -348,13 +344,7 @@ public function write($sessionId, $data) // false positives due to longer gap locking. if (!$updateStmt->rowCount()) { try { - $insertStmt = $this->pdo->prepare( - "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)" - ); - $insertStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); - $insertStmt->bindParam(':data', $data, \PDO::PARAM_LOB); - $insertStmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT); - $insertStmt->bindValue(':time', time(), \PDO::PARAM_INT); + $insertStmt = $this->getInsertStatement($sessionId, $data, $maxlifetime); $insertStmt->execute(); } catch (\PDOException $e) { // Handle integrity violation SQLSTATE 23000 (or a subclass like 23505 in Postgres) for duplicate keys @@ -374,6 +364,30 @@ public function write($sessionId, $data) return true; } + /** + * {@inheritdoc} + */ + public function updateTimestamp($sessionId, $data) + { + $maxlifetime = (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" + ); + $updateStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); + $updateStmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT); + $updateStmt->bindValue(':time', time(), \PDO::PARAM_INT); + $updateStmt->execute(); + } catch (\PDOException $e) { + $this->rollback(); + + throw $e; + } + + return true; + } + /** * {@inheritdoc} */ @@ -389,7 +403,11 @@ public function close() $this->gcCalled = false; // delete the session records that have expired - $sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol < :time"; + if ('mysql' === $this->driver) { + $sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol < :time"; + } else { + $sql = "DELETE FROM $this->table WHERE $this->lifetimeCol < :time - $this->timeCol"; + } $stmt = $this->pdo->prepare($sql); $stmt->bindValue(':time', time(), \PDO::PARAM_INT); @@ -415,6 +433,102 @@ private function connect($dsn) $this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); } + /** + * 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) + { + // (pdo_)?sqlite3?:///... => (pdo_)?sqlite3?://localhost/... or else the URL will be invalid + $url = preg_replace('#^((?:pdo_)?sqlite3?):///#', '$1://localhost/', $dsnOrUrl); + + $params = parse_url($url); + + if (false === $params) { + return $dsnOrUrl; // If the URL is not valid, let's assume it might be a DSN already. + } + + $params = array_map('rawurldecode', $params); + + // Override the default username and password. Values passed through options will still win over these in the constructor. + if (isset($params['user'])) { + $this->username = $params['user']; + } + + if (isset($params['pass'])) { + $this->password = $params['pass']; + } + + if (!isset($params['scheme'])) { + throw new \InvalidArgumentException('URLs without scheme are not supported to configure the PdoSessionHandler'); + } + + $driverAliasMap = [ + 'mssql' => 'sqlsrv', + 'mysql2' => 'mysql', // Amazon RDS, for some weird reason + 'postgres' => 'pgsql', + 'postgresql' => 'pgsql', + 'sqlite3' => 'sqlite', + ]; + + $driver = isset($driverAliasMap[$params['scheme']]) ? $driverAliasMap[$params['scheme']] : $params['scheme']; + + // Doctrine DBAL supports passing its internal pdo_* driver names directly too (allowing both dashes and underscores). This allows supporting the same here. + if (0 === strpos($driver, 'pdo_') || 0 === strpos($driver, 'pdo-')) { + $driver = substr($driver, 4); + } + + switch ($driver) { + case 'mysql': + case 'pgsql': + $dsn = $driver.':'; + + if (isset($params['host']) && '' !== $params['host']) { + $dsn .= 'host='.$params['host'].';'; + } + + if (isset($params['port']) && '' !== $params['port']) { + $dsn .= 'port='.$params['port'].';'; + } + + if (isset($params['path'])) { + $dbName = substr($params['path'], 1); // Remove the leading slash + $dsn .= 'dbname='.$dbName.';'; + } + + return $dsn; + + case 'sqlite': + return 'sqlite:'.substr($params['path'], 1); + + case 'sqlsrv': + $dsn = 'sqlsrv:server='; + + if (isset($params['host'])) { + $dsn .= $params['host']; + } + + if (isset($params['port']) && '' !== $params['port']) { + $dsn .= ','.$params['port']; + } + + if (isset($params['path'])) { + $dbName = substr($params['path'], 1); // Remove the leading slash + $dsn .= ';Database='.$dbName; + } + + return $dsn; + + default: + throw new \InvalidArgumentException(sprintf('The scheme "%s" is not supported by the PdoSessionHandler URL configuration. Pass a PDO DSN directly.', $params['scheme'])); + } + } + /** * Helper method to begin a transaction. * @@ -424,7 +538,7 @@ private function connect($dsn) * 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() @@ -493,10 +607,8 @@ private function rollback() * * @return string The session data */ - private function doRead($sessionId) + protected function doRead($sessionId) { - $this->sessionExpired = false; - if (self::LOCK_ADVISORY === $this->lockMode) { $this->unlockStatements[] = $this->doAdvisoryLock($sessionId); } @@ -504,6 +616,7 @@ private function doRead($sessionId) $selectSql = $this->getSelectSql(); $selectStmt = $this->pdo->prepare($selectSql); $selectStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); + $insertStmt = null; do { $selectStmt->execute(); @@ -516,20 +629,21 @@ private function doRead($sessionId) return ''; } - return is_resource($sessionRows[0][0]) ? stream_get_contents($sessionRows[0][0]) : $sessionRows[0][0]; + return \is_resource($sessionRows[0][0]) ? stream_get_contents($sessionRows[0][0]) : $sessionRows[0][0]; } - if (self::LOCK_TRANSACTIONAL === $this->lockMode && 'sqlite' !== $this->driver) { + if (null !== $insertStmt) { + $this->rollback(); + throw new \RuntimeException('Failed to read session: INSERT reported a duplicate id but next SELECT did not return any data.'); + } + + if (!filter_var(ini_get('session.use_strict_mode'), FILTER_VALIDATE_BOOLEAN) && self::LOCK_TRANSACTIONAL === $this->lockMode && 'sqlite' !== $this->driver) { + // In strict mode, session fixation is not possible: new sessions always start with a unique + // random id, so that concurrency is not possible and this code path can be skipped. // Exclusive-reading of non-existent rows does not block, so we need to do an insert to block // until other connections to the session are committed. try { - $insertStmt = $this->pdo->prepare( - "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)" - ); - $insertStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); - $insertStmt->bindValue(':data', '', \PDO::PARAM_LOB); - $insertStmt->bindValue(':lifetime', 0, \PDO::PARAM_INT); - $insertStmt->bindValue(':time', time(), \PDO::PARAM_INT); + $insertStmt = $this->getInsertStatement($sessionId, '', 0); $insertStmt->execute(); } catch (\PDOException $e) { // Catch duplicate key error because other connection created the session already. @@ -568,23 +682,25 @@ private function doAdvisoryLock($sessionId) { switch ($this->driver) { case 'mysql': + // MySQL 5.7.5 and later enforces a maximum length on lock names of 64 characters. Previously, no limit was enforced. + $lockId = substr($sessionId, 0, 64); // should we handle the return value? 0 on timeout, null on error // we use a timeout of 50 seconds which is also the default for innodb_lock_wait_timeout $stmt = $this->pdo->prepare('SELECT GET_LOCK(:key, 50)'); - $stmt->bindValue(':key', $sessionId, \PDO::PARAM_STR); + $stmt->bindValue(':key', $lockId, \PDO::PARAM_STR); $stmt->execute(); $releaseStmt = $this->pdo->prepare('DO RELEASE_LOCK(:key)'); - $releaseStmt->bindValue(':key', $sessionId, \PDO::PARAM_STR); + $releaseStmt->bindValue(':key', $lockId, \PDO::PARAM_STR); return $releaseStmt; case 'pgsql': // Obtaining an exclusive session level advisory lock requires an integer key. - // So we convert the HEX representation of the session id to an integer. - // Since integers are signed, we have to skip one hex char to fit in the range. - if (4 === PHP_INT_SIZE) { - $sessionInt1 = hexdec(substr($sessionId, 0, 7)); - $sessionInt2 = hexdec(substr($sessionId, 7, 7)); + // When session.sid_bits_per_character > 4, the session id can contain non-hex-characters. + // So we cannot just use hexdec(). + if (4 === \PHP_INT_SIZE) { + $sessionInt1 = $this->convertStringToInt($sessionId); + $sessionInt2 = $this->convertStringToInt(substr($sessionId, 4, 4)); $stmt = $this->pdo->prepare('SELECT pg_advisory_lock(:key1, :key2)'); $stmt->bindValue(':key1', $sessionInt1, \PDO::PARAM_INT); @@ -595,7 +711,7 @@ private function doAdvisoryLock($sessionId) $releaseStmt->bindValue(':key1', $sessionInt1, \PDO::PARAM_INT); $releaseStmt->bindValue(':key2', $sessionInt2, \PDO::PARAM_INT); } else { - $sessionBigInt = hexdec(substr($sessionId, 0, 15)); + $sessionBigInt = $this->convertStringToInt($sessionId); $stmt = $this->pdo->prepare('SELECT pg_advisory_lock(:key)'); $stmt->bindValue(':key', $sessionBigInt, \PDO::PARAM_INT); @@ -613,6 +729,27 @@ private function doAdvisoryLock($sessionId) } } + /** + * Encodes the first 4 (when PHP_INT_SIZE == 4) or 8 characters of the string as an integer. + * + * Keep in mind, PHP integers are signed. + * + * @param string $string + * + * @return int + */ + private function convertStringToInt($string) + { + if (4 === \PHP_INT_SIZE) { + return (\ord($string[3]) << 24) + (\ord($string[2]) << 16) + (\ord($string[1]) << 8) + \ord($string[0]); + } + + $int1 = (\ord($string[7]) << 24) + (\ord($string[6]) << 16) + (\ord($string[5]) << 8) + \ord($string[4]); + $int2 = (\ord($string[3]) << 24) + (\ord($string[2]) << 16) + (\ord($string[1]) << 8) + \ord($string[0]); + + return $int2 + ($int1 << 32); + } + /** * Return a locking or nonlocking SQL query to read session information. * @@ -643,6 +780,72 @@ private function getSelectSql() return "SELECT $this->dataCol, $this->lifetimeCol, $this->timeCol FROM $this->table WHERE $this->idCol = :id"; } + /** + * 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) + { + 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"; + break; + default: + $data = $sessionData; + $sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :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(':time', time(), \PDO::PARAM_INT); + + return $stmt; + } + + /** + * 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) + { + 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"; + break; + default: + $data = $sessionData; + $sql = "UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $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(':time', time(), \PDO::PARAM_INT); + + return $stmt; + } + /** * Returns a merge/upsert (i.e. insert or update) statement when supported by the database for writing session data. * @@ -654,21 +857,14 @@ private function getSelectSql() */ private function getMergeStatement($sessionId, $data, $maxlifetime) { - $mergeSql = null; switch (true) { case 'mysql' === $this->driver: $mergeSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ". "ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)"; break; - case 'oci' === $this->driver: - // DUAL is Oracle specific dummy table - $mergeSql = "MERGE INTO $this->table USING DUAL ON ($this->idCol = ?) ". - "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ". - "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?"; - break; case 'sqlsrv' === $this->driver && version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '10', '>='): // MERGE is only available since SQL Server 2008 and must be terminated by semicolon - // It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx + // It also requires HOLDLOCK according to https://weblogs.sqlteam.com/dang/2009/01/31/upsert-race-condition-with-merge/ $mergeSql = "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = ?) ". "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ". "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?;"; @@ -680,29 +876,30 @@ private function getMergeStatement($sessionId, $data, $maxlifetime) $mergeSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ". "ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->lifetimeCol, $this->timeCol) = (EXCLUDED.$this->dataCol, EXCLUDED.$this->lifetimeCol, EXCLUDED.$this->timeCol)"; break; + default: + // MERGE is not supported with LOBs: https://oracle.com/technetwork/articles/fuecks-lobs-095315.html + return null; } - if (null !== $mergeSql) { - $mergeStmt = $this->pdo->prepare($mergeSql); - - if ('sqlsrv' === $this->driver || 'oci' === $this->driver) { - $mergeStmt->bindParam(1, $sessionId, \PDO::PARAM_STR); - $mergeStmt->bindParam(2, $sessionId, \PDO::PARAM_STR); - $mergeStmt->bindParam(3, $data, \PDO::PARAM_LOB); - $mergeStmt->bindParam(4, $maxlifetime, \PDO::PARAM_INT); - $mergeStmt->bindValue(5, time(), \PDO::PARAM_INT); - $mergeStmt->bindParam(6, $data, \PDO::PARAM_LOB); - $mergeStmt->bindParam(7, $maxlifetime, \PDO::PARAM_INT); - $mergeStmt->bindValue(8, time(), \PDO::PARAM_INT); - } else { - $mergeStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); - $mergeStmt->bindParam(':data', $data, \PDO::PARAM_LOB); - $mergeStmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT); - $mergeStmt->bindValue(':time', time(), \PDO::PARAM_INT); - } - - return $mergeStmt; + $mergeStmt = $this->pdo->prepare($mergeSql); + + if ('sqlsrv' === $this->driver) { + $mergeStmt->bindParam(1, $sessionId, \PDO::PARAM_STR); + $mergeStmt->bindParam(2, $sessionId, \PDO::PARAM_STR); + $mergeStmt->bindParam(3, $data, \PDO::PARAM_LOB); + $mergeStmt->bindParam(4, $maxlifetime, \PDO::PARAM_INT); + $mergeStmt->bindValue(5, time(), \PDO::PARAM_INT); + $mergeStmt->bindParam(6, $data, \PDO::PARAM_LOB); + $mergeStmt->bindParam(7, $maxlifetime, \PDO::PARAM_INT); + $mergeStmt->bindValue(8, time(), \PDO::PARAM_INT); + } else { + $mergeStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); + $mergeStmt->bindParam(':data', $data, \PDO::PARAM_LOB); + $mergeStmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT); + $mergeStmt->bindValue(':time', time(), \PDO::PARAM_INT); } + + return $mergeStmt; } /** diff --git a/Session/Storage/Handler/StrictSessionHandler.php b/Session/Storage/Handler/StrictSessionHandler.php new file mode 100644 index 000000000..fab8e9a16 --- /dev/null +++ b/Session/Storage/Handler/StrictSessionHandler.php @@ -0,0 +1,103 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +/** + * Adds basic `SessionUpdateTimestampHandlerInterface` behaviors to another `SessionHandlerInterface`. + * + * @author Nicolas Grekas + */ +class StrictSessionHandler extends AbstractSessionHandler +{ + private $handler; + private $doDestroy; + + public function __construct(\SessionHandlerInterface $handler) + { + if ($handler instanceof \SessionUpdateTimestampHandlerInterface) { + throw new \LogicException(sprintf('"%s" is already an instance of "SessionUpdateTimestampHandlerInterface", you cannot wrap it with "%s".', \get_class($handler), self::class)); + } + + $this->handler = $handler; + } + + /** + * {@inheritdoc} + */ + public function open($savePath, $sessionName) + { + parent::open($savePath, $sessionName); + + return $this->handler->open($savePath, $sessionName); + } + + /** + * {@inheritdoc} + */ + protected function doRead($sessionId) + { + return $this->handler->read($sessionId); + } + + /** + * {@inheritdoc} + */ + public function updateTimestamp($sessionId, $data) + { + return $this->write($sessionId, $data); + } + + /** + * {@inheritdoc} + */ + protected function doWrite($sessionId, $data) + { + return $this->handler->write($sessionId, $data); + } + + /** + * {@inheritdoc} + */ + public function destroy($sessionId) + { + $this->doDestroy = true; + $destroyed = parent::destroy($sessionId); + + return $this->doDestroy ? $this->doDestroy($sessionId) : $destroyed; + } + + /** + * {@inheritdoc} + */ + protected function doDestroy($sessionId) + { + $this->doDestroy = false; + + return $this->handler->destroy($sessionId); + } + + /** + * {@inheritdoc} + */ + public function close() + { + return $this->handler->close(); + } + + /** + * @return bool + */ + public function gc($maxlifetime) + { + return $this->handler->gc($maxlifetime); + } +} diff --git a/Session/Storage/Handler/WriteCheckSessionHandler.php b/Session/Storage/Handler/WriteCheckSessionHandler.php index d49c36cae..127e47f21 100644 --- a/Session/Storage/Handler/WriteCheckSessionHandler.php +++ b/Session/Storage/Handler/WriteCheckSessionHandler.php @@ -15,12 +15,11 @@ * Wraps another SessionHandlerInterface to only write the session when it has been modified. * * @author Adrien Brault + * + * @deprecated since version 3.4, to be removed in 4.0. Implement `SessionUpdateTimestampHandlerInterface` or extend `AbstractSessionHandler` instead. */ class WriteCheckSessionHandler implements \SessionHandlerInterface { - /** - * @var \SessionHandlerInterface - */ private $wrappedSessionHandler; /** @@ -30,6 +29,8 @@ class WriteCheckSessionHandler implements \SessionHandlerInterface public function __construct(\SessionHandlerInterface $wrappedSessionHandler) { + @trigger_error(sprintf('The %s class is deprecated since Symfony 3.4 and will be removed in 4.0. Implement `SessionUpdateTimestampHandlerInterface` or extend `AbstractSessionHandler` instead.', self::class), E_USER_DEPRECATED); + $this->wrappedSessionHandler = $wrappedSessionHandler; } diff --git a/Session/Storage/MetadataBag.php b/Session/Storage/MetadataBag.php index 322dd560f..a62f108b3 100644 --- a/Session/Storage/MetadataBag.php +++ b/Session/Storage/MetadataBag.php @@ -39,7 +39,7 @@ class MetadataBag implements SessionBagInterface /** * @var array */ - protected $meta = array(self::CREATED => 0, self::UPDATED => 0, self::LIFETIME => 0); + protected $meta = [self::CREATED => 0, self::UPDATED => 0, self::LIFETIME => 0]; /** * Unix timestamp. @@ -54,8 +54,6 @@ class MetadataBag implements SessionBagInterface private $updateThreshold; /** - * Constructor. - * * @param string $storageKey The key used to store bag in the session * @param int $updateThreshold The time to wait between two UPDATED updates */ diff --git a/Session/Storage/MockArraySessionStorage.php b/Session/Storage/MockArraySessionStorage.php index 348fd2301..c1e7523c5 100644 --- a/Session/Storage/MockArraySessionStorage.php +++ b/Session/Storage/MockArraySessionStorage.php @@ -50,7 +50,7 @@ class MockArraySessionStorage implements SessionStorageInterface /** * @var array */ - protected $data = array(); + protected $data = []; /** * @var MetadataBag @@ -60,11 +60,9 @@ class MockArraySessionStorage implements SessionStorageInterface /** * @var array|SessionBagInterface[] */ - protected $bags = array(); + protected $bags = []; /** - * Constructor. - * * @param string $name Session name * @param MetadataBag $metaBag MetadataBag instance */ @@ -74,11 +72,6 @@ public function __construct($name = 'MOCKSESSID', MetadataBag $metaBag = null) $this->setMetadataBag($metaBag); } - /** - * Sets the session data. - * - * @param array $array - */ public function setSessionData(array $array) { $this->data = $array; @@ -177,7 +170,7 @@ public function clear() } // clear out the session - $this->data = array(); + $this->data = []; // reconnect the bags to the session $this->loadSession(); @@ -215,11 +208,6 @@ public function isStarted() return $this->started; } - /** - * Sets the MetadataBag. - * - * @param MetadataBag $bag - */ public function setMetadataBag(MetadataBag $bag = null) { if (null === $bag) { @@ -254,11 +242,11 @@ protected function generateId() protected function loadSession() { - $bags = array_merge($this->bags, array($this->metadataBag)); + $bags = array_merge($this->bags, [$this->metadataBag]); foreach ($bags as $bag) { $key = $bag->getStorageKey(); - $this->data[$key] = isset($this->data[$key]) ? $this->data[$key] : array(); + $this->data[$key] = isset($this->data[$key]) ? $this->data[$key] : []; $bag->initialize($this->data[$key]); } diff --git a/Session/Storage/MockFileSessionStorage.php b/Session/Storage/MockFileSessionStorage.php index 71f9e5551..9bbd1baf2 100644 --- a/Session/Storage/MockFileSessionStorage.php +++ b/Session/Storage/MockFileSessionStorage.php @@ -24,14 +24,9 @@ */ class MockFileSessionStorage extends MockArraySessionStorage { - /** - * @var string - */ private $savePath; /** - * Constructor. - * * @param string $savePath Path of directory to save session files * @param string $name Session name * @param MetadataBag $metaBag MetadataBag instance @@ -96,7 +91,26 @@ public function save() throw new \RuntimeException('Trying to save a session that was not started yet or was already closed'); } - file_put_contents($this->getFilePath(), serialize($this->data)); + $data = $this->data; + + foreach ($this->bags as $bag) { + if (empty($data[$key = $bag->getStorageKey()])) { + unset($data[$key]); + } + } + if ([$key = $this->metadataBag->getStorageKey()] === array_keys($data)) { + unset($data[$key]); + } + + try { + if ($data) { + file_put_contents($this->getFilePath(), serialize($data)); + } else { + $this->destroy(); + } + } finally { + $this->data = $data; + } // this is needed for Silex, where the session object is re-used across requests // in functional tests. In Symfony, the container is rebooted, so we don't have @@ -131,7 +145,7 @@ private function getFilePath() private function read() { $filePath = $this->getFilePath(); - $this->data = is_readable($filePath) && is_file($filePath) ? unserialize(file_get_contents($filePath)) : array(); + $this->data = is_readable($filePath) && is_file($filePath) ? unserialize(file_get_contents($filePath)) : []; $this->loadSession(); } diff --git a/Session/Storage/NativeSessionStorage.php b/Session/Storage/NativeSessionStorage.php index 97161b8d0..4c5873728 100644 --- a/Session/Storage/NativeSessionStorage.php +++ b/Session/Storage/NativeSessionStorage.php @@ -11,9 +11,8 @@ namespace Symfony\Component\HttpFoundation\Session\Storage; -use Symfony\Component\Debug\Exception\ContextErrorException; use Symfony\Component\HttpFoundation\Session\SessionBagInterface; -use Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeSessionHandler; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler; use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy; use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy; @@ -25,11 +24,9 @@ class NativeSessionStorage implements SessionStorageInterface { /** - * Array of SessionBagInterface. - * * @var SessionBagInterface[] */ - protected $bags; + protected $bags = []; /** * @var bool @@ -42,7 +39,7 @@ class NativeSessionStorage implements SessionStorageInterface protected $closed = false; /** - * @var AbstractProxy + * @var AbstractProxy|\SessionHandlerInterface */ protected $saveHandler; @@ -52,20 +49,19 @@ class NativeSessionStorage implements SessionStorageInterface protected $metadataBag; /** - * Constructor. - * * Depending on how you want the storage driver to behave you probably * want to override this constructor entirely. * * List of options for $options array with their defaults. * - * @see http://php.net/session.configuration for options + * @see https://php.net/session.configuration for options * but we omit 'session.' from the beginning of the keys for convenience. * * ("auto_start", is not supported as it tells PHP to start a session before * PHP starts to execute user-land code. Setting during runtime has no effect). * * cache_limiter, "" (use "0" to prevent headers from being sent entirely). + * cache_expire, "0" * cookie_domain, "" * cookie_httponly, "" * cookie_lifetime, "0" @@ -78,6 +74,7 @@ class NativeSessionStorage implements SessionStorageInterface * gc_probability, "1" * hash_bits_per_character, "4" * hash_function, "0" + * lazy_write, "1" * name, "PHPSESSID" * referer_check, "" * serialize_handler, "php" @@ -97,14 +94,22 @@ 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 AbstractProxy|NativeSessionHandler|\SessionHandlerInterface|null $handler - * @param MetadataBag $metaBag MetadataBag + * @param array $options Session configuration options + * @param \SessionHandlerInterface|null $handler + * @param MetadataBag $metaBag MetadataBag */ - public function __construct(array $options = array(), $handler = null, MetadataBag $metaBag = null) + public function __construct(array $options = [], $handler = null, MetadataBag $metaBag = null) { - session_cache_limiter(''); // disable by default because it's managed by HeaderBag (if used) - ini_set('session.use_cookies', 1); + if (!\extension_loaded('session')) { + throw new \LogicException('PHP extension "session" is required.'); + } + + $options += [ + 'cache_limiter' => '', + 'cache_expire' => 0, + 'use_cookies' => 1, + 'lazy_write' => 1, + ]; session_register_shutdown(); @@ -116,7 +121,7 @@ public function __construct(array $options = array(), $handler = null, MetadataB /** * Gets the save handler instance. * - * @return AbstractProxy + * @return AbstractProxy|\SessionHandlerInterface */ public function getSaveHandler() { @@ -136,7 +141,7 @@ public function start() throw new \RuntimeException('Failed to start the session: already started by PHP.'); } - if (ini_get('session.use_cookies') && headers_sent($file, $line)) { + if (filter_var(ini_get('session.use_cookies'), FILTER_VALIDATE_BOOLEAN) && headers_sent($file, $line)) { throw new \RuntimeException(sprintf('Failed to start the session because headers have already been sent by "%s" at line %d.', $file, $line)); } @@ -192,6 +197,10 @@ public function regenerate($destroy = false, $lifetime = null) return false; } + if (headers_sent()) { + return false; + } + if (null !== $lifetime) { ini_set('session.cookie_lifetime', $lifetime); } @@ -203,7 +212,7 @@ 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 + // @see https://bugs.php.net/70013 $this->loadSession(); return $isRegenerated; @@ -214,24 +223,32 @@ public function regenerate($destroy = false, $lifetime = null) */ public function save() { - // Register custom error handler to catch a possible failure warning during session write - set_error_handler(function ($errno, $errstr, $errfile, $errline, $errcontext) { - throw new ContextErrorException($errstr, $errno, E_WARNING, $errfile, $errline, $errcontext); - }, E_WARNING); + $session = $_SESSION; - try { - session_write_close(); - restore_error_handler(); - } catch (ContextErrorException $e) { - // The default PHP error message is not very helpful, as it does not give any information on the current save handler. - // Therefore, we catch this error and trigger a warning with a better error message - $handler = $this->getSaveHandler(); - if ($handler instanceof SessionHandlerProxy) { - $handler = $handler->getHandler(); + foreach ($this->bags as $bag) { + if (empty($_SESSION[$key = $bag->getStorageKey()])) { + unset($_SESSION[$key]); + } + } + if ([$key = $this->metadataBag->getStorageKey()] === array_keys($_SESSION)) { + unset($_SESSION[$key]); + } + + // Register error handler to add information about the current save handler + $previousHandler = set_error_handler(function ($type, $msg, $file, $line) use (&$previousHandler) { + if (E_WARNING === $type && 0 === strpos($msg, 'session_write_close():')) { + $handler = $this->saveHandler instanceof SessionHandlerProxy ? $this->saveHandler->getHandler() : $this->saveHandler; + $msg = sprintf('session_write_close(): Failed to write session data with "%s" handler', \get_class($handler)); } + return $previousHandler ? $previousHandler($type, $msg, $file, $line) : false; + }); + + try { + session_write_close(); + } finally { restore_error_handler(); - trigger_error(sprintf('session_write_close(): Failed to write session data with %s handler', get_class($handler)), E_USER_WARNING); + $_SESSION = $session; } $this->closed = true; @@ -249,7 +266,7 @@ public function clear() } // clear out the session - $_SESSION = array(); + $_SESSION = []; // reconnect the bags to the session $this->loadSession(); @@ -276,7 +293,7 @@ public function getBag($name) throw new \InvalidArgumentException(sprintf('The SessionBagInterface %s is not registered.', $name)); } - if ($this->saveHandler->isActive() && !$this->started) { + if (!$this->started && $this->saveHandler->isActive()) { $this->loadSession(); } elseif (!$this->started) { $this->start(); @@ -285,11 +302,6 @@ public function getBag($name) return $this->bags[$name]; } - /** - * Sets the MetadataBag. - * - * @param MetadataBag $metaBag - */ public function setMetadataBag(MetadataBag $metaBag = null) { if (null === $metaBag) { @@ -323,28 +335,32 @@ public function isStarted() * For convenience we omit 'session.' from the beginning of the keys. * Explicitly ignores other ini keys. * - * @param array $options Session ini directives array(key => value) + * @param array $options Session ini directives [key => value] * - * @see http://php.net/session.configuration + * @see https://php.net/session.configuration */ public function setOptions(array $options) { - $validOptions = array_flip(array( - 'cache_limiter', 'cookie_domain', 'cookie_httponly', + if (headers_sent() || \PHP_SESSION_ACTIVE === session_status()) { + return; + } + + $validOptions = array_flip([ + 'cache_expire', 'cache_limiter', 'cookie_domain', 'cookie_httponly', 'cookie_lifetime', 'cookie_path', 'cookie_secure', 'entropy_file', 'entropy_length', 'gc_divisor', 'gc_maxlifetime', 'gc_probability', 'hash_bits_per_character', - 'hash_function', 'name', 'referer_check', + 'hash_function', 'lazy_write', 'name', 'referer_check', 'serialize_handler', 'use_strict_mode', 'use_cookies', 'use_only_cookies', 'use_trans_sid', 'upload_progress.enabled', 'upload_progress.cleanup', 'upload_progress.prefix', 'upload_progress.name', - 'upload_progress.freq', 'upload_progress.min-freq', 'url_rewriter.tags', + 'upload_progress.freq', 'upload_progress.min_freq', 'url_rewriter.tags', 'sid_length', 'sid_bits_per_character', 'trans_sid_hosts', 'trans_sid_tags', - )); + ]); foreach ($options as $key => $value) { if (isset($validOptions[$key])) { - ini_set('session.'.$key, $value); + ini_set('url_rewriter.tags' !== $key ? 'session.'.$key : $key, $value); } } } @@ -358,37 +374,40 @@ public function setOptions(array $options) * ini_set('session.save_handler', 'files'); * ini_set('session.save_path', '/tmp'); * - * or pass in a NativeSessionHandler instance which configures session.save_handler in the + * 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 * - * @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 + * @see https://github.com/zikula/NativeSession * - * @param AbstractProxy|NativeSessionHandler|\SessionHandlerInterface|null $saveHandler + * @param \SessionHandlerInterface|null $saveHandler * * @throws \InvalidArgumentException */ public function setSaveHandler($saveHandler = null) { if (!$saveHandler instanceof AbstractProxy && - !$saveHandler instanceof NativeSessionHandler && !$saveHandler instanceof \SessionHandlerInterface && null !== $saveHandler) { - throw new \InvalidArgumentException('Must be instance of AbstractProxy or NativeSessionHandler; implement \SessionHandlerInterface; or be null.'); + throw new \InvalidArgumentException('Must be instance of AbstractProxy; implement \SessionHandlerInterface; or be null.'); } // Wrap $saveHandler in proxy and prevent double wrapping of proxy if (!$saveHandler instanceof AbstractProxy && $saveHandler instanceof \SessionHandlerInterface) { $saveHandler = new SessionHandlerProxy($saveHandler); } elseif (!$saveHandler instanceof AbstractProxy) { - $saveHandler = new SessionHandlerProxy(new \SessionHandler()); + $saveHandler = new SessionHandlerProxy(new StrictSessionHandler(new \SessionHandler())); } $this->saveHandler = $saveHandler; - if ($this->saveHandler instanceof \SessionHandlerInterface) { + if (headers_sent() || \PHP_SESSION_ACTIVE === session_status()) { + return; + } + + if ($this->saveHandler instanceof SessionHandlerProxy) { session_set_save_handler($this->saveHandler, false); } } @@ -400,8 +419,6 @@ public function setSaveHandler($saveHandler = null) * are set to (either PHP's internal, or a custom save handler set with session_set_save_handler()). * PHP takes the return value from the read() handler, unserializes it * and populates $_SESSION with the result automatically. - * - * @param array|null $session */ protected function loadSession(array &$session = null) { @@ -409,11 +426,11 @@ protected function loadSession(array &$session = null) $session = &$_SESSION; } - $bags = array_merge($this->bags, array($this->metadataBag)); + $bags = array_merge($this->bags, [$this->metadataBag]); foreach ($bags as $bag) { $key = $bag->getStorageKey(); - $session[$key] = isset($session[$key]) ? $session[$key] : array(); + $session[$key] = isset($session[$key]) && \is_array($session[$key]) ? $session[$key] : []; $bag->initialize($session[$key]); } diff --git a/Session/Storage/PhpBridgeSessionStorage.php b/Session/Storage/PhpBridgeSessionStorage.php index 6f02a7fd7..8969e609a 100644 --- a/Session/Storage/PhpBridgeSessionStorage.php +++ b/Session/Storage/PhpBridgeSessionStorage.php @@ -11,9 +11,6 @@ namespace Symfony\Component\HttpFoundation\Session\Storage; -use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy; -use Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeSessionHandler; - /** * Allows session to be started by PHP and managed by Symfony. * @@ -22,13 +19,15 @@ class PhpBridgeSessionStorage extends NativeSessionStorage { /** - * Constructor. - * - * @param AbstractProxy|NativeSessionHandler|\SessionHandlerInterface|null $handler - * @param MetadataBag $metaBag MetadataBag + * @param \SessionHandlerInterface|null $handler + * @param MetadataBag $metaBag MetadataBag */ public function __construct($handler = null, MetadataBag $metaBag = null) { + if (!\extension_loaded('session')) { + throw new \LogicException('PHP extension "session" is required.'); + } + $this->setMetadataBag($metaBag); $this->setSaveHandler($handler); } diff --git a/Session/Storage/Proxy/AbstractProxy.php b/Session/Storage/Proxy/AbstractProxy.php index a7478656d..0303729e7 100644 --- a/Session/Storage/Proxy/AbstractProxy.php +++ b/Session/Storage/Proxy/AbstractProxy.php @@ -12,8 +12,6 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Proxy; /** - * AbstractProxy. - * * @author Drak */ abstract class AbstractProxy @@ -33,7 +31,7 @@ abstract class AbstractProxy /** * Gets the session.save_handler name. * - * @return string + * @return string|null */ public function getSaveHandlerName() { diff --git a/Session/Storage/Proxy/NativeProxy.php b/Session/Storage/Proxy/NativeProxy.php index 0db34aa28..082eed143 100644 --- a/Session/Storage/Proxy/NativeProxy.php +++ b/Session/Storage/Proxy/NativeProxy.php @@ -11,18 +11,17 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Proxy; +@trigger_error('The '.__NAMESPACE__.'\NativeProxy class is deprecated since Symfony 3.4 and will be removed in 4.0. Use your session handler implementation directly.', E_USER_DEPRECATED); + /** - * NativeProxy. + * This proxy is built-in session handlers in PHP 5.3.x. * - * This proxy is built-in session handlers in PHP 5.3.x + * @deprecated since version 3.4, to be removed in 4.0. Use your session handler implementation directly. * * @author Drak */ class NativeProxy extends AbstractProxy { - /** - * Constructor. - */ public function __construct() { // this makes an educated guess as to what the handler is since it should already be set. diff --git a/Session/Storage/Proxy/SessionHandlerProxy.php b/Session/Storage/Proxy/SessionHandlerProxy.php index 68ed713c2..e40712d93 100644 --- a/Session/Storage/Proxy/SessionHandlerProxy.php +++ b/Session/Storage/Proxy/SessionHandlerProxy.php @@ -12,22 +12,12 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Proxy; /** - * SessionHandler proxy. - * * @author Drak */ -class SessionHandlerProxy extends AbstractProxy implements \SessionHandlerInterface +class SessionHandlerProxy extends AbstractProxy implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface { - /** - * @var \SessionHandlerInterface - */ protected $handler; - /** - * Constructor. - * - * @param \SessionHandlerInterface $handler - */ public function __construct(\SessionHandlerInterface $handler) { $this->handler = $handler; @@ -86,10 +76,26 @@ public function destroy($sessionId) } /** - * {@inheritdoc} + * @return bool */ public function gc($maxlifetime) { return (bool) $this->handler->gc($maxlifetime); } + + /** + * {@inheritdoc} + */ + public function validateId($sessionId) + { + return !$this->handler instanceof \SessionUpdateTimestampHandlerInterface || $this->handler->validateId($sessionId); + } + + /** + * {@inheritdoc} + */ + public function updateTimestamp($sessionId, $data) + { + return $this->handler instanceof \SessionUpdateTimestampHandlerInterface ? $this->handler->updateTimestamp($sessionId, $data) : $this->write($sessionId, $data); + } } diff --git a/Session/Storage/SessionStorageInterface.php b/Session/Storage/SessionStorageInterface.php index 34f6c4633..eeb396a2f 100644 --- a/Session/Storage/SessionStorageInterface.php +++ b/Session/Storage/SessionStorageInterface.php @@ -26,7 +26,7 @@ interface SessionStorageInterface * * @return bool True if started * - * @throws \RuntimeException If something goes wrong starting the session. + * @throws \RuntimeException if something goes wrong starting the session */ public function start(); @@ -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. @@ -104,8 +104,8 @@ public function regenerate($destroy = false, $lifetime = null); * a real PHP session would interfere with testing, in which case * it should actually persist the session data if required. * - * @throws \RuntimeException If the session is saved without being started, or if the session - * is already closed. + * @throws \RuntimeException if the session is saved without being started, or if the session + * is already closed */ public function save(); @@ -127,8 +127,6 @@ public function getBag($name); /** * Registers a SessionBagInterface for use. - * - * @param SessionBagInterface $bag */ public function registerBag(SessionBagInterface $bag); diff --git a/StreamedResponse.php b/StreamedResponse.php index 928531309..b9148ea87 100644 --- a/StreamedResponse.php +++ b/StreamedResponse.php @@ -17,7 +17,7 @@ * A StreamedResponse uses a callback for its content. * * The callback should use the standard PHP functions like echo - * to stream the response back to the client. The flush() method + * to stream the response back to the client. The flush() function * can also be used if needed. * * @see flush() @@ -31,13 +31,11 @@ class StreamedResponse extends Response private $headersSent; /** - * Constructor. - * * @param callable|null $callback A valid PHP callback or null to set it later * @param int $status The response status code * @param array $headers An array of response headers */ - public function __construct(callable $callback = null, $status = 200, $headers = array()) + public function __construct(callable $callback = null, $status = 200, $headers = []) { parent::__construct(null, $status, $headers); @@ -57,7 +55,7 @@ public function __construct(callable $callback = null, $status = 200, $headers = * * @return static */ - public static function create($callback = null, $status = 200, $headers = array()) + public static function create($callback = null, $status = 200, $headers = []) { return new static($callback, $status, $headers); } @@ -66,37 +64,45 @@ public static function create($callback = null, $status = 200, $headers = array( * Sets the PHP callback associated with this Response. * * @param callable $callback A valid PHP callback + * + * @return $this */ public function setCallback(callable $callback) { $this->callback = $callback; + + return $this; } /** * {@inheritdoc} * * This method only sends the headers once. + * + * @return $this */ public function sendHeaders() { if ($this->headersSent) { - return; + return $this; } $this->headersSent = true; - parent::sendHeaders(); + return parent::sendHeaders(); } /** * {@inheritdoc} * * This method only sends the content once. + * + * @return $this */ public function sendContent() { if ($this->streamed) { - return; + return $this; } $this->streamed = true; @@ -105,25 +111,31 @@ public function sendContent() throw new \LogicException('The Response callback must not be null.'); } - call_user_func($this->callback); + \call_user_func($this->callback); + + return $this; } /** * {@inheritdoc} * * @throws \LogicException when the content is not null + * + * @return $this */ public function setContent($content) { if (null !== $content) { throw new \LogicException('The content cannot be set on a StreamedResponse instance.'); } + + $this->streamed = true; + + return $this; } /** * {@inheritdoc} - * - * @return false */ public function getContent() { diff --git a/Tests/AcceptHeaderItemTest.php b/Tests/AcceptHeaderItemTest.php index cb43bb351..a40a7621d 100644 --- a/Tests/AcceptHeaderItemTest.php +++ b/Tests/AcceptHeaderItemTest.php @@ -28,24 +28,24 @@ public function testFromString($string, $value, array $attributes) public function provideFromStringData() { - return array( - array( + return [ + [ 'text/html', - 'text/html', array(), - ), - array( + 'text/html', [], + ], + [ '"this;should,not=matter"', - 'this;should,not=matter', array(), - ), - array( + 'this;should,not=matter', [], + ], + [ "text/plain; charset=utf-8;param=\"this;should,not=matter\";\tfootnotes=true", - 'text/plain', array('charset' => 'utf-8', 'param' => 'this;should,not=matter', 'footnotes' => 'true'), - ), - array( + 'text/plain', ['charset' => 'utf-8', 'param' => 'this;should,not=matter', 'footnotes' => 'true'], + ], + [ '"this;should,not=matter";charset=utf-8', - 'this;should,not=matter', array('charset' => 'utf-8'), - ), - ); + 'this;should,not=matter', ['charset' => 'utf-8'], + ], + ]; } /** @@ -59,21 +59,21 @@ public function testToString($value, array $attributes, $string) public function provideToStringData() { - return array( - array( - 'text/html', array(), + return [ + [ + 'text/html', [], 'text/html', - ), - array( - 'text/plain', array('charset' => 'utf-8', 'param' => 'this;should,not=matter', 'footnotes' => 'true'), + ], + [ + 'text/plain', ['charset' => 'utf-8', 'param' => 'this;should,not=matter', 'footnotes' => 'true'], 'text/plain;charset=utf-8;param="this;should,not=matter";footnotes=true', - ), - ); + ], + ]; } public function testValue() { - $item = new AcceptHeaderItem('value', array()); + $item = new AcceptHeaderItem('value', []); $this->assertEquals('value', $item->getValue()); $item->setValue('new value'); @@ -85,7 +85,7 @@ public function testValue() public function testQuality() { - $item = new AcceptHeaderItem('value', array()); + $item = new AcceptHeaderItem('value', []); $this->assertEquals(1.0, $item->getQuality()); $item->setQuality(0.5); @@ -98,14 +98,14 @@ public function testQuality() public function testAttribute() { - $item = new AcceptHeaderItem('value', array()); - $this->assertEquals(array(), $item->getAttributes()); + $item = new AcceptHeaderItem('value', []); + $this->assertEquals([], $item->getAttributes()); $this->assertFalse($item->hasAttribute('test')); $this->assertNull($item->getAttribute('test')); $this->assertEquals('default', $item->getAttribute('test', 'default')); $item->setAttribute('test', 'value'); - $this->assertEquals(array('test' => 'value'), $item->getAttributes()); + $this->assertEquals(['test' => 'value'], $item->getAttributes()); $this->assertTrue($item->hasAttribute('test')); $this->assertEquals('value', $item->getAttribute('test')); $this->assertEquals('value', $item->getAttribute('test', 'default')); diff --git a/Tests/AcceptHeaderTest.php b/Tests/AcceptHeaderTest.php index 9929eac28..31998696d 100644 --- a/Tests/AcceptHeaderTest.php +++ b/Tests/AcceptHeaderTest.php @@ -39,13 +39,13 @@ public function testFromString($string, array $items) public function provideFromStringData() { - return array( - array('', array()), - array('gzip', array(new AcceptHeaderItem('gzip'))), - array('gzip,deflate,sdch', array(new AcceptHeaderItem('gzip'), new AcceptHeaderItem('deflate'), new AcceptHeaderItem('sdch'))), - array("gzip, deflate\t,sdch", array(new AcceptHeaderItem('gzip'), new AcceptHeaderItem('deflate'), new AcceptHeaderItem('sdch'))), - array('"this;should,not=matter"', array(new AcceptHeaderItem('this;should,not=matter'))), - ); + return [ + ['', []], + ['gzip', [new AcceptHeaderItem('gzip')]], + ['gzip,deflate,sdch', [new AcceptHeaderItem('gzip'), new AcceptHeaderItem('deflate'), new AcceptHeaderItem('sdch')]], + ["gzip, deflate\t,sdch", [new AcceptHeaderItem('gzip'), new AcceptHeaderItem('deflate'), new AcceptHeaderItem('sdch')]], + ['"this;should,not=matter"', [new AcceptHeaderItem('this;should,not=matter')]], + ]; } /** @@ -59,12 +59,12 @@ public function testToString(array $items, $string) public function provideToStringData() { - return array( - array(array(), ''), - array(array(new AcceptHeaderItem('gzip')), 'gzip'), - array(array(new AcceptHeaderItem('gzip'), new AcceptHeaderItem('deflate'), new AcceptHeaderItem('sdch')), 'gzip,deflate,sdch'), - array(array(new AcceptHeaderItem('this;should,not=matter')), 'this;should,not=matter'), - ); + return [ + [[], ''], + [[new AcceptHeaderItem('gzip')], 'gzip'], + [[new AcceptHeaderItem('gzip'), new AcceptHeaderItem('deflate'), new AcceptHeaderItem('sdch')], 'gzip,deflate,sdch'], + [[new AcceptHeaderItem('this;should,not=matter')], 'this;should,not=matter'], + ]; } /** @@ -78,9 +78,9 @@ public function testFilter($string, $filter, array $values) public function provideFilterData() { - return array( - array('fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4', '/fr.*/', array('fr-FR', 'fr')), - ); + return [ + ['fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4', '/fr.*/', ['fr-FR', 'fr']], + ]; } /** @@ -94,10 +94,10 @@ public function testSorting($string, array $values) public function provideSortingData() { - return array( - 'quality has priority' => array('*;q=0.3,ISO-8859-1,utf-8;q=0.7', array('ISO-8859-1', 'utf-8', '*')), - 'order matters when q is equal' => array('*;q=0.3,ISO-8859-1;q=0.7,utf-8;q=0.7', array('ISO-8859-1', 'utf-8', '*')), - 'order matters when q is equal2' => array('*;q=0.3,utf-8;q=0.7,ISO-8859-1;q=0.7', array('utf-8', 'ISO-8859-1', '*')), - ); + return [ + 'quality has priority' => ['*;q=0.3,ISO-8859-1,utf-8;q=0.7', ['ISO-8859-1', 'utf-8', '*']], + 'order matters when q is equal' => ['*;q=0.3,ISO-8859-1;q=0.7,utf-8;q=0.7', ['ISO-8859-1', 'utf-8', '*']], + 'order matters when q is equal2' => ['*;q=0.3,utf-8;q=0.7,ISO-8859-1;q=0.7', ['utf-8', 'ISO-8859-1', '*']], + ]; } } diff --git a/Tests/ApacheRequestTest.php b/Tests/ApacheRequestTest.php index 157ab90ec..6fa3b8891 100644 --- a/Tests/ApacheRequestTest.php +++ b/Tests/ApacheRequestTest.php @@ -31,63 +31,63 @@ public function testUriMethods($server, $expectedRequestUri, $expectedBaseUrl, $ public function provideServerVars() { - return array( - array( - array( + return [ + [ + [ 'REQUEST_URI' => '/foo/app_dev.php/bar', 'SCRIPT_NAME' => '/foo/app_dev.php', 'PATH_INFO' => '/bar', - ), + ], '/foo/app_dev.php/bar', '/foo/app_dev.php', '/bar', - ), - array( - array( + ], + [ + [ 'REQUEST_URI' => '/foo/bar', 'SCRIPT_NAME' => '/foo/app_dev.php', - ), + ], '/foo/bar', '/foo', '/bar', - ), - array( - array( + ], + [ + [ 'REQUEST_URI' => '/app_dev.php/foo/bar', 'SCRIPT_NAME' => '/app_dev.php', 'PATH_INFO' => '/foo/bar', - ), + ], '/app_dev.php/foo/bar', '/app_dev.php', '/foo/bar', - ), - array( - array( + ], + [ + [ 'REQUEST_URI' => '/foo/bar', 'SCRIPT_NAME' => '/app_dev.php', - ), + ], '/foo/bar', '', '/foo/bar', - ), - array( - array( + ], + [ + [ 'REQUEST_URI' => '/app_dev.php', 'SCRIPT_NAME' => '/app_dev.php', - ), + ], '/app_dev.php', '/app_dev.php', '/', - ), - array( - array( + ], + [ + [ 'REQUEST_URI' => '/', 'SCRIPT_NAME' => '/app_dev.php', - ), + ], '/', '', '/', - ), - ); + ], + ]; } } diff --git a/Tests/BinaryFileResponseTest.php b/Tests/BinaryFileResponseTest.php index 1b9e58991..fcad11def 100644 --- a/Tests/BinaryFileResponseTest.php +++ b/Tests/BinaryFileResponseTest.php @@ -22,14 +22,14 @@ class BinaryFileResponseTest extends ResponseTestCase public function testConstruction() { $file = __DIR__.'/../README.md'; - $response = new BinaryFileResponse($file, 404, array('X-Header' => 'Foo'), true, null, true, true); + $response = new BinaryFileResponse($file, 404, ['X-Header' => 'Foo'], true, null, true, true); $this->assertEquals(404, $response->getStatusCode()); $this->assertEquals('Foo', $response->headers->get('X-Header')); $this->assertTrue($response->headers->has('ETag')); $this->assertTrue($response->headers->has('Last-Modified')); $this->assertFalse($response->headers->has('Content-Disposition')); - $response = BinaryFileResponse::create($file, 404, array(), true, ResponseHeaderBag::DISPOSITION_INLINE); + $response = BinaryFileResponse::create($file, 404, [], true, ResponseHeaderBag::DISPOSITION_INLINE); $this->assertEquals(404, $response->getStatusCode()); $this->assertFalse($response->headers->has('ETag')); $this->assertEquals('inline; filename="README.md"', $response->headers->get('Content-Disposition')); @@ -39,18 +39,16 @@ public function testConstructWithNonAsciiFilename() { touch(sys_get_temp_dir().'/fööö.html'); - $response = new BinaryFileResponse(sys_get_temp_dir().'/fööö.html', 200, array(), true, 'attachment'); + $response = new BinaryFileResponse(sys_get_temp_dir().'/fööö.html', 200, [], true, 'attachment'); @unlink(sys_get_temp_dir().'/fööö.html'); $this->assertSame('fööö.html', $response->getFile()->getFilename()); } - /** - * @expectedException \LogicException - */ public function testSetContent() { + $this->expectException('LogicException'); $response = new BinaryFileResponse(__FILE__); $response->setContent('foo'); } @@ -85,7 +83,7 @@ public function testSetContentDispositionGeneratesSafeFallbackFilenameForWrongly */ public function testRequests($requestRange, $offset, $length, $responseRange) { - $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, array('Content-Type' => 'application/octet-stream'))->setAutoEtag(); + $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream'])->setAutoEtag(); // do a request to get the ETag $request = Request::create('/'); @@ -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')); } /** @@ -117,7 +115,7 @@ public function testRequests($requestRange, $offset, $length, $responseRange) */ public function testRequestsWithoutEtag($requestRange, $offset, $length, $responseRange) { - $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, array('Content-Type' => 'application/octet-stream')); + $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream']); // do a request to get the LastModified $request = Request::create('/'); @@ -145,19 +143,19 @@ public function testRequestsWithoutEtag($requestRange, $offset, $length, $respon public function provideRanges() { - return array( - array('bytes=1-4', 1, 4, 'bytes 1-4/35'), - array('bytes=-5', 30, 5, 'bytes 30-34/35'), - array('bytes=30-', 30, 5, 'bytes 30-34/35'), - array('bytes=30-30', 30, 1, 'bytes 30-30/35'), - array('bytes=30-34', 30, 5, 'bytes 30-34/35'), - ); + return [ + ['bytes=1-4', 1, 4, 'bytes 1-4/35'], + ['bytes=-5', 30, 5, 'bytes 30-34/35'], + ['bytes=30-', 30, 5, 'bytes 30-34/35'], + ['bytes=30-30', 30, 1, 'bytes 30-30/35'], + ['bytes=30-34', 30, 5, 'bytes 30-34/35'], + ]; } public function testRangeRequestsWithoutLastModifiedDate() { // prevent auto last modified - $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, array('Content-Type' => 'application/octet-stream'), true, null, false, false); + $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream'], true, null, false, false); // prepare a request for a range of the testing file $request = Request::create('/'); @@ -178,7 +176,7 @@ public function testRangeRequestsWithoutLastModifiedDate() */ public function testFullFileRequests($requestRange) { - $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, array('Content-Type' => 'application/octet-stream'))->setAutoEtag(); + $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream'])->setAutoEtag(); // prepare a request for a range of the testing file $request = Request::create('/'); @@ -198,14 +196,27 @@ public function testFullFileRequests($requestRange) public function provideFullFileRanges() { - return array( - array('bytes=0-'), - array('bytes=0-34'), - array('bytes=-35'), + return [ + ['bytes=0-'], + ['bytes=0-34'], + ['bytes=-35'], // Syntactical invalid range-request should also return the full resource - array('bytes=20-10'), - array('bytes=50-40'), - ); + ['bytes=20-10'], + ['bytes=50-40'], + ]; + } + + public function testUnpreparedResponseSendsFullFile() + { + $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200); + + $data = file_get_contents(__DIR__.'/File/Fixtures/test.gif'); + + $this->expectOutputString($data); + $response = clone $response; + $response->sendContent(); + + $this->assertEquals(200, $response->getStatusCode()); } /** @@ -213,7 +224,7 @@ public function provideFullFileRanges() */ public function testInvalidRequests($requestRange) { - $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, array('Content-Type' => 'application/octet-stream'))->setAutoEtag(); + $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream'])->setAutoEtag(); // prepare a request for a range of the testing file $request = Request::create('/'); @@ -229,10 +240,10 @@ public function testInvalidRequests($requestRange) public function provideInvalidRanges() { - return array( - array('bytes=-40'), - array('bytes=30-40'), - ); + return [ + ['bytes=-40'], + ['bytes=30-40'], + ]; } /** @@ -244,21 +255,21 @@ public function testXSendfile($file) $request->headers->set('X-Sendfile-Type', 'X-Sendfile'); BinaryFileResponse::trustXSendfileTypeHeader(); - $response = BinaryFileResponse::create($file, 200, array('Content-Type' => 'application/octet-stream')); + $response = BinaryFileResponse::create($file, 200, ['Content-Type' => 'application/octet-stream']); $response->prepare($request); $this->expectOutputString(''); $response->sendContent(); - $this->assertContains('README.md', $response->headers->get('X-Sendfile')); + $this->assertStringContainsString('README.md', $response->headers->get('X-Sendfile')); } public function provideXSendfileFiles() { - return array( - array(__DIR__.'/../README.md'), - array('file://'.__DIR__.'/../README.md'), - ); + return [ + [__DIR__.'/../README.md'], + ['file://'.__DIR__.'/../README.md'], + ]; } /** @@ -273,7 +284,7 @@ public function testXAccelMapping($realpath, $mapping, $virtual) $file = new FakeFile($realpath, __DIR__.'/File/Fixtures/test'); BinaryFileResponse::trustXSendfileTypeHeader(); - $response = new BinaryFileResponse($file, 200, array('Content-Type' => 'application/octet-stream')); + $response = new BinaryFileResponse($file, 200, ['Content-Type' => 'application/octet-stream']); $reflection = new \ReflectionObject($response); $property = $reflection->getProperty('file'); $property->setAccessible(true); @@ -292,7 +303,7 @@ public function testDeleteFileAfterSend() $realPath = realpath($path); $this->assertFileExists($realPath); - $response = new BinaryFileResponse($realPath, 200, array('Content-Type' => 'application/octet-stream')); + $response = new BinaryFileResponse($realPath, 200, ['Content-Type' => 'application/octet-stream']); $response->deleteFileAfterSend(true); $response->prepare($request); @@ -304,7 +315,7 @@ public function testDeleteFileAfterSend() public function testAcceptRangeOnUnsafeMethods() { $request = Request::create('/', 'POST'); - $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, array('Content-Type' => 'application/octet-stream')); + $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream']); $response->prepare($request); $this->assertEquals('none', $response->headers->get('Accept-Ranges')); @@ -313,7 +324,7 @@ public function testAcceptRangeOnUnsafeMethods() public function testAcceptRangeNotOverriden() { $request = Request::create('/', 'POST'); - $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, array('Content-Type' => 'application/octet-stream')); + $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream']); $response->headers->set('Accept-Ranges', 'foo'); $response->prepare($request); @@ -322,16 +333,17 @@ public function testAcceptRangeNotOverriden() public function getSampleXAccelMappings() { - return array( - array('/var/www/var/www/files/foo.txt', '/var/www/=/files/', '/files/var/www/files/foo.txt'), - array('/home/foo/bar.txt', '/var/www/=/files/,/home/foo/=/baz/', '/baz/bar.txt'), - ); + return [ + ['/var/www/var/www/files/foo.txt', '/var/www/=/files/', '/files/var/www/files/foo.txt'], + ['/home/foo/bar.txt', '/var/www/=/files/,/home/foo/=/baz/', '/baz/bar.txt'], + ['/tmp/bar.txt', '"/var/www/"="/files/", "/home/Foo/"="/baz/"', null], + ]; } public function testStream() { $request = Request::create('/'); - $response = new BinaryFileResponse(new Stream(__DIR__.'/../README.md'), 200, array('Content-Type' => 'text/plain')); + $response = new BinaryFileResponse(new Stream(__DIR__.'/../README.md'), 200, ['Content-Type' => 'text/plain']); $response->prepare($request); $this->assertNull($response->headers->get('Content-Length')); @@ -339,7 +351,7 @@ public function testStream() protected function provideResponse() { - return new BinaryFileResponse(__DIR__.'/../README.md', 200, array('Content-Type' => 'application/octet-stream')); + return new BinaryFileResponse(__DIR__.'/../README.md', 200, ['Content-Type' => 'application/octet-stream']); } public static function tearDownAfterClass() diff --git a/Tests/CookieTest.php b/Tests/CookieTest.php index 070b7dd42..169f91787 100644 --- a/Tests/CookieTest.php +++ b/Tests/CookieTest.php @@ -24,35 +24,46 @@ */ class CookieTest extends TestCase { - public function invalidNames() + public function namesWithSpecialCharacters() { - return array( - array(''), - array(',MyName'), - array(';MyName'), - array(' MyName'), - array("\tMyName"), - array("\rMyName"), - array("\nMyName"), - array("\013MyName"), - array("\014MyName"), - ); + return [ + [',MyName'], + [';MyName'], + [' MyName'], + ["\tMyName"], + ["\rMyName"], + ["\nMyName"], + ["\013MyName"], + ["\014MyName"], + ]; } /** - * @dataProvider invalidNames - * @expectedException \InvalidArgumentException + * @dataProvider namesWithSpecialCharacters */ - public function testInstantiationThrowsExceptionIfCookieNameContainsInvalidCharacters($name) + public function testInstantiationThrowsExceptionIfRawCookieNameContainsSpecialCharacters($name) { - new Cookie($name); + $this->expectException('InvalidArgumentException'); + new Cookie($name, null, 0, null, null, null, false, true); } /** - * @expectedException \InvalidArgumentException + * @dataProvider namesWithSpecialCharacters */ + public function testInstantiationSucceedNonRawCookieNameContainsSpecialCharacters($name) + { + $this->assertInstanceOf(Cookie::class, new Cookie($name)); + } + + public function testInstantiationThrowsExceptionIfCookieNameIsEmpty() + { + $this->expectException('InvalidArgumentException'); + new Cookie(''); + } + public function testInvalidExpiration() { + $this->expectException('InvalidArgumentException'); new Cookie('MyCookie', 'foo', 'bar'); } @@ -121,7 +132,7 @@ public function testGetExpiresTimeWithStringValue() $cookie = new Cookie('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() @@ -157,18 +168,30 @@ public function testCookieIsCleared() $cookie = new Cookie('foo', 'bar', time() - 20); $this->assertTrue($cookie->isCleared(), '->isCleared() returns true if the cookie has expired'); + + $cookie = new Cookie('foo', 'bar'); + + $this->assertFalse($cookie->isCleared()); + + $cookie = new Cookie('foo', 'bar', 0); + + $this->assertFalse($cookie->isCleared()); + + $cookie = new Cookie('foo', 'bar', -1); + + $this->assertFalse($cookie->isCleared()); } public function testToString() { $cookie = new Cookie('foo', 'bar', $expire = strtotime('Fri, 20-May-2011 15:25:52 GMT'), '/', '.myfoodomain.com', true); - $this->assertEquals('foo=bar; expires=Fri, 20-May-2011 15:25:52 GMT; max-age='.($expire - time()).'; path=/; domain=.myfoodomain.com; secure; httponly', (string) $cookie, '->__toString() returns string representation of the cookie'); + $this->assertEquals('foo=bar; expires=Fri, 20-May-2011 15:25:52 GMT; Max-Age=0; path=/; domain=.myfoodomain.com; secure; httponly', (string) $cookie, '->__toString() returns string representation of the cookie'); $cookie = new Cookie('foo', 'bar with white spaces', strtotime('Fri, 20-May-2011 15:25:52 GMT'), '/', '.myfoodomain.com', true); - $this->assertEquals('foo=bar%20with%20white%20spaces; expires=Fri, 20-May-2011 15:25:52 GMT; max-age='.($expire - time()).'; path=/; domain=.myfoodomain.com; secure; httponly', (string) $cookie, '->__toString() encodes the value of the cookie according to RFC 3986 (white space = %20)'); + $this->assertEquals('foo=bar%20with%20white%20spaces; expires=Fri, 20-May-2011 15:25:52 GMT; Max-Age=0; path=/; domain=.myfoodomain.com; secure; httponly', (string) $cookie, '->__toString() encodes the value of the cookie according to RFC 3986 (white space = %20)'); $cookie = new Cookie('foo', null, 1, '/admin/', '.myfoodomain.com'); - $this->assertEquals('foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', $expire = time() - 31536001).'; max-age='.($expire - time()).'; path=/admin/; domain=.myfoodomain.com; httponly', (string) $cookie, '->__toString() returns string representation of a cleared cookie if value is NULL'); + $this->assertEquals('foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', $expire = time() - 31536001).'; Max-Age=0; path=/admin/; domain=.myfoodomain.com; httponly', (string) $cookie, '->__toString() returns string representation of a cleared cookie if value is NULL'); $cookie = new Cookie('foo', 'bar', 0, '/', ''); $this->assertEquals('foo=bar; path=/; httponly', (string) $cookie); @@ -194,7 +217,7 @@ public function testGetMaxAge() $this->assertEquals($expire - time(), $cookie->getMaxAge()); $cookie = new Cookie('foo', 'bar', $expire = time() - 100); - $this->assertEquals($expire - time(), $cookie->getMaxAge()); + $this->assertEquals(0, $cookie->getMaxAge()); } public function testFromString() diff --git a/Tests/ExpressionRequestMatcherTest.php b/Tests/ExpressionRequestMatcherTest.php index 1152e46c0..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()); } @@ -55,15 +53,15 @@ public function testMatchesWhenParentMatchesIsFalse($expression) public function provideExpressions() { - return array( - array('request.getMethod() == method', true), - array('request.getPathInfo() == path', true), - array('request.getHost() == host', true), - array('request.getClientIp() == ip', true), - array('request.attributes.all() == attributes', true), - array('request.getMethod() == method && request.getPathInfo() == path && request.getHost() == host && request.getClientIp() == ip && request.attributes.all() == attributes', true), - array('request.getMethod() != method', false), - array('request.getMethod() != method && request.getPathInfo() == path && request.getHost() == host && request.getClientIp() == ip && request.attributes.all() == attributes', false), - ); + return [ + ['request.getMethod() == method', true], + ['request.getPathInfo() == path', true], + ['request.getHost() == host', true], + ['request.getClientIp() == ip', true], + ['request.attributes.all() == attributes', true], + ['request.getMethod() == method && request.getPathInfo() == path && request.getHost() == host && request.getClientIp() == ip && request.attributes.all() == attributes', true], + ['request.getMethod() != method', false], + ['request.getMethod() != method && request.getPathInfo() == path && request.getHost() == host && request.getClientIp() == ip && request.attributes.all() == attributes', false], + ]; } } diff --git a/Tests/File/FileTest.php b/Tests/File/FileTest.php index dbd9c44bd..b463aadf8 100644 --- a/Tests/File/FileTest.php +++ b/Tests/File/FileTest.php @@ -64,7 +64,7 @@ public function testGuessExtensionWithReset() public function testConstructWhenFileNotExists() { - $this->{method_exists($this, $_ = 'expectException') ? $_ : 'setExpectedException'}('Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException'); + $this->expectException('Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException'); new File(__DIR__.'/Fixtures/not_here'); } @@ -110,14 +110,14 @@ public function testMoveWithNewName() public function getFilenameFixtures() { - return array( - array('original.gif', 'original.gif'), - array('..\\..\\original.gif', 'original.gif'), - array('../../original.gif', 'original.gif'), - array('файлfile.gif', 'файлfile.gif'), - array('..\\..\\файлfile.gif', 'файлfile.gif'), - array('../../файлfile.gif', 'файлfile.gif'), - ); + return [ + ['original.gif', 'original.gif'], + ['..\\..\\original.gif', 'original.gif'], + ['../../original.gif', 'original.gif'], + ['файлfile.gif', 'файлfile.gif'], + ['..\\..\\файлfile.gif', 'файлfile.gif'], + ['../../файлfile.gif', 'файлfile.gif'], + ]; } /** @@ -172,7 +172,7 @@ protected function createMockGuesser($path, $mimeType) ->expects($this->once()) ->method('guess') ->with($this->equalTo($path)) - ->will($this->returnValue($mimeType)) + ->willReturn($mimeType) ; return $guesser; 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/case-sensitive-mime-type.xlsm b/Tests/File/Fixtures/case-sensitive-mime-type.xlsm new file mode 100644 index 000000000..94d85e613 Binary files /dev/null and b/Tests/File/Fixtures/case-sensitive-mime-type.xlsm differ diff --git a/Tests/File/MimeType/MimeTypeTest.php b/Tests/File/MimeType/MimeTypeTest.php index 5a2b7a21c..0418726b5 100644 --- a/Tests/File/MimeType/MimeTypeTest.php +++ b/Tests/File/MimeType/MimeTypeTest.php @@ -12,15 +12,24 @@ namespace Symfony\Component\HttpFoundation\Tests\File\MimeType; use PHPUnit\Framework\TestCase; -use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesser; use Symfony\Component\HttpFoundation\File\MimeType\FileBinaryMimeTypeGuesser; +use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesser; /** * @requires extension fileinfo */ class MimeTypeTest extends TestCase { - protected $path; + public function testGuessWithLeadingDash() + { + $cwd = getcwd(); + chdir(__DIR__.'/../Fixtures'); + try { + $this->assertEquals('image/gif', MimeTypeGuesser::getInstance()->guess('-test')); + } finally { + chdir($cwd); + } + } public function testGuessImageWithoutExtension() { @@ -29,7 +38,7 @@ public function testGuessImageWithoutExtension() public function testGuessImageWithDirectory() { - $this->{method_exists($this, $_ = 'expectException') ? $_ : 'setExpectedException'}('Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException'); + $this->expectException('Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException'); MimeTypeGuesser::getInstance()->guess(__DIR__.'/../Fixtures/directory'); } @@ -53,13 +62,13 @@ public function testGuessFileWithUnknownExtension() public function testGuessWithIncorrectPath() { - $this->{method_exists($this, $_ = 'expectException') ? $_ : 'setExpectedException'}('Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException'); + $this->expectException('Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException'); MimeTypeGuesser::getInstance()->guess(__DIR__.'/../Fixtures/not_here'); } public function testGuessWithNonReadablePath() { - if ('\\' === DIRECTORY_SEPARATOR) { + if ('\\' === \DIRECTORY_SEPARATOR) { $this->markTestSkipped('Can not verify chmod operations on Windows'); } @@ -71,8 +80,8 @@ public function testGuessWithNonReadablePath() touch($path); @chmod($path, 0333); - if (substr(sprintf('%o', fileperms($path)), -4) == '0333') { - $this->{method_exists($this, $_ = 'expectException') ? $_ : 'setExpectedException'}('Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException'); + if ('0333' == substr(sprintf('%o', fileperms($path)), -4)) { + $this->expectException('Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException'); MimeTypeGuesser::getInstance()->guess($path); } else { $this->markTestSkipped('Can not verify chmod operations, change of file permissions failed'); diff --git a/Tests/File/UploadedFileTest.php b/Tests/File/UploadedFileTest.php index 36f122fe7..2ea924bac 100644 --- a/Tests/File/UploadedFileTest.php +++ b/Tests/File/UploadedFileTest.php @@ -25,7 +25,7 @@ protected function setUp() public function testConstructWhenFileNotExists() { - $this->{method_exists($this, $_ = 'expectException') ? $_ : 'setExpectedException'}('Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException'); + $this->expectException('Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException'); new UploadedFile( __DIR__.'/Fixtures/not_here', @@ -46,7 +46,7 @@ public function testFileUploadsWithNoMimeType() $this->assertEquals('application/octet-stream', $file->getClientMimeType()); - if (extension_loaded('fileinfo')) { + if (\extension_loaded('fileinfo')) { $this->assertEquals('image/gif', $file->getMimeType()); } } @@ -90,6 +90,19 @@ public function testGuessClientExtensionWithIncorrectMimeType() $this->assertEquals('jpeg', $file->guessClientExtension()); } + public function testCaseSensitiveMimeType() + { + $file = new UploadedFile( + __DIR__.'/Fixtures/case-sensitive-mime-type.xlsm', + 'test.xlsm', + 'application/vnd.ms-excel.sheet.macroEnabled.12', + filesize(__DIR__.'/Fixtures/case-sensitive-mime-type.xlsm'), + null + ); + + $this->assertEquals('xlsm', $file->guessClientExtension()); + } + public function testErrorIsOkByDefault() { $file = new UploadedFile( @@ -129,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', @@ -142,7 +153,7 @@ public function testMoveLocalFileIsNotAllowed() UPLOAD_ERR_OK ); - $movedFile = $file->move(__DIR__.'/Fixtures/directory'); + $file->move(__DIR__.'/Fixtures/directory'); } public function testMoveLocalFileIsAllowedInTestMode() @@ -249,13 +260,13 @@ public function testIsInvalidOnUploadError($error) public function uploadedFileErrorProvider() { - return array( - array(UPLOAD_ERR_INI_SIZE), - array(UPLOAD_ERR_FORM_SIZE), - array(UPLOAD_ERR_PARTIAL), - array(UPLOAD_ERR_NO_TMP_DIR), - array(UPLOAD_ERR_EXTENSION), - ); + return [ + [UPLOAD_ERR_INI_SIZE], + [UPLOAD_ERR_FORM_SIZE], + [UPLOAD_ERR_PARTIAL], + [UPLOAD_ERR_NO_TMP_DIR], + [UPLOAD_ERR_EXTENSION], + ]; } public function testIsInvalidIfNotHttpUpload() @@ -270,4 +281,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 e7defa677..a3882bc86 100644 --- a/Tests/FileBagTest.php +++ b/Tests/FileBagTest.php @@ -23,12 +23,10 @@ */ class FileBagTest extends TestCase { - /** - * @expectedException \InvalidArgumentException - */ public function testFileMustBeAnArrayOrUploadedFile() { - new FileBag(array('file' => 'foo')); + $this->expectException('InvalidArgumentException'); + new FileBag(['file' => 'foo']); } public function testShouldConvertsUploadedFiles() @@ -36,54 +34,80 @@ public function testShouldConvertsUploadedFiles() $tmpFile = $this->createTempFile(); $file = new UploadedFile($tmpFile, basename($tmpFile), 'text/plain', 100, 0); - $bag = new FileBag(array('file' => array( + $bag = new FileBag(['file' => [ 'name' => basename($tmpFile), 'type' => 'text/plain', 'tmp_name' => $tmpFile, 'error' => 0, 'size' => 100, - ))); + ]]); $this->assertEquals($file, $bag->get('file')); } public function testShouldSetEmptyUploadedFilesToNull() { - $bag = new FileBag(array('file' => array( + $bag = new FileBag(['file' => [ 'name' => '', 'type' => '', 'tmp_name' => '', 'error' => UPLOAD_ERR_NO_FILE, 'size' => 0, - ))); + ]]); $this->assertNull($bag->get('file')); } + public function testShouldRemoveEmptyUploadedFilesForMultiUpload() + { + $bag = new FileBag(['files' => [ + 'name' => [''], + 'type' => [''], + 'tmp_name' => [''], + 'error' => [UPLOAD_ERR_NO_FILE], + 'size' => [0], + ]]); + + $this->assertSame([], $bag->get('files')); + } + + public function testShouldNotRemoveEmptyUploadedFilesForAssociativeArray() + { + $bag = new FileBag(['files' => [ + 'name' => ['file1' => ''], + 'type' => ['file1' => ''], + 'tmp_name' => ['file1' => ''], + 'error' => ['file1' => UPLOAD_ERR_NO_FILE], + 'size' => ['file1' => 0], + ]]); + + $this->assertSame(['file1' => null], $bag->get('files')); + } + public function testShouldConvertUploadedFilesWithPhpBug() { $tmpFile = $this->createTempFile(); $file = new UploadedFile($tmpFile, basename($tmpFile), 'text/plain', 100, 0); - $bag = new FileBag(array( - 'child' => array( - 'name' => array( + $bag = new FileBag([ + 'child' => [ + 'name' => [ 'file' => basename($tmpFile), - ), - 'type' => array( + ], + 'type' => [ 'file' => 'text/plain', - ), - 'tmp_name' => array( + ], + 'tmp_name' => [ 'file' => $tmpFile, - ), - 'error' => array( + ], + 'error' => [ 'file' => 0, - ), - 'size' => array( + ], + 'size' => [ 'file' => 100, - ), - ), - )); + ], + ], + ]); $files = $bag->all(); $this->assertEquals($file, $files['child']['file']); @@ -94,25 +118,25 @@ public function testShouldConvertNestedUploadedFilesWithPhpBug() $tmpFile = $this->createTempFile(); $file = new UploadedFile($tmpFile, basename($tmpFile), 'text/plain', 100, 0); - $bag = new FileBag(array( - 'child' => array( - 'name' => array( - 'sub' => array('file' => basename($tmpFile)), - ), - 'type' => array( - 'sub' => array('file' => 'text/plain'), - ), - 'tmp_name' => array( - 'sub' => array('file' => $tmpFile), - ), - 'error' => array( - 'sub' => array('file' => 0), - ), - 'size' => array( - 'sub' => array('file' => 100), - ), - ), - )); + $bag = new FileBag([ + 'child' => [ + 'name' => [ + 'sub' => ['file' => basename($tmpFile)], + ], + 'type' => [ + 'sub' => ['file' => 'text/plain'], + ], + 'tmp_name' => [ + 'sub' => ['file' => $tmpFile], + ], + 'error' => [ + 'sub' => ['file' => 0], + ], + 'size' => [ + 'sub' => ['file' => 100], + ], + ], + ]); $files = $bag->all(); $this->assertEquals($file, $files['child']['sub']['file']); @@ -122,7 +146,7 @@ public function testShouldNotConvertNestedUploadedFiles() { $tmpFile = $this->createTempFile(); $file = new UploadedFile($tmpFile, basename($tmpFile), 'text/plain', 100, 0); - $bag = new FileBag(array('image' => array('file' => $file))); + $bag = new FileBag(['image' => ['file' => $file]]); $files = $bag->all(); $this->assertEquals($file, $files['image']['file']); diff --git a/Tests/Fixtures/response-functional/common.inc b/Tests/Fixtures/response-functional/common.inc new file mode 100644 index 000000000..0bdf9e4b7 --- /dev/null +++ b/Tests/Fixtures/response-functional/common.inc @@ -0,0 +1,43 @@ +headers->set('Date', 'Sat, 12 Nov 1955 20:04:00 GMT'); + +return $r; diff --git a/Tests/Fixtures/response-functional/cookie_max_age.expected b/Tests/Fixtures/response-functional/cookie_max_age.expected new file mode 100644 index 000000000..bdb9d023f --- /dev/null +++ b/Tests/Fixtures/response-functional/cookie_max_age.expected @@ -0,0 +1,11 @@ + +Warning: Expiry date cannot have a year greater than 9999 in %scookie_max_age.php on line 10 + +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: foo=bar; expires=Sat, 01-Jan-10000 02:46:40 GMT; Max-Age=%d; path=/ +) +shutdown diff --git a/Tests/Fixtures/response-functional/cookie_max_age.php b/Tests/Fixtures/response-functional/cookie_max_age.php new file mode 100644 index 000000000..8775a5cce --- /dev/null +++ b/Tests/Fixtures/response-functional/cookie_max_age.php @@ -0,0 +1,10 @@ +headers->setCookie(new Cookie('foo', 'bar', 253402310800, '', null, false, false)); +$r->sendHeaders(); + +setcookie('foo2', 'bar', 253402310800, '/'); diff --git a/Tests/Fixtures/response-functional/cookie_raw_urlencode.expected b/Tests/Fixtures/response-functional/cookie_raw_urlencode.expected new file mode 100644 index 000000000..0c097972e --- /dev/null +++ b/Tests/Fixtures/response-functional/cookie_raw_urlencode.expected @@ -0,0 +1,10 @@ + +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: ?*():@&+$/%#[]=?*():@&+$/%#[]; path=/ + [4] => Set-Cookie: ?*():@&+$/%#[]=?*():@&+$/%#[]; path=/ +) +shutdown diff --git a/Tests/Fixtures/response-functional/cookie_raw_urlencode.php b/Tests/Fixtures/response-functional/cookie_raw_urlencode.php new file mode 100644 index 000000000..2ca5b59f1 --- /dev/null +++ b/Tests/Fixtures/response-functional/cookie_raw_urlencode.php @@ -0,0 +1,12 @@ +headers->setCookie(new Cookie($str, $str, 0, '/', null, false, false, true)); +$r->sendHeaders(); + +setrawcookie($str, $str, 0, '/', null, false, false); diff --git a/Tests/Fixtures/response-functional/cookie_samesite_lax.expected b/Tests/Fixtures/response-functional/cookie_samesite_lax.expected new file mode 100644 index 000000000..cbde2cbfe --- /dev/null +++ b/Tests/Fixtures/response-functional/cookie_samesite_lax.expected @@ -0,0 +1,9 @@ + +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: CookieSamesiteLaxTest=LaxValue; path=/; httponly; samesite=lax +) +shutdown diff --git a/Tests/Fixtures/response-functional/cookie_samesite_lax.php b/Tests/Fixtures/response-functional/cookie_samesite_lax.php new file mode 100644 index 000000000..9a476f1d2 --- /dev/null +++ b/Tests/Fixtures/response-functional/cookie_samesite_lax.php @@ -0,0 +1,8 @@ +headers->setCookie(new Cookie('CookieSamesiteLaxTest', 'LaxValue', 0, '/', null, false, true, false, Cookie::SAMESITE_LAX)); +$r->sendHeaders(); diff --git a/Tests/Fixtures/response-functional/cookie_samesite_strict.expected b/Tests/Fixtures/response-functional/cookie_samesite_strict.expected new file mode 100644 index 000000000..adc491fd2 --- /dev/null +++ b/Tests/Fixtures/response-functional/cookie_samesite_strict.expected @@ -0,0 +1,9 @@ + +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: CookieSamesiteStrictTest=StrictValue; path=/; httponly; samesite=strict +) +shutdown diff --git a/Tests/Fixtures/response-functional/cookie_samesite_strict.php b/Tests/Fixtures/response-functional/cookie_samesite_strict.php new file mode 100644 index 000000000..3bcb41f8f --- /dev/null +++ b/Tests/Fixtures/response-functional/cookie_samesite_strict.php @@ -0,0 +1,8 @@ +headers->setCookie(new Cookie('CookieSamesiteStrictTest', 'StrictValue', 0, '/', null, false, true, false, Cookie::SAMESITE_STRICT)); +$r->sendHeaders(); diff --git a/Tests/Fixtures/response-functional/cookie_urlencode.expected b/Tests/Fixtures/response-functional/cookie_urlencode.expected new file mode 100644 index 000000000..17a9efc66 --- /dev/null +++ b/Tests/Fixtures/response-functional/cookie_urlencode.expected @@ -0,0 +1,11 @@ + +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: %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 new file mode 100644 index 000000000..9ffb0dfec --- /dev/null +++ b/Tests/Fixtures/response-functional/cookie_urlencode.php @@ -0,0 +1,15 @@ +headers->setCookie(new Cookie($str1, $str1, 0, '', null, false, false, false, null)); + +$str2 = '?*():@&+$/%#[]'; + +$r->headers->setCookie(new Cookie($str2, $str2, 0, '', null, false, false, false, null)); +$r->sendHeaders(); + +setcookie($str2, $str2, 0, '/'); diff --git a/Tests/Fixtures/response-functional/invalid_cookie_name.expected b/Tests/Fixtures/response-functional/invalid_cookie_name.expected new file mode 100644 index 000000000..2b560f0bd --- /dev/null +++ b/Tests/Fixtures/response-functional/invalid_cookie_name.expected @@ -0,0 +1,6 @@ +The cookie name "Hello + world" contains invalid characters. +Array +( + [0] => Content-Type: text/plain; charset=utf-8 +) +shutdown diff --git a/Tests/Fixtures/response-functional/invalid_cookie_name.php b/Tests/Fixtures/response-functional/invalid_cookie_name.php new file mode 100644 index 000000000..3acf86039 --- /dev/null +++ b/Tests/Fixtures/response-functional/invalid_cookie_name.php @@ -0,0 +1,11 @@ +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 1acf59308..cabe038bd 100644 --- a/Tests/HeaderBagTest.php +++ b/Tests/HeaderBagTest.php @@ -18,7 +18,7 @@ class HeaderBagTest extends TestCase { public function testConstructor() { - $bag = new HeaderBag(array('foo' => 'bar')); + $bag = new HeaderBag(['foo' => 'bar']); $this->assertTrue($bag->has('foo')); } @@ -30,31 +30,36 @@ public function testToStringNull() public function testToStringNotNull() { - $bag = new HeaderBag(array('foo' => 'bar')); + $bag = new HeaderBag(['foo' => 'bar']); $this->assertEquals("Foo: bar\r\n", $bag->__toString()); } public function testKeys() { - $bag = new HeaderBag(array('foo' => 'bar')); + $bag = new HeaderBag(['foo' => 'bar']); $keys = $bag->keys(); $this->assertEquals('foo', $keys[0]); } public function testGetDate() { - $bag = new HeaderBag(array('foo' => 'Tue, 4 Sep 2012 20:00:00 +0200')); + $bag = new HeaderBag(['foo' => 'Tue, 4 Sep 2012 20:00:00 +0200']); $headerDate = $bag->getDate('foo'); $this->assertInstanceOf('DateTime', $headerDate); } - /** - * @expectedException \RuntimeException - */ - public function testGetDateException() + public function testGetDateNull() { - $bag = new HeaderBag(array('foo' => 'Tue')); + $bag = new HeaderBag(['foo' => null]); $headerDate = $bag->getDate('foo'); + $this->assertNull($headerDate); + } + + public function testGetDateException() + { + $this->expectException('RuntimeException'); + $bag = new HeaderBag(['foo' => 'Tue']); + $bag->getDate('foo'); } public function testGetCacheControlHeader() @@ -67,50 +72,53 @@ public function testGetCacheControlHeader() public function testAll() { - $bag = new HeaderBag(array('foo' => 'bar')); - $this->assertEquals(array('foo' => array('bar')), $bag->all(), '->all() gets all the input'); + $bag = new HeaderBag(['foo' => 'bar']); + $this->assertEquals(['foo' => ['bar']], $bag->all(), '->all() gets all the input'); - $bag = new HeaderBag(array('FOO' => 'BAR')); - $this->assertEquals(array('foo' => array('BAR')), $bag->all(), '->all() gets all the input key are lower case'); + $bag = new HeaderBag(['FOO' => 'BAR']); + $this->assertEquals(['foo' => ['BAR']], $bag->all(), '->all() gets all the input key are lower case'); } public function testReplace() { - $bag = new HeaderBag(array('foo' => 'bar')); + $bag = new HeaderBag(['foo' => 'bar']); - $bag->replace(array('NOPE' => 'BAR')); - $this->assertEquals(array('nope' => array('BAR')), $bag->all(), '->replace() replaces the input with the argument'); + $bag->replace(['NOPE' => 'BAR']); + $this->assertEquals(['nope' => ['BAR']], $bag->all(), '->replace() replaces the input with the argument'); $this->assertFalse($bag->has('foo'), '->replace() overrides previously set the input'); } public function testGet() { - $bag = new HeaderBag(array('foo' => 'bar', 'fuzz' => 'bizz')); + $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(array('bar'), $bag->get('foo', 'nope', false), '->get return the value as array'); + $this->assertEquals(['bar'], $bag->get('foo', 'nope', false), '->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(array('default'), $bag->get('none', 'default', false), '->get unknown values returns default as array'); + $this->assertEquals(['default'], $bag->get('none', 'default', false), '->get unknown values returns default as array'); $bag->set('foo', 'bor', false); $this->assertEquals('bar', $bag->get('foo'), '->get return first value'); - $this->assertEquals(array('bar', 'bor'), $bag->get('foo', 'nope', false), '->get return all values as array'); + $this->assertEquals(['bar', 'bor'], $bag->get('foo', 'nope', false), '->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'); } public function testSetAssociativeArray() { $bag = new HeaderBag(); - $bag->set('foo', array('bad-assoc-index' => 'value')); + $bag->set('foo', ['bad-assoc-index' => 'value']); $this->assertSame('value', $bag->get('foo')); - $this->assertEquals(array('value'), $bag->get('foo', 'nope', false), 'assoc indices of multi-valued headers are ignored'); + $this->assertEquals(['value'], $bag->get('foo', 'nope', false), 'assoc indices of multi-valued headers are ignored'); } public function testContains() { - $bag = new HeaderBag(array('foo' => 'bar', 'fuzz' => 'bizz')); + $bag = new HeaderBag(['foo' => 'bar', 'fuzz' => 'bizz']); $this->assertTrue($bag->contains('foo', 'bar'), '->contains first value'); $this->assertTrue($bag->contains('fuzz', 'bizz'), '->contains second value'); $this->assertFalse($bag->contains('nope', 'nope'), '->contains unknown value'); @@ -143,7 +151,7 @@ public function testCacheControlDirectiveAccessors() public function testCacheControlDirectiveParsing() { - $bag = new HeaderBag(array('cache-control' => 'public, max-age=10')); + $bag = new HeaderBag(['cache-control' => 'public, max-age=10']); $this->assertTrue($bag->hasCacheControlDirective('public')); $this->assertTrue($bag->getCacheControlDirective('public')); @@ -156,15 +164,15 @@ public function testCacheControlDirectiveParsing() public function testCacheControlDirectiveParsingQuotedZero() { - $bag = new HeaderBag(array('cache-control' => 'max-age="0"')); + $bag = new HeaderBag(['cache-control' => 'max-age="0"']); $this->assertTrue($bag->hasCacheControlDirective('max-age')); $this->assertEquals(0, $bag->getCacheControlDirective('max-age')); } public function testCacheControlDirectiveOverrideWithReplace() { - $bag = new HeaderBag(array('cache-control' => 'private, max-age=100')); - $bag->replace(array('cache-control' => 'public, max-age=10')); + $bag = new HeaderBag(['cache-control' => 'private, max-age=100']); + $bag->replace(['cache-control' => 'public, max-age=10']); $this->assertTrue($bag->hasCacheControlDirective('public')); $this->assertTrue($bag->getCacheControlDirective('public')); @@ -174,7 +182,7 @@ public function testCacheControlDirectiveOverrideWithReplace() public function testCacheControlClone() { - $headers = array('foo' => 'bar'); + $headers = ['foo' => 'bar']; $bag1 = new HeaderBag($headers); $bag2 = new HeaderBag($bag1->all()); @@ -183,23 +191,23 @@ public function testCacheControlClone() public function testGetIterator() { - $headers = array('foo' => 'bar', 'hello' => 'world', 'third' => 'charm'); + $headers = ['foo' => 'bar', 'hello' => 'world', 'third' => 'charm']; $headerBag = new HeaderBag($headers); $i = 0; foreach ($headerBag as $key => $val) { ++$i; - $this->assertEquals(array($headers[$key]), $val); + $this->assertEquals([$headers[$key]], $val); } - $this->assertEquals(count($headers), $i); + $this->assertEquals(\count($headers), $i); } public function testCount() { - $headers = array('foo' => 'bar', 'HELLO' => 'WORLD'); + $headers = ['foo' => 'bar', 'HELLO' => 'WORLD']; $headerBag = new HeaderBag($headers); - $this->assertEquals(count($headers), count($headerBag)); + $this->assertCount(\count($headers), $headerBag); } } diff --git a/Tests/IpUtilsTest.php b/Tests/IpUtilsTest.php index 297ee3d8d..d3b262e04 100644 --- a/Tests/IpUtilsTest.php +++ b/Tests/IpUtilsTest.php @@ -26,20 +26,20 @@ public function testIpv4($matches, $remoteAddr, $cidr) public function getIpv4Data() { - return array( - array(true, '192.168.1.1', '192.168.1.1'), - array(true, '192.168.1.1', '192.168.1.1/1'), - array(true, '192.168.1.1', '192.168.1.0/24'), - array(false, '192.168.1.1', '1.2.3.4/1'), - array(false, '192.168.1.1', '192.168.1.1/33'), // invalid subnet - array(true, '192.168.1.1', array('1.2.3.4/1', '192.168.1.0/24')), - array(true, '192.168.1.1', array('192.168.1.0/24', '1.2.3.4/1')), - array(false, '192.168.1.1', array('1.2.3.4/1', '4.3.2.1/1')), - array(true, '1.2.3.4', '0.0.0.0/0'), - array(true, '1.2.3.4', '192.168.1.0/0'), - array(false, '1.2.3.4', '256.256.256/0'), // invalid CIDR notation - array(false, 'an_invalid_ip', '192.168.1.0/24'), - ); + return [ + [true, '192.168.1.1', '192.168.1.1'], + [true, '192.168.1.1', '192.168.1.1/1'], + [true, '192.168.1.1', '192.168.1.0/24'], + [false, '192.168.1.1', '1.2.3.4/1'], + [false, '192.168.1.1', '192.168.1.1/33'], // invalid subnet + [true, '192.168.1.1', ['1.2.3.4/1', '192.168.1.0/24']], + [true, '192.168.1.1', ['192.168.1.0/24', '1.2.3.4/1']], + [false, '192.168.1.1', ['1.2.3.4/1', '4.3.2.1/1']], + [true, '1.2.3.4', '0.0.0.0/0'], + [true, '1.2.3.4', '192.168.1.0/0'], + [false, '1.2.3.4', '256.256.256/0'], // invalid CIDR notation + [false, 'an_invalid_ip', '192.168.1.0/24'], + ]; } /** @@ -47,7 +47,7 @@ public function getIpv4Data() */ public function testIpv6($matches, $remoteAddr, $cidr) { - if (!defined('AF_INET6')) { + if (!\defined('AF_INET6')) { $this->markTestSkipped('Only works when PHP is compiled without the option "disable-ipv6".'); } @@ -56,30 +56,49 @@ public function testIpv6($matches, $remoteAddr, $cidr) public function getIpv6Data() { - return array( - array(true, '2a01:198:603:0:396e:4789:8e99:890f', '2a01:198:603:0::/65'), - array(false, '2a00:198:603:0:396e:4789:8e99:890f', '2a01:198:603:0::/65'), - array(false, '2a01:198:603:0:396e:4789:8e99:890f', '::1'), - array(true, '0:0:0:0:0:0:0:1', '::1'), - array(false, '0:0:603:0:396e:4789:8e99:0001', '::1'), - array(true, '2a01:198:603:0:396e:4789:8e99:890f', array('::1', '2a01:198:603:0::/65')), - array(true, '2a01:198:603:0:396e:4789:8e99:890f', array('2a01:198:603:0::/65', '::1')), - array(false, '2a01:198:603:0:396e:4789:8e99:890f', array('::1', '1a01:198:603:0::/65')), - array(false, '}__test|O:21:"JDatabaseDriverMysqli":3:{s:2', '::1'), - array(false, '2a01:198:603:0:396e:4789:8e99:890f', 'unknown'), - ); + return [ + [true, '2a01:198:603:0:396e:4789:8e99:890f', '2a01:198:603:0::/65'], + [false, '2a00:198:603:0:396e:4789:8e99:890f', '2a01:198:603:0::/65'], + [false, '2a01:198:603:0:396e:4789:8e99:890f', '::1'], + [true, '0:0:0:0:0:0:0:1', '::1'], + [false, '0:0:603:0:396e:4789:8e99:0001', '::1'], + [true, '0:0:603:0:396e:4789:8e99:0001', '::/0'], + [true, '0:0:603:0:396e:4789:8e99:0001', '2a01:198:603:0::/0'], + [true, '2a01:198:603:0:396e:4789:8e99:890f', ['::1', '2a01:198:603:0::/65']], + [true, '2a01:198:603:0:396e:4789:8e99:890f', ['2a01:198:603:0::/65', '::1']], + [false, '2a01:198:603:0:396e:4789:8e99:890f', ['::1', '1a01:198:603:0::/65']], + [false, '}__test|O:21:"JDatabaseDriverMysqli":3:{s:2', '::1'], + [false, '2a01:198:603:0:396e:4789:8e99:890f', 'unknown'], + ]; } /** - * @expectedException \RuntimeException * @requires extension sockets */ public function testAnIpv6WithOptionDisabledIpv6() { - if (defined('AF_INET6')) { + $this->expectException('RuntimeException'); + if (\defined('AF_INET6')) { $this->markTestSkipped('Only works when PHP is compiled with the option "disable-ipv6".'); } IpUtils::checkIp('2a01:198:603:0:396e:4789:8e99:890f', '2a01:198:603:0::/65'); } + + /** + * @dataProvider invalidIpAddressData + */ + public function testInvalidIpAddressesDoNotMatch($requestIp, $proxyIp) + { + $this->assertFalse(IpUtils::checkIp4($requestIp, $proxyIp)); + } + + public function invalidIpAddressData() + { + return [ + 'invalid proxy wildcard' => ['192.168.20.13', '*'], + 'invalid proxy missing netmask' => ['192.168.20.13', '0.0.0.0'], + 'invalid request IP with invalid proxy wildcard' => ['0.0.0.0', '*'], + ]; + } } diff --git a/Tests/JsonResponseTest.php b/Tests/JsonResponseTest.php index c8b937789..9642dc28d 100644 --- a/Tests/JsonResponseTest.php +++ b/Tests/JsonResponseTest.php @@ -16,6 +16,15 @@ class JsonResponseTest extends TestCase { + protected function setUp() + { + parent::setUp(); + + if (!\defined('HHVM_VERSION')) { + $this->iniSet('serialize_precision', 14); + } + } + public function testConstructorEmptyCreatesJsonObject() { $response = new JsonResponse(); @@ -24,13 +33,13 @@ public function testConstructorEmptyCreatesJsonObject() public function testConstructorWithArrayCreatesJsonArray() { - $response = new JsonResponse(array(0, 1, 2, 3)); + $response = new JsonResponse([0, 1, 2, 3]); $this->assertSame('[0,1,2,3]', $response->getContent()); } public function testConstructorWithAssocArrayCreatesJsonObject() { - $response = new JsonResponse(array('foo' => 'bar')); + $response = new JsonResponse(['foo' => 'bar']); $this->assertSame('{"foo":"bar"}', $response->getContent()); } @@ -43,7 +52,8 @@ public function testConstructorWithSimpleTypes() $this->assertSame('0', $response->getContent()); $response = new JsonResponse(0.1); - $this->assertSame('0.1', $response->getContent()); + $this->assertEquals(0.1, $response->getContent()); + $this->assertIsString($response->getContent()); $response = new JsonResponse(true); $this->assertSame('true', $response->getContent()); @@ -51,7 +61,7 @@ public function testConstructorWithSimpleTypes() public function testConstructorWithCustomStatus() { - $response = new JsonResponse(array(), 202); + $response = new JsonResponse([], 202); $this->assertSame(202, $response->getStatusCode()); } @@ -63,35 +73,35 @@ public function testConstructorAddsContentTypeHeader() public function testConstructorWithCustomHeaders() { - $response = new JsonResponse(array(), 200, array('ETag' => 'foo')); + $response = new JsonResponse([], 200, ['ETag' => 'foo']); $this->assertSame('application/json', $response->headers->get('Content-Type')); $this->assertSame('foo', $response->headers->get('ETag')); } public function testConstructorWithCustomContentType() { - $headers = array('Content-Type' => 'application/vnd.acme.blog-v1+json'); + $headers = ['Content-Type' => 'application/vnd.acme.blog-v1+json']; - $response = new JsonResponse(array(), 200, $headers); + $response = new JsonResponse([], 200, $headers); $this->assertSame('application/vnd.acme.blog-v1+json', $response->headers->get('Content-Type')); } public function testSetJson() { - $response = new JsonResponse('1', 200, array(), true); + $response = new JsonResponse('1', 200, [], true); $this->assertEquals('1', $response->getContent()); - $response = new JsonResponse('[1]', 200, array(), true); + $response = new JsonResponse('[1]', 200, [], true); $this->assertEquals('[1]', $response->getContent()); - $response = new JsonResponse(null, 200, array()); + $response = new JsonResponse(null, 200, []); $response->setJson('true'); $this->assertEquals('true', $response->getContent()); } public function testCreate() { - $response = JsonResponse::create(array('foo' => 'bar'), 204); + $response = JsonResponse::create(['foo' => 'bar'], 204); $this->assertInstanceOf('Symfony\Component\HttpFoundation\JsonResponse', $response); $this->assertEquals('{"foo":"bar"}', $response->getContent()); @@ -107,14 +117,14 @@ public function testStaticCreateEmptyJsonObject() public function testStaticCreateJsonArray() { - $response = JsonResponse::create(array(0, 1, 2, 3)); + $response = JsonResponse::create([0, 1, 2, 3]); $this->assertInstanceOf('Symfony\Component\HttpFoundation\JsonResponse', $response); $this->assertSame('[0,1,2,3]', $response->getContent()); } public function testStaticCreateJsonObject() { - $response = JsonResponse::create(array('foo' => 'bar')); + $response = JsonResponse::create(['foo' => 'bar']); $this->assertInstanceOf('Symfony\Component\HttpFoundation\JsonResponse', $response); $this->assertSame('{"foo":"bar"}', $response->getContent()); } @@ -131,7 +141,8 @@ public function testStaticCreateWithSimpleTypes() $response = JsonResponse::create(0.1); $this->assertInstanceOf('Symfony\Component\HttpFoundation\JsonResponse', $response); - $this->assertSame('0.1', $response->getContent()); + $this->assertEquals(0.1, $response->getContent()); + $this->assertIsString($response->getContent()); $response = JsonResponse::create(true); $this->assertInstanceOf('Symfony\Component\HttpFoundation\JsonResponse', $response); @@ -140,7 +151,7 @@ public function testStaticCreateWithSimpleTypes() public function testStaticCreateWithCustomStatus() { - $response = JsonResponse::create(array(), 202); + $response = JsonResponse::create([], 202); $this->assertSame(202, $response->getStatusCode()); } @@ -152,22 +163,22 @@ public function testStaticCreateAddsContentTypeHeader() public function testStaticCreateWithCustomHeaders() { - $response = JsonResponse::create(array(), 200, array('ETag' => 'foo')); + $response = JsonResponse::create([], 200, ['ETag' => 'foo']); $this->assertSame('application/json', $response->headers->get('Content-Type')); $this->assertSame('foo', $response->headers->get('ETag')); } public function testStaticCreateWithCustomContentType() { - $headers = array('Content-Type' => 'application/vnd.acme.blog-v1+json'); + $headers = ['Content-Type' => 'application/vnd.acme.blog-v1+json']; - $response = JsonResponse::create(array(), 200, $headers); + $response = JsonResponse::create([], 200, $headers); $this->assertSame('application/vnd.acme.blog-v1+json', $response->headers->get('Content-Type')); } public function testSetCallback() { - $response = JsonResponse::create(array('foo' => 'bar'))->setCallback('callback'); + $response = JsonResponse::create(['foo' => 'bar'])->setCallback('callback'); $this->assertEquals('/**/callback({"foo":"bar"});', $response->getContent()); $this->assertEquals('text/javascript', $response->headers->get('Content-Type')); @@ -190,7 +201,7 @@ public function testGetEncodingOptions() public function testSetEncodingOptions() { $response = new JsonResponse(); - $response->setData(array(array(1, 2, 3))); + $response->setData([[1, 2, 3]]); $this->assertEquals('[[1,2,3]]', $response->getContent()); @@ -205,29 +216,27 @@ 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.'); + } + $serializable = new JsonSerializableObject(); JsonResponse::create($serializable); @@ -235,14 +244,14 @@ public function testSetContentJsonSerializeError() public function testSetComplexCallback() { - $response = JsonResponse::create(array('foo' => 'bar')); + $response = JsonResponse::create(['foo' => 'bar']); $response->setCallback('ಠ_ಠ["foo"].bar[0]'); $this->assertEquals('/**/ಠ_ಠ["foo"].bar[0]({"foo":"bar"});', $response->getContent()); } } -if (interface_exists('JsonSerializable')) { +if (interface_exists('JsonSerializable', false)) { class JsonSerializableObject implements \JsonSerializable { public function jsonSerialize() diff --git a/Tests/ParameterBagTest.php b/Tests/ParameterBagTest.php index 5311a0d80..d2a5c991c 100644 --- a/Tests/ParameterBagTest.php +++ b/Tests/ParameterBagTest.php @@ -23,44 +23,44 @@ public function testConstructor() public function testAll() { - $bag = new ParameterBag(array('foo' => 'bar')); - $this->assertEquals(array('foo' => 'bar'), $bag->all(), '->all() gets all the input'); + $bag = new ParameterBag(['foo' => 'bar']); + $this->assertEquals(['foo' => 'bar'], $bag->all(), '->all() gets all the input'); } public function testKeys() { - $bag = new ParameterBag(array('foo' => 'bar')); - $this->assertEquals(array('foo'), $bag->keys()); + $bag = new ParameterBag(['foo' => 'bar']); + $this->assertEquals(['foo'], $bag->keys()); } public function testAdd() { - $bag = new ParameterBag(array('foo' => 'bar')); - $bag->add(array('bar' => 'bas')); - $this->assertEquals(array('foo' => 'bar', 'bar' => 'bas'), $bag->all()); + $bag = new ParameterBag(['foo' => 'bar']); + $bag->add(['bar' => 'bas']); + $this->assertEquals(['foo' => 'bar', 'bar' => 'bas'], $bag->all()); } public function testRemove() { - $bag = new ParameterBag(array('foo' => 'bar')); - $bag->add(array('bar' => 'bas')); - $this->assertEquals(array('foo' => 'bar', 'bar' => 'bas'), $bag->all()); + $bag = new ParameterBag(['foo' => 'bar']); + $bag->add(['bar' => 'bas']); + $this->assertEquals(['foo' => 'bar', 'bar' => 'bas'], $bag->all()); $bag->remove('bar'); - $this->assertEquals(array('foo' => 'bar'), $bag->all()); + $this->assertEquals(['foo' => 'bar'], $bag->all()); } public function testReplace() { - $bag = new ParameterBag(array('foo' => 'bar')); + $bag = new ParameterBag(['foo' => 'bar']); - $bag->replace(array('FOO' => 'BAR')); - $this->assertEquals(array('FOO' => 'BAR'), $bag->all(), '->replace() replaces the input with the argument'); + $bag->replace(['FOO' => 'BAR']); + $this->assertEquals(['FOO' => 'BAR'], $bag->all(), '->replace() replaces the input with the argument'); $this->assertFalse($bag->has('foo'), '->replace() overrides previously set the input'); } public function testGet() { - $bag = new ParameterBag(array('foo' => 'bar', 'null' => null)); + $bag = new ParameterBag(['foo' => 'bar', 'null' => null]); $this->assertEquals('bar', $bag->get('foo'), '->get() gets the value of a parameter'); $this->assertEquals('default', $bag->get('unknown', 'default'), '->get() returns second argument as default if a parameter is not defined'); @@ -69,14 +69,14 @@ public function testGet() public function testGetDoesNotUseDeepByDefault() { - $bag = new ParameterBag(array('foo' => array('bar' => 'moo'))); + $bag = new ParameterBag(['foo' => ['bar' => 'moo']]); $this->assertNull($bag->get('foo[bar]')); } public function testSet() { - $bag = new ParameterBag(array()); + $bag = new ParameterBag([]); $bag->set('foo', 'bar'); $this->assertEquals('bar', $bag->get('foo'), '->set() sets the value of parameter'); @@ -87,7 +87,7 @@ public function testSet() public function testHas() { - $bag = new ParameterBag(array('foo' => 'bar')); + $bag = new ParameterBag(['foo' => 'bar']); $this->assertTrue($bag->has('foo'), '->has() returns true if a parameter is defined'); $this->assertFalse($bag->has('unknown'), '->has() return false if a parameter is not defined'); @@ -95,7 +95,7 @@ public function testHas() public function testGetAlpha() { - $bag = new ParameterBag(array('word' => 'foo_BAR_012')); + $bag = new ParameterBag(['word' => 'foo_BAR_012']); $this->assertEquals('fooBAR', $bag->getAlpha('word'), '->getAlpha() gets only alphabetic characters'); $this->assertEquals('', $bag->getAlpha('unknown'), '->getAlpha() returns empty string if a parameter is not defined'); @@ -103,7 +103,7 @@ public function testGetAlpha() public function testGetAlnum() { - $bag = new ParameterBag(array('word' => 'foo_BAR_012')); + $bag = new ParameterBag(['word' => 'foo_BAR_012']); $this->assertEquals('fooBAR012', $bag->getAlnum('word'), '->getAlnum() gets only alphanumeric characters'); $this->assertEquals('', $bag->getAlnum('unknown'), '->getAlnum() returns empty string if a parameter is not defined'); @@ -111,7 +111,7 @@ public function testGetAlnum() public function testGetDigits() { - $bag = new ParameterBag(array('word' => 'foo_BAR_012')); + $bag = new ParameterBag(['word' => 'foo_BAR_012']); $this->assertEquals('012', $bag->getDigits('word'), '->getDigits() gets only digits as string'); $this->assertEquals('', $bag->getDigits('unknown'), '->getDigits() returns empty string if a parameter is not defined'); @@ -119,7 +119,7 @@ public function testGetDigits() public function testGetInt() { - $bag = new ParameterBag(array('digits' => '0123')); + $bag = new ParameterBag(['digits' => '0123']); $this->assertEquals(123, $bag->getInt('digits'), '->getInt() gets a value of parameter as integer'); $this->assertEquals(0, $bag->getInt('unknown'), '->getInt() returns zero if a parameter is not defined'); @@ -127,14 +127,14 @@ public function testGetInt() public function testFilter() { - $bag = new ParameterBag(array( + $bag = new ParameterBag([ 'digits' => '0123ab', 'email' => 'example@example.com', 'url' => 'http://example.com/foo', 'dec' => '256', 'hex' => '0x100', - 'array' => array('bang'), - )); + 'array' => ['bang'], + ]); $this->assertEmpty($bag->filter('nokey'), '->filter() should return empty by default if no key is found'); @@ -142,27 +142,27 @@ public function testFilter() $this->assertEquals('example@example.com', $bag->filter('email', '', FILTER_VALIDATE_EMAIL), '->filter() gets a value of parameter as email'); - $this->assertEquals('http://example.com/foo', $bag->filter('url', '', FILTER_VALIDATE_URL, array('flags' => FILTER_FLAG_PATH_REQUIRED)), '->filter() gets a value of parameter as URL with a path'); + $this->assertEquals('http://example.com/foo', $bag->filter('url', '', FILTER_VALIDATE_URL, ['flags' => FILTER_FLAG_PATH_REQUIRED]), '->filter() gets a value of parameter as URL with a path'); // This test is repeated for code-coverage $this->assertEquals('http://example.com/foo', $bag->filter('url', '', FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED), '->filter() gets a value of parameter as URL with a path'); - $this->assertFalse($bag->filter('dec', '', FILTER_VALIDATE_INT, array( + $this->assertFalse($bag->filter('dec', '', FILTER_VALIDATE_INT, [ 'flags' => FILTER_FLAG_ALLOW_HEX, - 'options' => array('min_range' => 1, 'max_range' => 0xff), - )), '->filter() gets a value of parameter as integer between boundaries'); + 'options' => ['min_range' => 1, 'max_range' => 0xff], + ]), '->filter() gets a value of parameter as integer between boundaries'); - $this->assertFalse($bag->filter('hex', '', FILTER_VALIDATE_INT, array( + $this->assertFalse($bag->filter('hex', '', FILTER_VALIDATE_INT, [ 'flags' => FILTER_FLAG_ALLOW_HEX, - 'options' => array('min_range' => 1, 'max_range' => 0xff), - )), '->filter() gets a value of parameter as integer between boundaries'); + 'options' => ['min_range' => 1, 'max_range' => 0xff], + ]), '->filter() gets a value of parameter as integer between boundaries'); - $this->assertEquals(array('bang'), $bag->filter('array', ''), '->filter() gets a value of parameter as an array'); + $this->assertEquals(['bang'], $bag->filter('array', ''), '->filter() gets a value of parameter as an array'); } public function testGetIterator() { - $parameters = array('foo' => 'bar', 'hello' => 'world'); + $parameters = ['foo' => 'bar', 'hello' => 'world']; $bag = new ParameterBag($parameters); $i = 0; @@ -171,20 +171,20 @@ public function testGetIterator() $this->assertEquals($parameters[$key], $val); } - $this->assertEquals(count($parameters), $i); + $this->assertEquals(\count($parameters), $i); } public function testCount() { - $parameters = array('foo' => 'bar', 'hello' => 'world'); + $parameters = ['foo' => 'bar', 'hello' => 'world']; $bag = new ParameterBag($parameters); - $this->assertEquals(count($parameters), count($bag)); + $this->assertCount(\count($parameters), $bag); } public function testGetBoolean() { - $parameters = array('string_true' => 'true', 'string_false' => 'false'); + $parameters = ['string_true' => 'true', 'string_false' => 'false']; $bag = new ParameterBag($parameters); $this->assertTrue($bag->getBoolean('string_true'), '->getBoolean() gets the string true as boolean true'); diff --git a/Tests/RedirectResponseTest.php b/Tests/RedirectResponseTest.php index d389e83db..2bbf5aa1a 100644 --- a/Tests/RedirectResponseTest.php +++ b/Tests/RedirectResponseTest.php @@ -20,26 +20,19 @@ public function testGenerateMetaRedirect() { $response = new RedirectResponse('foo.bar'); - $this->assertEquals(1, preg_match( - '##', - preg_replace(array('/\s+/', '/\'/'), array(' ', '"'), $response->getContent()) - )); + $this->assertRegExp('##', preg_replace('/\s+/', ' ', $response->getContent())); } - /** - * @expectedException \InvalidArgumentException - */ public function testRedirectResponseConstructorNullUrl() { - $response = new RedirectResponse(null); + $this->expectException('InvalidArgumentException'); + new RedirectResponse(null); } - /** - * @expectedException \InvalidArgumentException - */ public function testRedirectResponseConstructorWrongStatusCode() { - $response = new RedirectResponse('foo.bar', 404); + $this->expectException('InvalidArgumentException'); + new RedirectResponse('foo.bar', 404); } public function testGenerateLocationHeader() @@ -65,11 +58,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); } @@ -87,7 +78,11 @@ public function testCacheHeaders() $response = new RedirectResponse('foo.bar', 301); $this->assertFalse($response->headers->hasCacheControlDirective('no-cache')); - $response = new RedirectResponse('foo.bar', 301, array('cache-control' => 'max-age=86400')); + $response = new RedirectResponse('foo.bar', 301, ['cache-control' => 'max-age=86400']); + $this->assertFalse($response->headers->hasCacheControlDirective('no-cache')); + $this->assertTrue($response->headers->hasCacheControlDirective('max-age')); + + $response = new RedirectResponse('foo.bar', 301, ['Cache-Control' => 'max-age=86400']); $this->assertFalse($response->headers->hasCacheControlDirective('no-cache')); $this->assertTrue($response->headers->hasCacheControlDirective('max-age')); diff --git a/Tests/RequestMatcherTest.php b/Tests/RequestMatcherTest.php index b5d80048f..7fb6925bb 100644 --- a/Tests/RequestMatcherTest.php +++ b/Tests/RequestMatcherTest.php @@ -12,8 +12,8 @@ namespace Symfony\Component\HttpFoundation\Tests; use PHPUnit\Framework\TestCase; -use Symfony\Component\HttpFoundation\RequestMatcher; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestMatcher; class RequestMatcherTest extends TestCase { @@ -34,20 +34,20 @@ public function testMethod($requestMethod, $matcherMethod, $isMatch) public function getMethodData() { - return array( - array('get', 'get', true), - array('get', array('get', 'post'), true), - array('get', 'post', false), - array('get', 'GET', true), - array('get', array('GET', 'POST'), true), - array('get', 'POST', false), - ); + return [ + ['get', 'get', true], + ['get', ['get', 'post'], true], + ['get', 'post', false], + ['get', 'GET', true], + ['get', ['GET', 'POST'], true], + ['get', 'POST', false], + ]; } public function testScheme() { $httpRequest = $request = $request = Request::create(''); - $httpsRequest = $request = $request = Request::create('', 'get', array(), array(), array(), array('HTTPS' => 'on')); + $httpsRequest = $request = $request = Request::create('', 'get', [], [], [], ['HTTPS' => 'on']); $matcher = new RequestMatcher(); $matcher->matchScheme('https'); @@ -69,7 +69,7 @@ public function testScheme() public function testHost($pattern, $isMatch) { $matcher = new RequestMatcher(); - $request = Request::create('', 'get', array(), array(), array(), array('HTTP_HOST' => 'foo.example.com')); + $request = Request::create('', 'get', [], [], [], ['HTTP_HOST' => 'foo.example.com']); $matcher->matchHost($pattern); $this->assertSame($isMatch, $matcher->matches($request)); @@ -80,16 +80,16 @@ public function testHost($pattern, $isMatch) public function getHostData() { - return array( - array('.*\.example\.com', true), - array('\.example\.com$', true), - array('^.*\.example\.com$', true), - array('.*\.sensio\.com', false), - array('.*\.example\.COM', true), - array('\.example\.COM$', true), - array('^.*\.example\.COM$', true), - array('.*\.sensio\.COM', false), - ); + return [ + ['.*\.example\.com', true], + ['\.example\.com$', true], + ['^.*\.example\.com$', true], + ['.*\.sensio\.com', false], + ['.*\.example\.COM', true], + ['\.example\.COM$', true], + ['^.*\.example\.COM$', true], + ['.*\.sensio\.COM', false], + ]; } public function testPath() diff --git a/Tests/RequestTest.php b/Tests/RequestTest.php index b36fbb7e9..bf70a5f83 100644 --- a/Tests/RequestTest.php +++ b/Tests/RequestTest.php @@ -13,32 +13,32 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException; -use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; -use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; class RequestTest extends TestCase { protected function tearDown() { - // reset - Request::setTrustedProxies(array(), -1); + Request::setTrustedProxies([], -1); + Request::setTrustedHosts([]); } public function testInitialize() { $request = new Request(); - $request->initialize(array('foo' => 'bar')); + $request->initialize(['foo' => 'bar']); $this->assertEquals('bar', $request->query->get('foo'), '->initialize() takes an array of query parameters as its first argument'); - $request->initialize(array(), array('foo' => 'bar')); + $request->initialize([], ['foo' => 'bar']); $this->assertEquals('bar', $request->request->get('foo'), '->initialize() takes an array of request parameters as its second argument'); - $request->initialize(array(), array(), array('foo' => 'bar')); + $request->initialize([], [], ['foo' => 'bar']); $this->assertEquals('bar', $request->attributes->get('foo'), '->initialize() takes an array of attributes as its third argument'); - $request->initialize(array(), array(), array(), array(), array(), array('HTTP_FOO' => 'bar')); + $request->initialize([], [], [], [], [], ['HTTP_FOO' => 'bar']); $this->assertEquals('bar', $request->headers->get('FOO'), '->initialize() takes an array of HTTP headers as its sixth argument'); } @@ -52,18 +52,18 @@ public function testGetLocale() public function testGetUser() { - $request = Request::create('http://user_test:password_test@test.com/'); + $request = Request::create('http://user:password@test.com'); $user = $request->getUser(); - $this->assertEquals('user_test', $user); + $this->assertEquals('user', $user); } public function testGetPassword() { - $request = Request::create('http://user_test:password_test@test.com/'); + $request = Request::create('http://user:password@test.com'); $password = $request->getPassword(); - $this->assertEquals('password_test', $password); + $this->assertEquals('password', $password); } public function testIsNoCache() @@ -101,7 +101,7 @@ public function testCreate() $this->assertEquals('test.com', $request->getHttpHost()); $this->assertFalse($request->isSecure()); - $request = Request::create('http://test.com/foo', 'GET', array('bar' => 'baz')); + $request = Request::create('http://test.com/foo', 'GET', ['bar' => 'baz']); $this->assertEquals('http://test.com/foo?bar=baz', $request->getUri()); $this->assertEquals('/foo', $request->getPathInfo()); $this->assertEquals('bar=baz', $request->getQueryString()); @@ -109,7 +109,7 @@ public function testCreate() $this->assertEquals('test.com', $request->getHttpHost()); $this->assertFalse($request->isSecure()); - $request = Request::create('http://test.com/foo?bar=foo', 'GET', array('bar' => 'baz')); + $request = Request::create('http://test.com/foo?bar=foo', 'GET', ['bar' => 'baz']); $this->assertEquals('http://test.com/foo?bar=baz', $request->getUri()); $this->assertEquals('/foo', $request->getPathInfo()); $this->assertEquals('bar=baz', $request->getQueryString()); @@ -166,7 +166,7 @@ public function testCreate() $this->assertTrue($request->isSecure()); $json = '{"jsonrpc":"2.0","method":"echo","id":7,"params":["Hello World"]}'; - $request = Request::create('http://example.com/jsonrpc', 'POST', array(), array(), array(), array(), $json); + $request = Request::create('http://example.com/jsonrpc', 'POST', [], [], [], [], $json); $this->assertEquals($json, $request->getContent()); $this->assertFalse($request->isSecure()); @@ -216,28 +216,126 @@ public function testCreate() $request = Request::create('http://test.com/?foo'); $this->assertEquals('/?foo', $request->getRequestUri()); - $this->assertEquals(array('foo' => ''), $request->query->all()); + $this->assertEquals(['foo' => ''], $request->query->all()); // assume rewrite rule: (.*) --> app/app.php; app/ is a symlink to a symfony web/ directory - $request = Request::create('http://test.com/apparthotel-1234', 'GET', array(), array(), array(), - array( + $request = Request::create('http://test.com/apparthotel-1234', 'GET', [], [], [], + [ 'DOCUMENT_ROOT' => '/var/www/www.test.com', 'SCRIPT_FILENAME' => '/var/www/www.test.com/app/app.php', 'SCRIPT_NAME' => '/app/app.php', 'PHP_SELF' => '/app/app.php/apparthotel-1234', - )); + ]); $this->assertEquals('http://test.com/apparthotel-1234', $request->getUri()); $this->assertEquals('/apparthotel-1234', $request->getPathInfo()); $this->assertEquals('', $request->getQueryString()); $this->assertEquals(80, $request->getPort()); $this->assertEquals('test.com', $request->getHttpHost()); $this->assertFalse($request->isSecure()); + + // Fragment should not be included in the URI + $request = Request::create('http://test.com/foo#bar'); + $this->assertEquals('http://test.com/foo', $request->getUri()); + } + + public function testCreateWithRequestUri() + { + $request = Request::create('http://test.com:80/foo'); + $request->server->set('REQUEST_URI', 'http://test.com:80/foo'); + $this->assertEquals('http://test.com/foo', $request->getUri()); + $this->assertEquals('/foo', $request->getPathInfo()); + $this->assertEquals('test.com', $request->getHost()); + $this->assertEquals('test.com', $request->getHttpHost()); + $this->assertEquals(80, $request->getPort()); + $this->assertFalse($request->isSecure()); + + $request = Request::create('http://test.com:8080/foo'); + $request->server->set('REQUEST_URI', 'http://test.com:8080/foo'); + $this->assertEquals('http://test.com:8080/foo', $request->getUri()); + $this->assertEquals('/foo', $request->getPathInfo()); + $this->assertEquals('test.com', $request->getHost()); + $this->assertEquals('test.com:8080', $request->getHttpHost()); + $this->assertEquals(8080, $request->getPort()); + $this->assertFalse($request->isSecure()); + + $request = Request::create('http://test.com/foo?bar=foo', 'GET', ['bar' => 'baz']); + $request->server->set('REQUEST_URI', 'http://test.com/foo?bar=foo'); + $this->assertEquals('http://test.com/foo?bar=baz', $request->getUri()); + $this->assertEquals('/foo', $request->getPathInfo()); + $this->assertEquals('bar=baz', $request->getQueryString()); + $this->assertEquals('test.com', $request->getHost()); + $this->assertEquals('test.com', $request->getHttpHost()); + $this->assertEquals(80, $request->getPort()); + $this->assertFalse($request->isSecure()); + + $request = Request::create('https://test.com:443/foo'); + $request->server->set('REQUEST_URI', 'https://test.com:443/foo'); + $this->assertEquals('https://test.com/foo', $request->getUri()); + $this->assertEquals('/foo', $request->getPathInfo()); + $this->assertEquals('test.com', $request->getHost()); + $this->assertEquals('test.com', $request->getHttpHost()); + $this->assertEquals(443, $request->getPort()); + $this->assertTrue($request->isSecure()); + + // Fragment should not be included in the URI + $request = Request::create('http://test.com/foo#bar'); + $request->server->set('REQUEST_URI', 'http://test.com/foo#bar'); + $this->assertEquals('http://test.com/foo', $request->getUri()); + } + + /** + * @dataProvider getRequestUriData + */ + public function testGetRequestUri($serverRequestUri, $expected, $message) + { + $request = new Request(); + $request->server->add([ + 'REQUEST_URI' => $serverRequestUri, + + // For having http://test.com + 'SERVER_NAME' => 'test.com', + 'SERVER_PORT' => 80, + ]); + + $this->assertSame($expected, $request->getRequestUri(), $message); + $this->assertSame($expected, $request->server->get('REQUEST_URI'), 'Normalize the request URI.'); + } + + public function getRequestUriData() + { + $message = 'Do not modify the path.'; + yield ['/foo', '/foo', $message]; + yield ['//bar/foo', '//bar/foo', $message]; + yield ['///bar/foo', '///bar/foo', $message]; + + $message = 'Handle when the scheme, host are on REQUEST_URI.'; + yield ['http://test.com/foo?bar=baz', '/foo?bar=baz', $message]; + + $message = 'Handle when the scheme, host and port are on REQUEST_URI.'; + yield ['http://test.com:80/foo', '/foo', $message]; + yield ['https://test.com:8080/foo', '/foo', $message]; + yield ['https://test.com:443/foo', '/foo', $message]; + + $message = 'Fragment should not be included in the URI'; + yield ['http://test.com/foo#bar', '/foo', $message]; + yield ['/foo#bar', '/foo', $message]; + } + + public function testGetRequestUriWithoutRequiredHeader() + { + $expected = ''; + + $request = new Request(); + + $message = 'Fallback to empty URI when headers are missing.'; + $this->assertSame($expected, $request->getRequestUri(), $message); + $this->assertSame($expected, $request->server->get('REQUEST_URI'), 'Normalize the request URI.'); } public function testCreateCheckPrecedence() { // server is used by default - $request = Request::create('/', 'DELETE', array(), array(), array(), array( + $request = Request::create('/', 'DELETE', [], [], [], [ 'HTTP_HOST' => 'example.com', 'HTTPS' => 'on', 'SERVER_PORT' => 443, @@ -245,7 +343,7 @@ public function testCreateCheckPrecedence() 'PHP_AUTH_PW' => 'pa$$', 'QUERY_STRING' => 'foo=bar', 'CONTENT_TYPE' => 'application/json', - )); + ]); $this->assertEquals('example.com', $request->getHost()); $this->assertEquals(443, $request->getPort()); $this->assertTrue($request->isSecure()); @@ -255,11 +353,11 @@ public function testCreateCheckPrecedence() $this->assertEquals('application/json', $request->headers->get('CONTENT_TYPE')); // URI has precedence over server - $request = Request::create('http://thomas:pokemon@example.net:8080/?foo=bar', 'GET', array(), array(), array(), array( + $request = Request::create('http://thomas:pokemon@example.net:8080/?foo=bar', 'GET', [], [], [], [ 'HTTP_HOST' => 'example.com', 'HTTPS' => 'on', 'SERVER_PORT' => 443, - )); + ]); $this->assertEquals('example.net', $request->getHost()); $this->assertEquals(8080, $request->getPort()); $this->assertFalse($request->isSecure()); @@ -270,7 +368,7 @@ public function testCreateCheckPrecedence() public function testDuplicate() { - $request = new Request(array('foo' => 'bar'), array('foo' => 'bar'), array('foo' => 'bar'), array(), array(), array('HTTP_FOO' => 'bar')); + $request = new Request(['foo' => 'bar'], ['foo' => 'bar'], ['foo' => 'bar'], [], [], ['HTTP_FOO' => 'bar']); $dup = $request->duplicate(); $this->assertEquals($request->query->all(), $dup->query->all(), '->duplicate() duplicates a request an copy the current query parameters'); @@ -278,17 +376,17 @@ public function testDuplicate() $this->assertEquals($request->attributes->all(), $dup->attributes->all(), '->duplicate() duplicates a request an copy the current attributes'); $this->assertEquals($request->headers->all(), $dup->headers->all(), '->duplicate() duplicates a request an copy the current HTTP headers'); - $dup = $request->duplicate(array('foo' => 'foobar'), array('foo' => 'foobar'), array('foo' => 'foobar'), array(), array(), array('HTTP_FOO' => 'foobar')); + $dup = $request->duplicate(['foo' => 'foobar'], ['foo' => 'foobar'], ['foo' => 'foobar'], [], [], ['HTTP_FOO' => 'foobar']); - $this->assertEquals(array('foo' => 'foobar'), $dup->query->all(), '->duplicate() overrides the query parameters if provided'); - $this->assertEquals(array('foo' => 'foobar'), $dup->request->all(), '->duplicate() overrides the request parameters if provided'); - $this->assertEquals(array('foo' => 'foobar'), $dup->attributes->all(), '->duplicate() overrides the attributes if provided'); - $this->assertEquals(array('foo' => array('foobar')), $dup->headers->all(), '->duplicate() overrides the HTTP header if provided'); + $this->assertEquals(['foo' => 'foobar'], $dup->query->all(), '->duplicate() overrides the query parameters if provided'); + $this->assertEquals(['foo' => 'foobar'], $dup->request->all(), '->duplicate() overrides the request parameters if provided'); + $this->assertEquals(['foo' => 'foobar'], $dup->attributes->all(), '->duplicate() overrides the attributes if provided'); + $this->assertEquals(['foo' => ['foobar']], $dup->headers->all(), '->duplicate() overrides the HTTP header if provided'); } public function testDuplicateWithFormat() { - $request = new Request(array(), array(), array('_format' => 'json')); + $request = new Request([], [], ['_format' => 'json']); $dup = $request->duplicate(); $this->assertEquals('json', $dup->getRequestFormat()); @@ -323,7 +421,7 @@ public function testGetFormatFromMimeType($format, $mimeTypes) public function getFormatToMimeTypeMapProviderWithAdditionalNullFormat() { return array_merge( - array(array(null, array(null, 'unexistent-mime-type'))), + [[null, [null, 'unexistent-mime-type']]], $this->getFormatToMimeTypeMapProvider() ); } @@ -332,6 +430,9 @@ public function testGetFormatFromMimeTypeWithParameters() { $request = new Request(); $this->assertEquals('json', $request->getFormat('application/json; charset=utf-8')); + $this->assertEquals('json', $request->getFormat('application/json;charset=utf-8')); + $this->assertEquals('json', $request->getFormat('application/json ; charset=utf-8')); + $this->assertEquals('json', $request->getFormat('application/json ;charset=utf-8')); } /** @@ -355,7 +456,7 @@ public function testGetMimeTypesFromInexistentFormat() { $request = new Request(); $this->assertNull($request->getMimeType('foo')); - $this->assertEquals(array(), Request::getMimeTypes('foo')); + $this->assertEquals([], Request::getMimeTypes('foo')); } public function testGetFormatWithCustomMimeType() @@ -367,20 +468,21 @@ public function testGetFormatWithCustomMimeType() public function getFormatToMimeTypeMapProvider() { - return array( - array('txt', array('text/plain')), - array('js', array('application/javascript', 'application/x-javascript', 'text/javascript')), - array('css', array('text/css')), - array('json', array('application/json', 'application/x-json')), - array('xml', array('text/xml', 'application/xml', 'application/x-xml')), - array('rdf', array('application/rdf+xml')), - array('atom', array('application/atom+xml')), - ); + return [ + ['txt', ['text/plain']], + ['js', ['application/javascript', 'application/x-javascript', 'text/javascript']], + ['css', ['text/css']], + ['json', ['application/json', 'application/x-json']], + ['jsonld', ['application/ld+json']], + ['xml', ['text/xml', 'application/xml', 'application/x-xml']], + ['rdf', ['application/rdf+xml']], + ['atom', ['application/atom+xml']], + ]; } public function testGetUri() { - $server = array(); + $server = []; // Standard Request on non default PORT // http://host:8080/index.php/path/info?query=string @@ -399,7 +501,7 @@ public function testGetUri() $request = new Request(); - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals('http://host:8080/index.php/path/info?query=string', $request->getUri(), '->getUri() with non default port'); @@ -408,7 +510,7 @@ public function testGetUri() $server['SERVER_NAME'] = 'servername'; $server['SERVER_PORT'] = '80'; - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals('http://host/index.php/path/info?query=string', $request->getUri(), '->getUri() with default port'); @@ -417,7 +519,7 @@ public function testGetUri() $server['SERVER_NAME'] = 'servername'; $server['SERVER_PORT'] = '80'; - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals('http://servername/index.php/path/info?query=string', $request->getUri(), '->getUri() with default port without HOST_HEADER'); @@ -425,7 +527,7 @@ public function testGetUri() // RewriteCond %{REQUEST_FILENAME} !-f // RewriteRule ^(.*)$ index.php [QSA,L] // http://host:8080/path/info?query=string - $server = array(); + $server = []; $server['HTTP_HOST'] = 'host:8080'; $server['SERVER_NAME'] = 'servername'; $server['SERVER_PORT'] = '8080'; @@ -439,7 +541,7 @@ public function testGetUri() $server['PHP_SELF'] = '/index.php'; $server['SCRIPT_FILENAME'] = '/some/where/index.php'; - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals('http://host:8080/path/info?query=string', $request->getUri(), '->getUri() with rewrite'); // Use std port number @@ -448,7 +550,7 @@ public function testGetUri() $server['SERVER_NAME'] = 'servername'; $server['SERVER_PORT'] = '80'; - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals('http://host/path/info?query=string', $request->getUri(), '->getUri() with rewrite and default port'); @@ -457,13 +559,13 @@ public function testGetUri() $server['SERVER_NAME'] = 'servername'; $server['SERVER_PORT'] = '80'; - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals('http://servername/path/info?query=string', $request->getUri(), '->getUri() with rewrite, default port without HOST_HEADER'); // With encoded characters - $server = array( + $server = [ 'HTTP_HOST' => 'host:8080', 'SERVER_NAME' => 'servername', 'SERVER_PORT' => '8080', @@ -473,9 +575,9 @@ public function testGetUri() 'PATH_TRANSLATED' => 'redirect:/index.php/foo bar/in+fo', 'PHP_SELF' => '/ba se/index_dev.php/path/info', 'SCRIPT_FILENAME' => '/some/where/ba se/index_dev.php', - ); + ]; - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals( 'http://host:8080/ba%20se/index_dev.php/foo%20bar/in+fo?query=string', @@ -485,11 +587,11 @@ public function testGetUri() // with user info $server['PHP_AUTH_USER'] = 'fabien'; - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals('http://host:8080/ba%20se/index_dev.php/foo%20bar/in+fo?query=string', $request->getUri()); $server['PHP_AUTH_PW'] = 'symfony'; - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals('http://host:8080/ba%20se/index_dev.php/foo%20bar/in+fo?query=string', $request->getUri()); } @@ -507,7 +609,7 @@ public function testGetUriForPath() $request = Request::create('https://test.com:90/foo?bar=baz'); $this->assertEquals('https://test.com:90/some/path', $request->getUriForPath('/some/path')); - $server = array(); + $server = []; // Standard Request on non default PORT // http://host:8080/index.php/path/info?query=string @@ -526,7 +628,7 @@ public function testGetUriForPath() $request = new Request(); - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals('http://host:8080/index.php/some/path', $request->getUriForPath('/some/path'), '->getUriForPath() with non default port'); @@ -535,7 +637,7 @@ public function testGetUriForPath() $server['SERVER_NAME'] = 'servername'; $server['SERVER_PORT'] = '80'; - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals('http://host/index.php/some/path', $request->getUriForPath('/some/path'), '->getUriForPath() with default port'); @@ -544,7 +646,7 @@ public function testGetUriForPath() $server['SERVER_NAME'] = 'servername'; $server['SERVER_PORT'] = '80'; - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals('http://servername/index.php/some/path', $request->getUriForPath('/some/path'), '->getUriForPath() with default port without HOST_HEADER'); @@ -552,7 +654,7 @@ public function testGetUriForPath() // RewriteCond %{REQUEST_FILENAME} !-f // RewriteRule ^(.*)$ index.php [QSA,L] // http://host:8080/path/info?query=string - $server = array(); + $server = []; $server['HTTP_HOST'] = 'host:8080'; $server['SERVER_NAME'] = 'servername'; $server['SERVER_PORT'] = '8080'; @@ -566,7 +668,7 @@ public function testGetUriForPath() $server['PHP_SELF'] = '/index.php'; $server['SCRIPT_FILENAME'] = '/some/where/index.php'; - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals('http://host:8080/some/path', $request->getUriForPath('/some/path'), '->getUri() with rewrite'); // Use std port number @@ -575,7 +677,7 @@ public function testGetUriForPath() $server['SERVER_NAME'] = 'servername'; $server['SERVER_PORT'] = '80'; - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals('http://host/some/path', $request->getUriForPath('/some/path'), '->getUriForPath() with rewrite and default port'); @@ -584,7 +686,7 @@ public function testGetUriForPath() $server['SERVER_NAME'] = 'servername'; $server['SERVER_PORT'] = '80'; - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals('http://servername/some/path', $request->getUriForPath('/some/path'), '->getUriForPath() with rewrite, default port without HOST_HEADER'); $this->assertEquals('servername', $request->getHttpHost()); @@ -592,11 +694,11 @@ public function testGetUriForPath() // with user info $server['PHP_AUTH_USER'] = 'fabien'; - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals('http://servername/some/path', $request->getUriForPath('/some/path')); $server['PHP_AUTH_PW'] = 'symfony'; - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals('http://servername/some/path', $request->getUriForPath('/some/path')); } @@ -610,30 +712,30 @@ public function testGetRelativeUriForPath($expected, $pathinfo, $path) public function getRelativeUriForPathData() { - return array( - array('me.png', '/foo', '/me.png'), - array('../me.png', '/foo/bar', '/me.png'), - array('me.png', '/foo/bar', '/foo/me.png'), - array('../baz/me.png', '/foo/bar/b', '/foo/baz/me.png'), - array('../../fooz/baz/me.png', '/foo/bar/b', '/fooz/baz/me.png'), - array('baz/me.png', '/foo/bar/b', 'baz/me.png'), - ); + return [ + ['me.png', '/foo', '/me.png'], + ['../me.png', '/foo/bar', '/me.png'], + ['me.png', '/foo/bar', '/foo/me.png'], + ['../baz/me.png', '/foo/bar/b', '/foo/baz/me.png'], + ['../../fooz/baz/me.png', '/foo/bar/b', '/fooz/baz/me.png'], + ['baz/me.png', '/foo/bar/b', 'baz/me.png'], + ]; } public function testGetUserInfo() { $request = new Request(); - $server = array('PHP_AUTH_USER' => 'fabien'); - $request->initialize(array(), array(), array(), array(), array(), $server); + $server = ['PHP_AUTH_USER' => 'fabien']; + $request->initialize([], [], [], [], [], $server); $this->assertEquals('fabien', $request->getUserInfo()); $server['PHP_AUTH_USER'] = '0'; - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals('0', $request->getUserInfo()); $server['PHP_AUTH_PW'] = '0'; - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals('0:0', $request->getUserInfo()); } @@ -641,22 +743,22 @@ public function testGetSchemeAndHttpHost() { $request = new Request(); - $server = array(); + $server = []; $server['SERVER_NAME'] = 'servername'; $server['SERVER_PORT'] = '90'; - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals('http://servername:90', $request->getSchemeAndHttpHost()); $server['PHP_AUTH_USER'] = 'fabien'; - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals('http://servername:90', $request->getSchemeAndHttpHost()); $server['PHP_AUTH_USER'] = '0'; - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals('http://servername:90', $request->getSchemeAndHttpHost()); $server['PHP_AUTH_PW'] = '0'; - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals('http://servername:90', $request->getSchemeAndHttpHost()); } @@ -673,29 +775,29 @@ public function testGetQueryString($query, $expectedQuery, $msg) public function getQueryStringNormalizationData() { - return array( - array('foo', 'foo', 'works with valueless parameters'), - array('foo=', 'foo=', 'includes a dangling equal sign'), - array('bar=&foo=bar', 'bar=&foo=bar', '->works with empty parameters'), - array('foo=bar&bar=', 'bar=&foo=bar', 'sorts keys alphabetically'), + return [ + ['foo', 'foo', 'works with valueless parameters'], + ['foo=', 'foo=', 'includes a dangling equal sign'], + ['bar=&foo=bar', 'bar=&foo=bar', '->works with empty parameters'], + ['foo=bar&bar=', 'bar=&foo=bar', 'sorts keys alphabetically'], // GET parameters, that are submitted from a HTML form, encode spaces as "+" by default (as defined in enctype application/x-www-form-urlencoded). // PHP also converts "+" to spaces when filling the global _GET or when using the function parse_str. - array('him=John%20Doe&her=Jane+Doe', 'her=Jane%20Doe&him=John%20Doe', 'normalizes spaces in both encodings "%20" and "+"'), + ['baz=Foo%20Baz&bar=Foo+Bar', 'bar=Foo%20Bar&baz=Foo%20Baz', 'normalizes spaces in both encodings "%20" and "+"'], - array('foo[]=1&foo[]=2', 'foo%5B%5D=1&foo%5B%5D=2', 'allows array notation'), - array('foo=1&foo=2', 'foo=1&foo=2', 'allows repeated parameters'), - array('pa%3Dram=foo%26bar%3Dbaz&test=test', 'pa%3Dram=foo%26bar%3Dbaz&test=test', 'works with encoded delimiters'), - array('0', '0', 'allows "0"'), - array('Jane Doe&John%20Doe', 'Jane%20Doe&John%20Doe', 'normalizes encoding in keys'), - array('her=Jane Doe&him=John%20Doe', 'her=Jane%20Doe&him=John%20Doe', 'normalizes encoding in values'), - array('foo=bar&&&test&&', 'foo=bar&test', 'removes unneeded delimiters'), - array('formula=e=m*c^2', 'formula=e%3Dm%2Ac%5E2', 'correctly treats only the first "=" as delimiter and the next as value'), + ['foo[]=1&foo[]=2', 'foo%5B%5D=1&foo%5B%5D=2', 'allows array notation'], + ['foo=1&foo=2', 'foo=1&foo=2', 'allows repeated parameters'], + ['pa%3Dram=foo%26bar%3Dbaz&test=test', 'pa%3Dram=foo%26bar%3Dbaz&test=test', 'works with encoded delimiters'], + ['0', '0', 'allows "0"'], + ['Foo Bar&Foo%20Baz', 'Foo%20Bar&Foo%20Baz', 'normalizes encoding in keys'], + ['bar=Foo Bar&baz=Foo%20Baz', 'bar=Foo%20Bar&baz=Foo%20Baz', 'normalizes encoding in values'], + ['foo=bar&&&test&&', 'foo=bar&test', 'removes unneeded delimiters'], + ['formula=e=m*c^2', 'formula=e%3Dm%2Ac%5E2', 'correctly treats only the first "=" as delimiter and the next as value'], // Ignore pairs with empty key, even if there was a value, e.g. "=value", as such nameless values cannot be retrieved anyway. // PHP also does not include them when building _GET. - array('foo=bar&=a=b&=x=y', 'foo=bar', 'removes params with empty key'), - ); + ['foo=bar&=a=b&=x=y', 'foo=bar', 'removes params with empty key'], + ]; } public function testGetQueryStringReturnsNull() @@ -712,85 +814,83 @@ public function testGetHost() { $request = new Request(); - $request->initialize(array('foo' => 'bar')); + $request->initialize(['foo' => 'bar']); $this->assertEquals('', $request->getHost(), '->getHost() return empty string if not initialized'); - $request->initialize(array(), array(), array(), array(), array(), array('HTTP_HOST' => 'www.example.com')); + $request->initialize([], [], [], [], [], ['HTTP_HOST' => 'www.example.com']); $this->assertEquals('www.example.com', $request->getHost(), '->getHost() from Host Header'); // Host header with port number - $request->initialize(array(), array(), array(), array(), array(), array('HTTP_HOST' => 'www.example.com:8080')); + $request->initialize([], [], [], [], [], ['HTTP_HOST' => 'www.example.com:8080']); $this->assertEquals('www.example.com', $request->getHost(), '->getHost() from Host Header with port number'); // Server values - $request->initialize(array(), array(), array(), array(), array(), array('SERVER_NAME' => 'www.example.com')); + $request->initialize([], [], [], [], [], ['SERVER_NAME' => 'www.example.com']); $this->assertEquals('www.example.com', $request->getHost(), '->getHost() from server name'); - $request->initialize(array(), array(), array(), array(), array(), array('SERVER_NAME' => 'www.example.com', 'HTTP_HOST' => 'www.host.com')); + $request->initialize([], [], [], [], [], ['SERVER_NAME' => 'www.example.com', 'HTTP_HOST' => 'www.host.com']); $this->assertEquals('www.host.com', $request->getHost(), '->getHost() value from Host header has priority over SERVER_NAME '); } public function testGetPort() { - $request = Request::create('http://example.com', 'GET', array(), array(), array(), array( + $request = Request::create('http://example.com', 'GET', [], [], [], [ 'HTTP_X_FORWARDED_PROTO' => 'https', 'HTTP_X_FORWARDED_PORT' => '443', - )); + ]); $port = $request->getPort(); $this->assertEquals(80, $port, 'Without trusted proxies FORWARDED_PROTO and FORWARDED_PORT are ignored.'); - Request::setTrustedProxies(array('1.1.1.1'), Request::HEADER_X_FORWARDED_ALL); - $request = Request::create('http://example.com', 'GET', array(), array(), array(), array( + Request::setTrustedProxies(['1.1.1.1'], Request::HEADER_X_FORWARDED_ALL); + $request = Request::create('http://example.com', 'GET', [], [], [], [ 'HTTP_X_FORWARDED_PROTO' => 'https', 'HTTP_X_FORWARDED_PORT' => '8443', - )); + ]); $this->assertEquals(80, $request->getPort(), 'With PROTO and PORT on untrusted connection server value takes precedence.'); $request->server->set('REMOTE_ADDR', '1.1.1.1'); $this->assertEquals(8443, $request->getPort(), 'With PROTO and PORT set PORT takes precedence.'); - $request = Request::create('http://example.com', 'GET', array(), array(), array(), array( + $request = Request::create('http://example.com', 'GET', [], [], [], [ 'HTTP_X_FORWARDED_PROTO' => 'https', - )); + ]); $this->assertEquals(80, $request->getPort(), 'With only PROTO set getPort() ignores trusted headers on untrusted connection.'); $request->server->set('REMOTE_ADDR', '1.1.1.1'); $this->assertEquals(443, $request->getPort(), 'With only PROTO set getPort() defaults to 443.'); - $request = Request::create('http://example.com', 'GET', array(), array(), array(), array( + $request = Request::create('http://example.com', 'GET', [], [], [], [ 'HTTP_X_FORWARDED_PROTO' => 'http', - )); + ]); $this->assertEquals(80, $request->getPort(), 'If X_FORWARDED_PROTO is set to HTTP getPort() ignores trusted headers on untrusted connection.'); $request->server->set('REMOTE_ADDR', '1.1.1.1'); $this->assertEquals(80, $request->getPort(), 'If X_FORWARDED_PROTO is set to HTTP getPort() returns port of the original request.'); - $request = Request::create('http://example.com', 'GET', array(), array(), array(), array( + $request = Request::create('http://example.com', 'GET', [], [], [], [ 'HTTP_X_FORWARDED_PROTO' => 'On', - )); + ]); $this->assertEquals(80, $request->getPort(), 'With only PROTO set and value is On, getPort() ignores trusted headers on untrusted connection.'); $request->server->set('REMOTE_ADDR', '1.1.1.1'); $this->assertEquals(443, $request->getPort(), 'With only PROTO set and value is On, getPort() defaults to 443.'); - $request = Request::create('http://example.com', 'GET', array(), array(), array(), array( + $request = Request::create('http://example.com', 'GET', [], [], [], [ 'HTTP_X_FORWARDED_PROTO' => '1', - )); + ]); $this->assertEquals(80, $request->getPort(), 'With only PROTO set and value is 1, getPort() ignores trusted headers on untrusted connection.'); $request->server->set('REMOTE_ADDR', '1.1.1.1'); $this->assertEquals(443, $request->getPort(), 'With only PROTO set and value is 1, getPort() defaults to 443.'); - $request = Request::create('http://example.com', 'GET', array(), array(), array(), array( + $request = Request::create('http://example.com', 'GET', [], [], [], [ 'HTTP_X_FORWARDED_PROTO' => 'something-else', - )); + ]); $port = $request->getPort(); $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(array(), array(), array(), array(), array(), array('HTTP_HOST' => 'www.host.com?query=string')); + $request->initialize([], [], [], [], [], ['HTTP_HOST' => 'www.host.com?query=string']); $request->getHost(); } @@ -847,6 +947,11 @@ public function testGetSetMethod() $request->setMethod('POST'); $request->headers->set('X-HTTP-METHOD-OVERRIDE', 'delete'); $this->assertEquals('DELETE', $request->getMethod(), '->getMethod() returns the method from X-HTTP-Method-Override if defined and POST'); + + $request = new Request(); + $request->setMethod('POST'); + $request->query->set('_method', ['delete', 'patch']); + $this->assertSame('POST', $request->getMethod(), '->getMethod() returns the request method if invalid type is defined in query'); } /** @@ -882,102 +987,122 @@ public function testGetClientIpsForwarded($expected, $remoteAddr, $httpForwarded public function getClientIpsForwardedProvider() { // $expected $remoteAddr $httpForwarded $trustedProxies - return array( - array(array('127.0.0.1'), '127.0.0.1', 'for="_gazonk"', null), - array(array('127.0.0.1'), '127.0.0.1', 'for="_gazonk"', array('127.0.0.1')), - array(array('88.88.88.88'), '127.0.0.1', 'for="88.88.88.88:80"', array('127.0.0.1')), - array(array('192.0.2.60'), '::1', 'for=192.0.2.60;proto=http;by=203.0.113.43', array('::1')), - array(array('2620:0:1cfe:face:b00c::3', '192.0.2.43'), '::1', 'for=192.0.2.43, for=2620:0:1cfe:face:b00c::3', array('::1')), - array(array('2001:db8:cafe::17'), '::1', 'for="[2001:db8:cafe::17]:4711', array('::1')), - ); + return [ + [['127.0.0.1'], '127.0.0.1', 'for="_gazonk"', null], + [['127.0.0.1'], '127.0.0.1', 'for="_gazonk"', ['127.0.0.1']], + [['88.88.88.88'], '127.0.0.1', 'for="88.88.88.88:80"', ['127.0.0.1']], + [['192.0.2.60'], '::1', 'for=192.0.2.60;proto=http;by=203.0.113.43', ['::1']], + [['2620:0:1cfe:face:b00c::3', '192.0.2.43'], '::1', 'for=192.0.2.43, for=2620:0:1cfe:face:b00c::3', ['::1']], + [['2001:db8:cafe::17'], '::1', 'for="[2001:db8:cafe::17]:4711', ['::1']], + ]; } public function getClientIpsProvider() { - // $expected $remoteAddr $httpForwardedFor $trustedProxies - return array( + // $expected $remoteAddr $httpForwardedFor $trustedProxies + return [ // simple IPv4 - array(array('88.88.88.88'), '88.88.88.88', null, null), + [['88.88.88.88'], '88.88.88.88', null, null], // trust the IPv4 remote addr - array(array('88.88.88.88'), '88.88.88.88', null, array('88.88.88.88')), + [['88.88.88.88'], '88.88.88.88', null, ['88.88.88.88']], // simple IPv6 - array(array('::1'), '::1', null, null), + [['::1'], '::1', null, null], // trust the IPv6 remote addr - array(array('::1'), '::1', null, array('::1')), + [['::1'], '::1', null, ['::1']], // forwarded for with remote IPv4 addr not trusted - array(array('127.0.0.1'), '127.0.0.1', '88.88.88.88', null), - // forwarded for with remote IPv4 addr trusted - array(array('88.88.88.88'), '127.0.0.1', '88.88.88.88', array('127.0.0.1')), + [['127.0.0.1'], '127.0.0.1', '88.88.88.88', null], + // forwarded for with remote IPv4 addr trusted + comma + [['88.88.88.88'], '127.0.0.1', '88.88.88.88,', ['127.0.0.1']], // forwarded for with remote IPv4 and all FF addrs trusted - array(array('88.88.88.88'), '127.0.0.1', '88.88.88.88', array('127.0.0.1', '88.88.88.88')), + [['88.88.88.88'], '127.0.0.1', '88.88.88.88', ['127.0.0.1', '88.88.88.88']], // forwarded for with remote IPv4 range trusted - array(array('88.88.88.88'), '123.45.67.89', '88.88.88.88', array('123.45.67.0/24')), + [['88.88.88.88'], '123.45.67.89', '88.88.88.88', ['123.45.67.0/24']], // forwarded for with remote IPv6 addr not trusted - array(array('1620:0:1cfe:face:b00c::3'), '1620:0:1cfe:face:b00c::3', '2620:0:1cfe:face:b00c::3', null), + [['1620:0:1cfe:face:b00c::3'], '1620:0:1cfe:face:b00c::3', '2620:0:1cfe:face:b00c::3', null], // forwarded for with remote IPv6 addr trusted - array(array('2620:0:1cfe:face:b00c::3'), '1620:0:1cfe:face:b00c::3', '2620:0:1cfe:face:b00c::3', array('1620:0:1cfe:face:b00c::3')), + [['2620:0:1cfe:face:b00c::3'], '1620:0:1cfe:face:b00c::3', '2620:0:1cfe:face:b00c::3', ['1620:0:1cfe:face:b00c::3']], // forwarded for with remote IPv6 range trusted - array(array('88.88.88.88'), '2a01:198:603:0:396e:4789:8e99:890f', '88.88.88.88', array('2a01:198:603:0::/65')), + [['88.88.88.88'], '2a01:198:603:0:396e:4789:8e99:890f', '88.88.88.88', ['2a01:198:603:0::/65']], // multiple forwarded for with remote IPv4 addr trusted - array(array('88.88.88.88', '87.65.43.21', '127.0.0.1'), '123.45.67.89', '127.0.0.1, 87.65.43.21, 88.88.88.88', array('123.45.67.89')), + [['88.88.88.88', '87.65.43.21', '127.0.0.1'], '123.45.67.89', '127.0.0.1, 87.65.43.21, 88.88.88.88', ['123.45.67.89']], // multiple forwarded for with remote IPv4 addr and some reverse proxies trusted - array(array('87.65.43.21', '127.0.0.1'), '123.45.67.89', '127.0.0.1, 87.65.43.21, 88.88.88.88', array('123.45.67.89', '88.88.88.88')), + [['87.65.43.21', '127.0.0.1'], '123.45.67.89', '127.0.0.1, 87.65.43.21, 88.88.88.88', ['123.45.67.89', '88.88.88.88']], // multiple forwarded for with remote IPv4 addr and some reverse proxies trusted but in the middle - array(array('88.88.88.88', '127.0.0.1'), '123.45.67.89', '127.0.0.1, 87.65.43.21, 88.88.88.88', array('123.45.67.89', '87.65.43.21')), + [['88.88.88.88', '127.0.0.1'], '123.45.67.89', '127.0.0.1, 87.65.43.21, 88.88.88.88', ['123.45.67.89', '87.65.43.21']], // multiple forwarded for with remote IPv4 addr and all reverse proxies trusted - array(array('127.0.0.1'), '123.45.67.89', '127.0.0.1, 87.65.43.21, 88.88.88.88', array('123.45.67.89', '87.65.43.21', '88.88.88.88', '127.0.0.1')), + [['127.0.0.1'], '123.45.67.89', '127.0.0.1, 87.65.43.21, 88.88.88.88', ['123.45.67.89', '87.65.43.21', '88.88.88.88', '127.0.0.1']], // multiple forwarded for with remote IPv6 addr trusted - array(array('2620:0:1cfe:face:b00c::3', '3620:0:1cfe:face:b00c::3'), '1620:0:1cfe:face:b00c::3', '3620:0:1cfe:face:b00c::3,2620:0:1cfe:face:b00c::3', array('1620:0:1cfe:face:b00c::3')), + [['2620:0:1cfe:face:b00c::3', '3620:0:1cfe:face:b00c::3'], '1620:0:1cfe:face:b00c::3', '3620:0:1cfe:face:b00c::3,2620:0:1cfe:face:b00c::3', ['1620:0:1cfe:face:b00c::3']], // multiple forwarded for with remote IPv6 addr and some reverse proxies trusted - array(array('3620:0:1cfe:face:b00c::3'), '1620:0:1cfe:face:b00c::3', '3620:0:1cfe:face:b00c::3,2620:0:1cfe:face:b00c::3', array('1620:0:1cfe:face:b00c::3', '2620:0:1cfe:face:b00c::3')), + [['3620:0:1cfe:face:b00c::3'], '1620:0:1cfe:face:b00c::3', '3620:0:1cfe:face:b00c::3,2620:0:1cfe:face:b00c::3', ['1620:0:1cfe:face:b00c::3', '2620:0:1cfe:face:b00c::3']], // multiple forwarded for with remote IPv4 addr and some reverse proxies trusted but in the middle - array(array('2620:0:1cfe:face:b00c::3', '4620:0:1cfe:face:b00c::3'), '1620:0:1cfe:face:b00c::3', '4620:0:1cfe:face:b00c::3,3620:0:1cfe:face:b00c::3,2620:0:1cfe:face:b00c::3', array('1620:0:1cfe:face:b00c::3', '3620:0:1cfe:face:b00c::3')), + [['2620:0:1cfe:face:b00c::3', '4620:0:1cfe:face:b00c::3'], '1620:0:1cfe:face:b00c::3', '4620:0:1cfe:face:b00c::3,3620:0:1cfe:face:b00c::3,2620:0:1cfe:face:b00c::3', ['1620:0:1cfe:face:b00c::3', '3620:0:1cfe:face:b00c::3']], // client IP with port - array(array('88.88.88.88'), '127.0.0.1', '88.88.88.88:12345, 127.0.0.1', array('127.0.0.1')), + [['88.88.88.88'], '127.0.0.1', '88.88.88.88:12345, 127.0.0.1', ['127.0.0.1']], // invalid forwarded IP is ignored - array(array('88.88.88.88'), '127.0.0.1', 'unknown,88.88.88.88', array('127.0.0.1')), - array(array('88.88.88.88'), '127.0.0.1', '}__test|O:21:"JDatabaseDriverMysqli":3:{s:2,88.88.88.88', array('127.0.0.1')), - ); + [['88.88.88.88'], '127.0.0.1', 'unknown,88.88.88.88', ['127.0.0.1']], + [['88.88.88.88'], '127.0.0.1', '}__test|O:21:"JDatabaseDriverMysqli":3:{s:2,88.88.88.88', ['127.0.0.1']], + ]; } /** - * @expectedException \Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException * @dataProvider getClientIpsWithConflictingHeadersProvider */ public function testGetClientIpsWithConflictingHeaders($httpForwarded, $httpXForwardedFor) { + $this->expectException('Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException'); $request = new Request(); - $server = array( + $server = [ 'REMOTE_ADDR' => '88.88.88.88', 'HTTP_FORWARDED' => $httpForwarded, 'HTTP_X_FORWARDED_FOR' => $httpXForwardedFor, - ); + ]; - Request::setTrustedProxies(array('88.88.88.88'), Request::HEADER_X_FORWARDED_ALL | Request::HEADER_FORWARDED); + Request::setTrustedProxies(['88.88.88.88'], Request::HEADER_X_FORWARDED_ALL | Request::HEADER_FORWARDED); - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $request->getClientIps(); } + /** + * @dataProvider getClientIpsWithConflictingHeadersProvider + */ + public function testGetClientIpsOnlyXHttpForwardedForTrusted($httpForwarded, $httpXForwardedFor) + { + $request = new Request(); + + $server = [ + 'REMOTE_ADDR' => '88.88.88.88', + 'HTTP_FORWARDED' => $httpForwarded, + 'HTTP_X_FORWARDED_FOR' => $httpXForwardedFor, + ]; + + Request::setTrustedProxies(['88.88.88.88'], Request::HEADER_X_FORWARDED_FOR); + + $request->initialize([], [], [], [], [], $server); + + $this->assertSame(array_reverse(explode(',', $httpXForwardedFor)), $request->getClientIps()); + } + public function getClientIpsWithConflictingHeadersProvider() { // $httpForwarded $httpXForwardedFor - return array( - array('for=87.65.43.21', '192.0.2.60'), - array('for=87.65.43.21, for=192.0.2.60', '192.0.2.60'), - array('for=192.0.2.60', '192.0.2.60,87.65.43.21'), - array('for="::face", for=192.0.2.60', '192.0.2.60,192.0.2.43'), - array('for=87.65.43.21, for=192.0.2.60', '192.0.2.60,87.65.43.21'), - ); + return [ + ['for=87.65.43.21', '192.0.2.60'], + ['for=87.65.43.21, for=192.0.2.60', '192.0.2.60'], + ['for=192.0.2.60', '192.0.2.60,87.65.43.21'], + ['for="::face", for=192.0.2.60', '192.0.2.60,192.0.2.43'], + ['for=87.65.43.21, for=192.0.2.60', '192.0.2.60,87.65.43.21'], + ]; } /** @@ -987,15 +1112,15 @@ public function testGetClientIpsWithAgreeingHeaders($httpForwarded, $httpXForwar { $request = new Request(); - $server = array( + $server = [ 'REMOTE_ADDR' => '88.88.88.88', 'HTTP_FORWARDED' => $httpForwarded, 'HTTP_X_FORWARDED_FOR' => $httpXForwardedFor, - ); + ]; - Request::setTrustedProxies(array('88.88.88.88'), Request::HEADER_X_FORWARDED_ALL); + Request::setTrustedProxies(['88.88.88.88'], -1); - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $clientIps = $request->getClientIps(); @@ -1005,14 +1130,14 @@ public function testGetClientIpsWithAgreeingHeaders($httpForwarded, $httpXForwar public function getClientIpsWithAgreeingHeadersProvider() { // $httpForwarded $httpXForwardedFor - return array( - array('for="192.0.2.60"', '192.0.2.60', array('192.0.2.60')), - array('for=192.0.2.60, for=87.65.43.21', '192.0.2.60,87.65.43.21', array('87.65.43.21', '192.0.2.60')), - array('for="[::face]", for=192.0.2.60', '::face,192.0.2.60', array('192.0.2.60', '::face')), - array('for="192.0.2.60:80"', '192.0.2.60', array('192.0.2.60')), - array('for=192.0.2.60;proto=http;by=203.0.113.43', '192.0.2.60', array('192.0.2.60')), - array('for="[2001:db8:cafe::17]:4711"', '2001:db8:cafe::17', array('2001:db8:cafe::17')), - ); + return [ + ['for="192.0.2.60"', '192.0.2.60', ['192.0.2.60']], + ['for=192.0.2.60, for=87.65.43.21', '192.0.2.60,87.65.43.21', ['87.65.43.21', '192.0.2.60']], + ['for="[::face]", for=192.0.2.60', '::face,192.0.2.60', ['192.0.2.60', '::face']], + ['for="192.0.2.60:80"', '192.0.2.60', ['192.0.2.60']], + ['for=192.0.2.60;proto=http;by=203.0.113.43', '192.0.2.60', ['192.0.2.60']], + ['for="[2001:db8:cafe::17]:4711"', '2001:db8:cafe::17', ['2001:db8:cafe::17']], + ]; } public function testGetContentWorksTwiceInDefaultMode() @@ -1026,17 +1151,17 @@ 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)); } public function testGetContentReturnsResourceWhenContentSetInConstructor() { - $req = new Request(array(), array(), array(), array(), array(), array(), 'MyContent'); + $req = new Request([], [], [], [], [], [], 'MyContent'); $resource = $req->getContent(true); - $this->assertInternalType('resource', $resource); + $this->assertIsResource($resource); $this->assertEquals('MyContent', stream_get_contents($resource)); } @@ -1046,17 +1171,17 @@ public function testContentAsResource() fwrite($resource, 'My other content'); rewind($resource); - $req = new Request(array(), array(), array(), array(), array(), array(), $resource); + $req = new Request([], [], [], [], [], [], $resource); $this->assertEquals('My other content', stream_get_contents($req->getContent(true))); $this->assertEquals('My other content', $req->getContent()); } /** - * @expectedException \LogicException * @dataProvider getContentCantBeCalledTwiceWithResourcesProvider */ public function testGetContentCantBeCalledTwiceWithResources($first, $second) { + $this->expectException('LogicException'); if (\PHP_VERSION_ID >= 50600) { $this->markTestSkipped('PHP >= 5.6 allows to open php://input several times.'); } @@ -1068,10 +1193,10 @@ public function testGetContentCantBeCalledTwiceWithResources($first, $second) public function getContentCantBeCalledTwiceWithResourcesProvider() { - return array( - 'Resource then fetch' => array(true, false), - 'Resource then resource' => array(true, true), - ); + return [ + 'Resource then fetch' => [true, false], + 'Resource then resource' => [true, true], + ]; } /** @@ -1097,24 +1222,24 @@ public function testGetContentCanBeCalledTwiceWithResources($first, $second) public function getContentCanBeCalledTwiceWithResourcesProvider() { - return array( - 'Fetch then fetch' => array(false, false), - 'Fetch then resource' => array(false, true), - 'Resource then fetch' => array(true, false), - 'Resource then resource' => array(true, true), - ); + return [ + 'Fetch then fetch' => [false, false], + 'Fetch then resource' => [false, true], + 'Resource then fetch' => [true, false], + 'Resource then resource' => [true, true], + ]; } public function provideOverloadedMethods() { - return array( - array('PUT'), - array('DELETE'), - array('PATCH'), - array('put'), - array('delete'), - array('patch'), - ); + return [ + ['PUT'], + ['DELETE'], + ['PATCH'], + ['put'], + ['delete'], + ['patch'], + ]; } /** @@ -1127,14 +1252,14 @@ public function testCreateFromGlobals($method) $_GET['foo1'] = 'bar1'; $_POST['foo2'] = 'bar2'; $_COOKIE['foo3'] = 'bar3'; - $_FILES['foo4'] = array('bar4'); + $_FILES['foo4'] = ['bar4']; $_SERVER['foo5'] = 'bar5'; $request = Request::createFromGlobals(); $this->assertEquals('bar1', $request->query->get('foo1'), '::fromGlobals() uses values from $_GET'); $this->assertEquals('bar2', $request->request->get('foo2'), '::fromGlobals() uses values from $_POST'); $this->assertEquals('bar3', $request->cookies->get('foo3'), '::fromGlobals() uses values from $_COOKIE'); - $this->assertEquals(array('bar4'), $request->files->get('foo4'), '::fromGlobals() uses values from $_FILES'); + $this->assertEquals(['bar4'], $request->files->get('foo4'), '::fromGlobals() uses values from $_FILES'); $this->assertEquals('bar5', $request->server->get('foo5'), '::fromGlobals() uses values from $_SERVER'); unset($_GET['foo1'], $_POST['foo2'], $_COOKIE['foo3'], $_FILES['foo4'], $_SERVER['foo5']); @@ -1164,25 +1289,25 @@ public function testCreateFromGlobals($method) public function testOverrideGlobals() { $request = new Request(); - $request->initialize(array('foo' => 'bar')); + $request->initialize(['foo' => 'bar']); // as the Request::overrideGlobals really work, it erase $_SERVER, so we must backup it $server = $_SERVER; $request->overrideGlobals(); - $this->assertEquals(array('foo' => 'bar'), $_GET); + $this->assertEquals(['foo' => 'bar'], $_GET); - $request->initialize(array(), array('foo' => 'bar')); + $request->initialize([], ['foo' => 'bar']); $request->overrideGlobals(); - $this->assertEquals(array('foo' => 'bar'), $_POST); + $this->assertEquals(['foo' => 'bar'], $_POST); $this->assertArrayNotHasKey('HTTP_X_FORWARDED_PROTO', $_SERVER); $request->headers->set('X_FORWARDED_PROTO', 'https'); - Request::setTrustedProxies(array('1.1.1.1'), Request::HEADER_X_FORWARDED_ALL); + Request::setTrustedProxies(['1.1.1.1'], Request::HEADER_X_FORWARDED_ALL); $this->assertFalse($request->isSecure()); $request->server->set('REMOTE_ADDR', '1.1.1.1'); $this->assertTrue($request->isSecure()); @@ -1199,12 +1324,12 @@ public function testOverrideGlobals() $this->assertArrayHasKey('CONTENT_TYPE', $_SERVER); $this->assertArrayHasKey('CONTENT_LENGTH', $_SERVER); - $request->initialize(array('foo' => 'bar', 'baz' => 'foo')); + $request->initialize(['foo' => 'bar', 'baz' => 'foo']); $request->query->remove('baz'); $request->overrideGlobals(); - $this->assertEquals(array('foo' => 'bar'), $_GET); + $this->assertEquals(['foo' => 'bar'], $_GET); $this->assertEquals('foo=bar', $_SERVER['QUERY_STRING']); $this->assertEquals('foo=bar', $request->server->get('QUERY_STRING')); @@ -1217,23 +1342,23 @@ public function testGetScriptName() $request = new Request(); $this->assertEquals('', $request->getScriptName()); - $server = array(); + $server = []; $server['SCRIPT_NAME'] = '/index.php'; - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals('/index.php', $request->getScriptName()); - $server = array(); + $server = []; $server['ORIG_SCRIPT_NAME'] = '/frontend.php'; - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals('/frontend.php', $request->getScriptName()); - $server = array(); + $server = []; $server['SCRIPT_NAME'] = '/index.php'; $server['ORIG_SCRIPT_NAME'] = '/frontend.php'; - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals('/index.php', $request->getScriptName()); } @@ -1243,29 +1368,29 @@ public function testGetBasePath() $request = new Request(); $this->assertEquals('', $request->getBasePath()); - $server = array(); + $server = []; $server['SCRIPT_FILENAME'] = '/some/where/index.php'; - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals('', $request->getBasePath()); - $server = array(); + $server = []; $server['SCRIPT_FILENAME'] = '/some/where/index.php'; $server['SCRIPT_NAME'] = '/index.php'; - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals('', $request->getBasePath()); - $server = array(); + $server = []; $server['SCRIPT_FILENAME'] = '/some/where/index.php'; $server['PHP_SELF'] = '/index.php'; - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals('', $request->getBasePath()); - $server = array(); + $server = []; $server['SCRIPT_FILENAME'] = '/some/where/index.php'; $server['ORIG_SCRIPT_NAME'] = '/index.php'; - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals('', $request->getBasePath()); } @@ -1275,17 +1400,23 @@ public function testGetPathInfo() $request = new Request(); $this->assertEquals('/', $request->getPathInfo()); - $server = array(); + $server = []; $server['REQUEST_URI'] = '/path/info'; - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals('/path/info', $request->getPathInfo()); - $server = array(); + $server = []; $server['REQUEST_URI'] = '/path%20test/info'; - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); $this->assertEquals('/path%20test/info', $request->getPathInfo()); + + $server = []; + $server['REQUEST_URI'] = '?a=b'; + $request->initialize([], [], [], [], [], $server); + + $this->assertEquals('/', $request->getPathInfo()); } public function testGetParameterPrecedence() @@ -1311,27 +1442,27 @@ public function testGetPreferredLanguage() { $request = new Request(); $this->assertNull($request->getPreferredLanguage()); - $this->assertNull($request->getPreferredLanguage(array())); - $this->assertEquals('fr', $request->getPreferredLanguage(array('fr'))); - $this->assertEquals('fr', $request->getPreferredLanguage(array('fr', 'en'))); - $this->assertEquals('en', $request->getPreferredLanguage(array('en', 'fr'))); - $this->assertEquals('fr-ch', $request->getPreferredLanguage(array('fr-ch', 'fr-fr'))); + $this->assertNull($request->getPreferredLanguage([])); + $this->assertEquals('fr', $request->getPreferredLanguage(['fr'])); + $this->assertEquals('fr', $request->getPreferredLanguage(['fr', 'en'])); + $this->assertEquals('en', $request->getPreferredLanguage(['en', 'fr'])); + $this->assertEquals('fr-ch', $request->getPreferredLanguage(['fr-ch', 'fr-fr'])); $request = new Request(); $request->headers->set('Accept-language', 'zh, en-us; q=0.8, en; q=0.6'); - $this->assertEquals('en', $request->getPreferredLanguage(array('en', 'en-us'))); + $this->assertEquals('en', $request->getPreferredLanguage(['en', 'en-us'])); $request = new Request(); $request->headers->set('Accept-language', 'zh, en-us; q=0.8, en; q=0.6'); - $this->assertEquals('en', $request->getPreferredLanguage(array('fr', 'en'))); + $this->assertEquals('en', $request->getPreferredLanguage(['fr', 'en'])); $request = new Request(); $request->headers->set('Accept-language', 'zh, en-us; q=0.8'); - $this->assertEquals('en', $request->getPreferredLanguage(array('fr', 'en'))); + $this->assertEquals('en', $request->getPreferredLanguage(['fr', 'en'])); $request = new Request(); $request->headers->set('Accept-language', 'zh, en-us; q=0.8, fr-fr; q=0.6, fr; q=0.5'); - $this->assertEquals('en', $request->getPreferredLanguage(array('fr', 'en'))); + $this->assertEquals('en', $request->getPreferredLanguage(['fr', 'en'])); } public function testIsXmlHttpRequest() @@ -1369,72 +1500,71 @@ public function testIntlLocale() public function testGetCharsets() { $request = new Request(); - $this->assertEquals(array(), $request->getCharsets()); + $this->assertEquals([], $request->getCharsets()); $request->headers->set('Accept-Charset', 'ISO-8859-1, US-ASCII, UTF-8; q=0.8, ISO-10646-UCS-2; q=0.6'); - $this->assertEquals(array(), $request->getCharsets()); // testing caching + $this->assertEquals([], $request->getCharsets()); // testing caching $request = new Request(); $request->headers->set('Accept-Charset', 'ISO-8859-1, US-ASCII, UTF-8; q=0.8, ISO-10646-UCS-2; q=0.6'); - $this->assertEquals(array('ISO-8859-1', 'US-ASCII', 'UTF-8', 'ISO-10646-UCS-2'), $request->getCharsets()); + $this->assertEquals(['ISO-8859-1', 'US-ASCII', 'UTF-8', 'ISO-10646-UCS-2'], $request->getCharsets()); $request = new Request(); $request->headers->set('Accept-Charset', 'ISO-8859-1,utf-8;q=0.7,*;q=0.7'); - $this->assertEquals(array('ISO-8859-1', 'utf-8', '*'), $request->getCharsets()); + $this->assertEquals(['ISO-8859-1', 'utf-8', '*'], $request->getCharsets()); } public function testGetEncodings() { $request = new Request(); - $this->assertEquals(array(), $request->getEncodings()); + $this->assertEquals([], $request->getEncodings()); $request->headers->set('Accept-Encoding', 'gzip,deflate,sdch'); - $this->assertEquals(array(), $request->getEncodings()); // testing caching + $this->assertEquals([], $request->getEncodings()); // testing caching $request = new Request(); $request->headers->set('Accept-Encoding', 'gzip,deflate,sdch'); - $this->assertEquals(array('gzip', 'deflate', 'sdch'), $request->getEncodings()); + $this->assertEquals(['gzip', 'deflate', 'sdch'], $request->getEncodings()); $request = new Request(); $request->headers->set('Accept-Encoding', 'gzip;q=0.4,deflate;q=0.9,compress;q=0.7'); - $this->assertEquals(array('deflate', 'compress', 'gzip'), $request->getEncodings()); + $this->assertEquals(['deflate', 'compress', 'gzip'], $request->getEncodings()); } public function testGetAcceptableContentTypes() { $request = new Request(); - $this->assertEquals(array(), $request->getAcceptableContentTypes()); + $this->assertEquals([], $request->getAcceptableContentTypes()); $request->headers->set('Accept', 'application/vnd.wap.wmlscriptc, text/vnd.wap.wml, application/vnd.wap.xhtml+xml, application/xhtml+xml, text/html, multipart/mixed, */*'); - $this->assertEquals(array(), $request->getAcceptableContentTypes()); // testing caching + $this->assertEquals([], $request->getAcceptableContentTypes()); // testing caching $request = new Request(); $request->headers->set('Accept', 'application/vnd.wap.wmlscriptc, text/vnd.wap.wml, application/vnd.wap.xhtml+xml, application/xhtml+xml, text/html, multipart/mixed, */*'); - $this->assertEquals(array('application/vnd.wap.wmlscriptc', 'text/vnd.wap.wml', 'application/vnd.wap.xhtml+xml', 'application/xhtml+xml', 'text/html', 'multipart/mixed', '*/*'), $request->getAcceptableContentTypes()); + $this->assertEquals(['application/vnd.wap.wmlscriptc', 'text/vnd.wap.wml', 'application/vnd.wap.xhtml+xml', 'application/xhtml+xml', 'text/html', 'multipart/mixed', '*/*'], $request->getAcceptableContentTypes()); } public function testGetLanguages() { $request = new Request(); - $this->assertEquals(array(), $request->getLanguages()); + $this->assertEquals([], $request->getLanguages()); $request = new Request(); $request->headers->set('Accept-language', 'zh, en-us; q=0.8, en; q=0.6'); - $this->assertEquals(array('zh', 'en_US', 'en'), $request->getLanguages()); - $this->assertEquals(array('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'); - $this->assertEquals(array('zh', 'en', 'en_US'), $request->getLanguages()); // Test out of order qvalues + $this->assertEquals(['zh', 'en', 'en_US'], $request->getLanguages()); // Test out of order qvalues $request = new Request(); $request->headers->set('Accept-language', 'zh, en, en-us'); - $this->assertEquals(array('zh', 'en', 'en_US'), $request->getLanguages()); // Test equal weighting without qvalues + $this->assertEquals(['zh', 'en', 'en_US'], $request->getLanguages()); // Test equal weighting without qvalues $request = new Request(); $request->headers->set('Accept-language', 'zh; q=0.6, en, en-us; q=0.6'); - $this->assertEquals(array('en', 'zh', 'en_US'), $request->getLanguages()); // Test equal weighting with qvalues + $this->assertEquals(['en', 'zh', 'en_US'], $request->getLanguages()); // Test equal weighting with qvalues $request = new Request(); $request->headers->set('Accept-language', 'zh, i-cherokee; q=0.6'); - $this->assertEquals(array('zh', 'cherokee'), $request->getLanguages()); + $this->assertEquals(['zh', 'cherokee'], $request->getLanguages()); } public function testGetRequestFormat() @@ -1454,7 +1584,7 @@ public function testGetRequestFormat() $this->assertNull($request->setRequestFormat('foo')); $this->assertEquals('foo', $request->getRequestFormat(null)); - $request = new Request(array('_format' => 'foo')); + $request = new Request(['_format' => 'foo']); $this->assertEquals('html', $request->getRequestFormat()); } @@ -1496,8 +1626,18 @@ public function testToString() $request = new Request(); $request->headers->set('Accept-language', 'zh, en-us; q=0.8, en; q=0.6'); + $request->cookies->set('Foo', 'Bar'); + + $asString = (string) $request; + + $this->assertStringContainsString('Accept-Language: zh, en-us; q=0.8, en; q=0.6', $asString); + $this->assertStringContainsString('Cookie: Foo=Bar', $asString); - $this->assertContains('Accept-Language: zh, en-us; q=0.8, en; q=0.6', $request->__toString()); + $request->cookies->set('Another', 'Cookie'); + + $asString = (string) $request; + + $this->assertStringContainsString('Cookie: Foo=Bar; Another=Cookie', $asString); } public function testIsMethod() @@ -1521,7 +1661,7 @@ public function testIsMethod() */ public function testGetBaseUrl($uri, $server, $expectedBaseUrl, $expectedPathInfo) { - $request = Request::create($uri, 'GET', array(), array(), array(), $server); + $request = Request::create($uri, 'GET', [], [], [], $server); $this->assertSame($expectedBaseUrl, $request->getBaseUrl(), 'baseUrl'); $this->assertSame($expectedPathInfo, $request->getPathInfo(), 'pathInfo'); @@ -1529,78 +1669,78 @@ public function testGetBaseUrl($uri, $server, $expectedBaseUrl, $expectedPathInf public function getBaseUrlData() { - return array( - array( + return [ + [ '/fruit/strawberry/1234index.php/blah', - array( + [ 'SCRIPT_FILENAME' => 'E:/Sites/cc-new/public_html/fruit/index.php', 'SCRIPT_NAME' => '/fruit/index.php', 'PHP_SELF' => '/fruit/index.php', - ), + ], '/fruit', '/strawberry/1234index.php/blah', - ), - array( + ], + [ '/fruit/strawberry/1234index.php/blah', - array( + [ 'SCRIPT_FILENAME' => 'E:/Sites/cc-new/public_html/index.php', 'SCRIPT_NAME' => '/index.php', 'PHP_SELF' => '/index.php', - ), + ], '', '/fruit/strawberry/1234index.php/blah', - ), - array( + ], + [ '/foo%20bar/', - array( + [ 'SCRIPT_FILENAME' => '/home/John Doe/public_html/foo bar/app.php', 'SCRIPT_NAME' => '/foo bar/app.php', 'PHP_SELF' => '/foo bar/app.php', - ), + ], '/foo%20bar', '/', - ), - array( + ], + [ '/foo%20bar/home', - array( + [ 'SCRIPT_FILENAME' => '/home/John Doe/public_html/foo bar/app.php', 'SCRIPT_NAME' => '/foo bar/app.php', 'PHP_SELF' => '/foo bar/app.php', - ), + ], '/foo%20bar', '/home', - ), - array( + ], + [ '/foo%20bar/app.php/home', - array( + [ 'SCRIPT_FILENAME' => '/home/John Doe/public_html/foo bar/app.php', 'SCRIPT_NAME' => '/foo bar/app.php', 'PHP_SELF' => '/foo bar/app.php', - ), + ], '/foo%20bar/app.php', '/home', - ), - array( + ], + [ '/foo%20bar/app.php/home%3Dbaz', - array( + [ 'SCRIPT_FILENAME' => '/home/John Doe/public_html/foo bar/app.php', 'SCRIPT_NAME' => '/foo bar/app.php', 'PHP_SELF' => '/foo bar/app.php', - ), + ], '/foo%20bar/app.php', '/home%3Dbaz', - ), - array( + ], + [ '/foo/bar+baz', - array( + [ 'SCRIPT_FILENAME' => '/home/John Doe/public_html/foo/app.php', 'SCRIPT_NAME' => '/foo/app.php', 'PHP_SELF' => '/foo/app.php', - ), + ], '/foo', '/bar+baz', - ), - ); + ], + ]; } /** @@ -1618,16 +1758,16 @@ public function testUrlencodedStringPrefix($string, $prefix, $expect) public function urlencodedStringPrefixData() { - return array( - array('foo', 'foo', 'foo'), - array('fo%6f', 'foo', 'fo%6f'), - array('foo/bar', 'foo', 'foo'), - array('fo%6f/bar', 'foo', 'fo%6f'), - array('f%6f%6f/bar', 'foo', 'f%6f%6f'), - array('%66%6F%6F/bar', 'foo', '%66%6F%6F'), - array('fo+o/bar', 'fo+o', 'fo+o'), - array('fo%2Bo/bar', 'fo+o', 'fo%2Bo'), - ); + return [ + ['foo', 'foo', 'foo'], + ['fo%6f', 'foo', 'fo%6f'], + ['foo/bar', 'foo', 'foo'], + ['fo%6f/bar', 'foo', 'fo%6f'], + ['f%6f%6f/bar', 'foo', 'f%6f%6f'], + ['%66%6F%6F/bar', 'foo', '%66%6F%6F'], + ['fo+o/bar', 'fo+o', 'fo+o'], + ['fo%2Bo/bar', 'fo+o', 'fo%2Bo'], + ]; } private function disableHttpMethodParameterOverride() @@ -1642,7 +1782,7 @@ private function getRequestInstanceForClientIpTests($remoteAddr, $httpForwardedF { $request = new Request(); - $server = array('REMOTE_ADDR' => $remoteAddr); + $server = ['REMOTE_ADDR' => $remoteAddr]; if (null !== $httpForwardedFor) { $server['HTTP_X_FORWARDED_FOR'] = $httpForwardedFor; } @@ -1651,7 +1791,7 @@ private function getRequestInstanceForClientIpTests($remoteAddr, $httpForwardedF Request::setTrustedProxies($trustedProxies, Request::HEADER_X_FORWARDED_ALL); } - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); return $request; } @@ -1660,7 +1800,7 @@ private function getRequestInstanceForClientIpsForwardedTests($remoteAddr, $http { $request = new Request(); - $server = array('REMOTE_ADDR' => $remoteAddr); + $server = ['REMOTE_ADDR' => $remoteAddr]; if (null !== $httpForwarded) { $server['HTTP_FORWARDED'] = $httpForwarded; @@ -1670,7 +1810,7 @@ private function getRequestInstanceForClientIpsForwardedTests($remoteAddr, $http Request::setTrustedProxies($trustedProxies, Request::HEADER_FORWARDED); } - $request->initialize(array(), array(), array(), array(), array(), $server); + $request->initialize([], [], [], [], [], $server); return $request; } @@ -1691,35 +1831,35 @@ public function testTrustedProxiesXForwardedFor() $this->assertFalse($request->isSecure()); // disabling proxy trusting - Request::setTrustedProxies(array(), Request::HEADER_X_FORWARDED_ALL); + Request::setTrustedProxies([], Request::HEADER_X_FORWARDED_ALL); $this->assertEquals('3.3.3.3', $request->getClientIp()); $this->assertEquals('example.com', $request->getHost()); $this->assertEquals(80, $request->getPort()); $this->assertFalse($request->isSecure()); // request is forwarded by a non-trusted proxy - Request::setTrustedProxies(array('2.2.2.2'), Request::HEADER_X_FORWARDED_ALL); + Request::setTrustedProxies(['2.2.2.2'], Request::HEADER_X_FORWARDED_ALL); $this->assertEquals('3.3.3.3', $request->getClientIp()); $this->assertEquals('example.com', $request->getHost()); $this->assertEquals(80, $request->getPort()); $this->assertFalse($request->isSecure()); // trusted proxy via setTrustedProxies() - Request::setTrustedProxies(array('3.3.3.3', '2.2.2.2'), Request::HEADER_X_FORWARDED_ALL); + Request::setTrustedProxies(['3.3.3.3', '2.2.2.2'], Request::HEADER_X_FORWARDED_ALL); $this->assertEquals('1.1.1.1', $request->getClientIp()); $this->assertEquals('foo.example.com', $request->getHost()); $this->assertEquals(443, $request->getPort()); $this->assertTrue($request->isSecure()); // trusted proxy via setTrustedProxies() - Request::setTrustedProxies(array('3.3.3.4', '2.2.2.2'), Request::HEADER_X_FORWARDED_ALL); + Request::setTrustedProxies(['3.3.3.4', '2.2.2.2'], Request::HEADER_X_FORWARDED_ALL); $this->assertEquals('3.3.3.3', $request->getClientIp()); $this->assertEquals('example.com', $request->getHost()); $this->assertEquals(80, $request->getPort()); $this->assertFalse($request->isSecure()); // check various X_FORWARDED_PROTO header values - Request::setTrustedProxies(array('3.3.3.3', '2.2.2.2'), Request::HEADER_X_FORWARDED_ALL); + Request::setTrustedProxies(['3.3.3.3', '2.2.2.2'], Request::HEADER_X_FORWARDED_ALL); $request->headers->set('X_FORWARDED_PROTO', 'ssl'); $this->assertTrue($request->isSecure()); @@ -1729,7 +1869,7 @@ public function testTrustedProxiesXForwardedFor() /** * @group legacy - * @expectedDeprecation The "Symfony\Component\HttpFoundation\Request::setTrustedHeaderName()" method is deprecated since version 3.3 and will be removed in 4.0. Use the $trustedHeaderSet argument of the Request::setTrustedProxies() method instead. + * @expectedDeprecation The "Symfony\Component\HttpFoundation\Request::setTrustedHeaderName()" method is deprecated since Symfony 3.3 and will be removed in 4.0. Use the $trustedHeaderSet argument of the Request::setTrustedProxies() method instead. */ public function testLegacyTrustedProxies() { @@ -1744,7 +1884,7 @@ public function testLegacyTrustedProxies() $request->headers->set('X_MY_PROTO', 'http'); $request->headers->set('X_MY_PORT', 81); - Request::setTrustedProxies(array('3.3.3.3', '2.2.2.2'), Request::HEADER_X_FORWARDED_ALL); + Request::setTrustedProxies(['3.3.3.3', '2.2.2.2'], Request::HEADER_X_FORWARDED_ALL); // custom header names Request::setTrustedHeaderName(Request::HEADER_CLIENT_IP, 'X_MY_FOR'); @@ -1787,35 +1927,35 @@ public function testTrustedProxiesForwarded() $this->assertFalse($request->isSecure()); // disabling proxy trusting - Request::setTrustedProxies(array(), Request::HEADER_FORWARDED); + Request::setTrustedProxies([], Request::HEADER_FORWARDED); $this->assertEquals('3.3.3.3', $request->getClientIp()); $this->assertEquals('example.com', $request->getHost()); $this->assertEquals(80, $request->getPort()); $this->assertFalse($request->isSecure()); // request is forwarded by a non-trusted proxy - Request::setTrustedProxies(array('2.2.2.2'), Request::HEADER_FORWARDED); + Request::setTrustedProxies(['2.2.2.2'], Request::HEADER_FORWARDED); $this->assertEquals('3.3.3.3', $request->getClientIp()); $this->assertEquals('example.com', $request->getHost()); $this->assertEquals(80, $request->getPort()); $this->assertFalse($request->isSecure()); // trusted proxy via setTrustedProxies() - Request::setTrustedProxies(array('3.3.3.3', '2.2.2.2'), Request::HEADER_FORWARDED); + Request::setTrustedProxies(['3.3.3.3', '2.2.2.2'], Request::HEADER_FORWARDED); $this->assertEquals('1.1.1.1', $request->getClientIp()); $this->assertEquals('foo.example.com', $request->getHost()); $this->assertEquals(8080, $request->getPort()); $this->assertTrue($request->isSecure()); // trusted proxy via setTrustedProxies() - Request::setTrustedProxies(array('3.3.3.4', '2.2.2.2'), Request::HEADER_FORWARDED); + Request::setTrustedProxies(['3.3.3.4', '2.2.2.2'], Request::HEADER_FORWARDED); $this->assertEquals('3.3.3.3', $request->getClientIp()); $this->assertEquals('example.com', $request->getHost()); $this->assertEquals(80, $request->getPort()); $this->assertFalse($request->isSecure()); // check various X_FORWARDED_PROTO header values - Request::setTrustedProxies(array('3.3.3.3', '2.2.2.2'), Request::HEADER_FORWARDED); + Request::setTrustedProxies(['3.3.3.3', '2.2.2.2'], Request::HEADER_FORWARDED); $request->headers->set('FORWARDED', 'proto=ssl'); $this->assertTrue($request->isSecure()); @@ -1825,20 +1965,20 @@ public function testTrustedProxiesForwarded() /** * @group legacy - * @expectedException \InvalidArgumentException */ public function testSetTrustedProxiesInvalidHeaderName() { + $this->expectException('InvalidArgumentException'); Request::create('http://example.com/'); Request::setTrustedHeaderName('bogus name', 'X_MY_FOR'); } /** * @group legacy - * @expectedException \InvalidArgumentException */ public function testGetTrustedProxiesInvalidHeaderName() { + $this->expectException('InvalidArgumentException'); Request::create('http://example.com/'); Request::getTrustedHeaderName('bogus name'); } @@ -1855,81 +1995,37 @@ public function testIISRequestUri($headers, $server, $expectedRequestUri) $this->assertEquals($expectedRequestUri, $request->getRequestUri(), '->getRequestUri() is correct'); $subRequestUri = '/bar/foo'; - $subRequest = Request::create($subRequestUri, 'get', array(), array(), array(), $request->server->all()); + $subRequest = Request::create($subRequestUri, 'get', [], [], [], $request->server->all()); $this->assertEquals($subRequestUri, $subRequest->getRequestUri(), '->getRequestUri() is correct in sub request'); } public function iisRequestUriProvider() { - return array( - array( - array( - 'X_ORIGINAL_URL' => '/foo/bar', - ), - array(), - '/foo/bar', - ), - array( - array( - 'X_REWRITE_URL' => '/foo/bar', - ), - array(), - '/foo/bar', - ), - array( - array(), - array( + return [ + [ + [], + [ 'IIS_WasUrlRewritten' => '1', 'UNENCODED_URL' => '/foo/bar', - ), + ], '/foo/bar', - ), - array( - array( - 'X_ORIGINAL_URL' => '/foo/bar', - ), - array( - 'HTTP_X_ORIGINAL_URL' => '/foo/bar', - ), - '/foo/bar', - ), - array( - array( - 'X_ORIGINAL_URL' => '/foo/bar', - ), - array( - 'IIS_WasUrlRewritten' => '1', - 'UNENCODED_URL' => '/foo/bar', - ), - '/foo/bar', - ), - array( - array( - 'X_ORIGINAL_URL' => '/foo/bar', - ), - array( - 'HTTP_X_ORIGINAL_URL' => '/foo/bar', - 'IIS_WasUrlRewritten' => '1', - 'UNENCODED_URL' => '/foo/bar', - ), - '/foo/bar', - ), - array( - array(), - array( + ], + [ + [], + [ 'ORIG_PATH_INFO' => '/foo/bar', - ), + ], '/foo/bar', - ), - array( - array(), - array( + ], + [ + [], + [ 'ORIG_PATH_INFO' => '/foo/bar', 'QUERY_STRING' => 'foo=bar', - ), + ], '/foo/bar?foo=bar', - ), - ); + ], + ]; } public function testTrustedHosts() @@ -1942,7 +2038,7 @@ public function testTrustedHosts() $this->assertEquals('evil.com', $request->getHost()); // add a trusted domain and all its subdomains - Request::setTrustedHosts(array('^([a-z]{9}\.)?trusted\.com$')); + Request::setTrustedHosts(['^([a-z]{9}\.)?trusted\.com$']); // untrusted host $request->headers->set('host', 'evil.com'); @@ -1970,14 +2066,20 @@ public function testTrustedHosts() $request->headers->set('host', 'subdomain.trusted.com'); $this->assertEquals('subdomain.trusted.com', $request->getHost()); + } - // reset request for following tests - Request::setTrustedHosts(array()); + public function testSetTrustedHostsDoesNotBreakOnSpecialCharacters() + { + Request::setTrustedHosts(['localhost(\.local){0,1}#,example.com', 'localhost']); + + $request = Request::create('/'); + $request->headers->set('host', 'localhost'); + $this->assertSame('localhost', $request->getHost()); } public function testFactory() { - Request::setFactory(function (array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null) { + Request::setFactory(function (array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null) { return new NewRequest(); }); @@ -2013,12 +2115,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(); } @@ -2026,23 +2124,23 @@ public function testHostValidity($host, $isValid, $expectedHost = null, $expecte public function getHostValidities() { - return array( - array('.a', false), - array('a..', false), - array('a.', true), - array("\xE9", false), - array('[::1]', true), - array('[::1]:80', true, '[::1]', 80), - array(str_repeat('.', 101), false), - ); + return [ + ['.a', false], + ['a..', false], + ['a.', true], + ["\xE9", false], + ['[::1]', true], + ['[::1]:80', true, '[::1]', 80], + [str_repeat('.', 101), false], + ]; } public function getLongHostNames() { - return array( - array('a'.str_repeat('.a', 40000)), - array(str_repeat(':', 101)), - ); + return [ + ['a'.str_repeat('.a', 40000)], + [str_repeat(':', 101)], + ]; } /** @@ -2057,18 +2155,18 @@ public function testMethodIdempotent($method, $idempotent) public function methodIdempotentProvider() { - return array( - array('HEAD', true), - array('GET', true), - array('POST', false), - array('PUT', true), - array('PATCH', false), - array('DELETE', true), - array('PURGE', true), - array('OPTIONS', true), - array('TRACE', true), - array('CONNECT', false), - ); + return [ + ['HEAD', true], + ['GET', true], + ['POST', false], + ['PUT', true], + ['PATCH', false], + ['DELETE', true], + ['PURGE', true], + ['OPTIONS', true], + ['TRACE', true], + ['CONNECT', false], + ]; } /** @@ -2083,23 +2181,23 @@ public function testMethodSafe($method, $safe) public function methodSafeProvider() { - return array( - array('HEAD', true), - array('GET', true), - array('POST', false), - array('PUT', false), - array('PATCH', false), - array('DELETE', false), - array('PURGE', false), - array('OPTIONS', true), - array('TRACE', true), - array('CONNECT', false), - ); + return [ + ['HEAD', true], + ['GET', true], + ['POST', false], + ['PUT', false], + ['PATCH', false], + ['DELETE', false], + ['PURGE', false], + ['OPTIONS', true], + ['TRACE', true], + ['CONNECT', false], + ]; } /** * @group legacy - * @expectedDeprecation Checking only for cacheable HTTP methods with Symfony\Component\HttpFoundation\Request::isMethodSafe() is deprecated since version 3.2 and will throw an exception in 4.0. Disable checking only for cacheable methods by calling the method with `false` as first argument or use the Request::isMethodCacheable() instead. + * @expectedDeprecation Checking only for cacheable HTTP methods with Symfony\Component\HttpFoundation\Request::isMethodSafe() is deprecated since Symfony 3.2 and will throw an exception in 4.0. Disable checking only for cacheable methods by calling the method with `false` as first argument or use the Request::isMethodCacheable() instead. */ public function testMethodSafeChecksCacheable() { @@ -2111,27 +2209,27 @@ public function testMethodSafeChecksCacheable() /** * @dataProvider methodCacheableProvider */ - public function testMethodCacheable($method, $chacheable) + public function testMethodCacheable($method, $cacheable) { $request = new Request(); $request->setMethod($method); - $this->assertEquals($chacheable, $request->isMethodCacheable()); + $this->assertEquals($cacheable, $request->isMethodCacheable()); } public function methodCacheableProvider() { - return array( - array('HEAD', true), - array('GET', true), - array('POST', false), - array('PUT', false), - array('PATCH', false), - array('DELETE', false), - array('PURGE', false), - array('OPTIONS', false), - array('TRACE', false), - array('CONNECT', false), - ); + return [ + ['HEAD', true], + ['GET', true], + ['POST', false], + ['PUT', false], + ['PATCH', false], + ['DELETE', false], + ['PURGE', false], + ['OPTIONS', false], + ['TRACE', false], + ['CONNECT', false], + ]; } /** @@ -2139,7 +2237,7 @@ public function methodCacheableProvider() */ public function testGetTrustedHeaderName() { - Request::setTrustedProxies(array('8.8.8.8'), Request::HEADER_X_FORWARDED_ALL); + Request::setTrustedProxies(['8.8.8.8'], Request::HEADER_X_FORWARDED_ALL); $this->assertNull(Request::getTrustedHeaderName(Request::HEADER_FORWARDED)); $this->assertSame('X_FORWARDED_FOR', Request::getTrustedHeaderName(Request::HEADER_CLIENT_IP)); @@ -2147,7 +2245,7 @@ public function testGetTrustedHeaderName() $this->assertSame('X_FORWARDED_PORT', Request::getTrustedHeaderName(Request::HEADER_CLIENT_PORT)); $this->assertSame('X_FORWARDED_PROTO', Request::getTrustedHeaderName(Request::HEADER_CLIENT_PROTO)); - Request::setTrustedProxies(array('8.8.8.8'), Request::HEADER_FORWARDED); + Request::setTrustedProxies(['8.8.8.8'], Request::HEADER_FORWARDED); $this->assertSame('FORWARDED', Request::getTrustedHeaderName(Request::HEADER_FORWARDED)); $this->assertNull(Request::getTrustedHeaderName(Request::HEADER_CLIENT_IP)); @@ -2161,7 +2259,7 @@ public function testGetTrustedHeaderName() Request::setTrustedHeaderName(Request::HEADER_CLIENT_PORT, 'D'); Request::setTrustedHeaderName(Request::HEADER_CLIENT_PROTO, 'E'); - Request::setTrustedProxies(array('8.8.8.8'), Request::HEADER_FORWARDED); + Request::setTrustedProxies(['8.8.8.8'], Request::HEADER_FORWARDED); $this->assertSame('A', Request::getTrustedHeaderName(Request::HEADER_FORWARDED)); $this->assertNull(Request::getTrustedHeaderName(Request::HEADER_CLIENT_IP)); @@ -2169,7 +2267,7 @@ public function testGetTrustedHeaderName() $this->assertNull(Request::getTrustedHeaderName(Request::HEADER_CLIENT_PORT)); $this->assertNull(Request::getTrustedHeaderName(Request::HEADER_CLIENT_PROTO)); - Request::setTrustedProxies(array('8.8.8.8'), Request::HEADER_X_FORWARDED_ALL); + Request::setTrustedProxies(['8.8.8.8'], Request::HEADER_X_FORWARDED_ALL); $this->assertNull(Request::getTrustedHeaderName(Request::HEADER_FORWARDED)); $this->assertSame('B', Request::getTrustedHeaderName(Request::HEADER_CLIENT_IP)); @@ -2177,7 +2275,7 @@ public function testGetTrustedHeaderName() $this->assertSame('D', Request::getTrustedHeaderName(Request::HEADER_CLIENT_PORT)); $this->assertSame('E', Request::getTrustedHeaderName(Request::HEADER_CLIENT_PROTO)); - Request::setTrustedProxies(array('8.8.8.8'), Request::HEADER_FORWARDED); + Request::setTrustedProxies(['8.8.8.8'], Request::HEADER_FORWARDED); $this->assertSame('A', Request::getTrustedHeaderName(Request::HEADER_FORWARDED)); @@ -2188,13 +2286,159 @@ public function testGetTrustedHeaderName() Request::setTrustedHeaderName(Request::HEADER_CLIENT_PORT, 'X_FORWARDED_PORT'); Request::setTrustedHeaderName(Request::HEADER_CLIENT_PROTO, 'X_FORWARDED_PROTO'); } + + /** + * @dataProvider protocolVersionProvider + */ + public function testProtocolVersion($serverProtocol, $trustedProxy, $via, $expected) + { + if ($trustedProxy) { + Request::setTrustedProxies(['1.1.1.1'], -1); + } + + $request = new Request(); + $request->server->set('SERVER_PROTOCOL', $serverProtocol); + $request->server->set('REMOTE_ADDR', '1.1.1.1'); + $request->headers->set('Via', $via); + + $this->assertSame($expected, $request->getProtocolVersion()); + } + + public function protocolVersionProvider() + { + return [ + 'untrusted without via' => ['HTTP/2.0', false, '', 'HTTP/2.0'], + 'untrusted with via' => ['HTTP/2.0', false, '1.0 fred, 1.1 nowhere.com (Apache/1.1)', 'HTTP/2.0'], + 'trusted without via' => ['HTTP/2.0', true, '', 'HTTP/2.0'], + 'trusted with via' => ['HTTP/2.0', true, '1.0 fred, 1.1 nowhere.com (Apache/1.1)', 'HTTP/1.0'], + 'trusted with via and protocol name' => ['HTTP/2.0', true, 'HTTP/1.0 fred, HTTP/1.1 nowhere.com (Apache/1.1)', 'HTTP/1.0'], + 'trusted with broken via' => ['HTTP/2.0', true, 'HTTP/1^0 foo', 'HTTP/2.0'], + 'trusted with partially-broken via' => ['HTTP/2.0', true, '1.0 fred, foo', 'HTTP/1.0'], + ]; + } + + public function nonstandardRequestsData() + { + return [ + ['', '', '/', 'http://host:8080/', ''], + ['/', '', '/', 'http://host:8080/', ''], + + ['hello/app.php/x', '', '/x', 'http://host:8080/hello/app.php/x', '/hello', '/hello/app.php'], + ['/hello/app.php/x', '', '/x', 'http://host:8080/hello/app.php/x', '/hello', '/hello/app.php'], + + ['', 'a=b', '/', 'http://host:8080/?a=b'], + ['?a=b', 'a=b', '/', 'http://host:8080/?a=b'], + ['/?a=b', 'a=b', '/', 'http://host:8080/?a=b'], + + ['x', 'a=b', '/x', 'http://host:8080/x?a=b'], + ['x?a=b', 'a=b', '/x', 'http://host:8080/x?a=b'], + ['/x?a=b', 'a=b', '/x', 'http://host:8080/x?a=b'], + + ['hello/x', '', '/x', 'http://host:8080/hello/x', '/hello'], + ['/hello/x', '', '/x', 'http://host:8080/hello/x', '/hello'], + + ['hello/app.php/x', 'a=b', '/x', 'http://host:8080/hello/app.php/x?a=b', '/hello', '/hello/app.php'], + ['hello/app.php/x?a=b', 'a=b', '/x', 'http://host:8080/hello/app.php/x?a=b', '/hello', '/hello/app.php'], + ['/hello/app.php/x?a=b', 'a=b', '/x', 'http://host:8080/hello/app.php/x?a=b', '/hello', '/hello/app.php'], + ]; + } + + /** + * @dataProvider nonstandardRequestsData + */ + public function testNonstandardRequests($requestUri, $queryString, $expectedPathInfo, $expectedUri, $expectedBasePath = '', $expectedBaseUrl = null) + { + if (null === $expectedBaseUrl) { + $expectedBaseUrl = $expectedBasePath; + } + + $server = [ + 'HTTP_HOST' => 'host:8080', + 'SERVER_PORT' => '8080', + 'QUERY_STRING' => $queryString, + 'PHP_SELF' => '/hello/app.php', + 'SCRIPT_FILENAME' => '/some/path/app.php', + 'REQUEST_URI' => $requestUri, + ]; + + $request = new Request([], [], [], [], [], $server); + + $this->assertEquals($expectedPathInfo, $request->getPathInfo()); + $this->assertEquals($expectedUri, $request->getUri()); + $this->assertEquals($queryString, $request->getQueryString()); + $this->assertEquals(8080, $request->getPort()); + $this->assertEquals('host:8080', $request->getHttpHost()); + $this->assertEquals($expectedBaseUrl, $request->getBaseUrl()); + $this->assertEquals($expectedBasePath, $request->getBasePath()); + } + + public function testTrustedHost() + { + Request::setTrustedProxies(['1.1.1.1'], -1); + + $request = Request::create('/'); + $request->server->set('REMOTE_ADDR', '1.1.1.1'); + $request->headers->set('Forwarded', 'host=localhost:8080'); + $request->headers->set('X-Forwarded-Host', 'localhost:8080'); + + $this->assertSame('localhost:8080', $request->getHttpHost()); + $this->assertSame(8080, $request->getPort()); + + $request = Request::create('/'); + $request->server->set('REMOTE_ADDR', '1.1.1.1'); + $request->headers->set('Forwarded', 'host="[::1]:443"'); + $request->headers->set('X-Forwarded-Host', '[::1]:443'); + $request->headers->set('X-Forwarded-Port', 443); + + $this->assertSame('[::1]:443', $request->getHttpHost()); + $this->assertSame(443, $request->getPort()); + } + + public function testTrustedPort() + { + Request::setTrustedProxies(['1.1.1.1'], -1); + + $request = Request::create('/'); + $request->server->set('REMOTE_ADDR', '1.1.1.1'); + $request->headers->set('Forwarded', 'host=localhost:8080'); + $request->headers->set('X-Forwarded-Port', 8080); + + $this->assertSame(8080, $request->getPort()); + + $request = Request::create('/'); + $request->server->set('REMOTE_ADDR', '1.1.1.1'); + $request->headers->set('Forwarded', 'host=localhost'); + $request->headers->set('X-Forwarded-Port', 80); + + $this->assertSame(80, $request->getPort()); + + $request = Request::create('/'); + $request->server->set('REMOTE_ADDR', '1.1.1.1'); + $request->headers->set('Forwarded', 'host="[::1]"'); + $request->headers->set('X-Forwarded-Proto', 'https'); + $request->headers->set('X-Forwarded-Port', 443); + + $this->assertSame(443, $request->getPort()); + } + + public function testTrustedPortDoesNotDefaultToZero() + { + Request::setTrustedProxies(['1.1.1.1'], Request::HEADER_X_FORWARDED_ALL); + + $request = Request::create('/'); + $request->server->set('REMOTE_ADDR', '1.1.1.1'); + $request->headers->set('X-Forwarded-Host', 'test.example.com'); + $request->headers->set('X-Forwarded-Port', ''); + + $this->assertSame(80, $request->getPort()); + } } class RequestContentProxy extends Request { public function getContent($asResource = false) { - return http_build_query(array('_method' => 'PUT', 'content' => 'mycontent')); + return http_build_query(['_method' => 'PUT', 'content' => 'mycontent'], '', '&'); } } diff --git a/Tests/ResponseFunctionalTest.php b/Tests/ResponseFunctionalTest.php new file mode 100644 index 000000000..3d3e696c7 --- /dev/null +++ b/Tests/ResponseFunctionalTest.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Tests; + +use PHPUnit\Framework\TestCase; + +/** + * @requires PHP 7.0 + */ +class ResponseFunctionalTest extends TestCase +{ + private static $server; + + public static function setUpBeforeClass() + { + $spec = [ + 1 => ['file', '/dev/null', 'w'], + 2 => ['file', '/dev/null', 'w'], + ]; + if (!self::$server = @proc_open('exec php -S localhost:8054', $spec, $pipes, __DIR__.'/Fixtures/response-functional')) { + self::markTestSkipped('PHP server unable to start.'); + } + sleep(1); + } + + public static function tearDownAfterClass() + { + if (self::$server) { + proc_terminate(self::$server); + proc_close(self::$server); + } + } + + /** + * @dataProvider provideCookie + */ + public function testCookie($fixture) + { + $result = file_get_contents(sprintf('http://localhost:8054/%s.php', $fixture)); + $this->assertStringMatchesFormatFile(__DIR__.sprintf('/Fixtures/response-functional/%s.expected', $fixture), $result); + } + + public function provideCookie() + { + foreach (glob(__DIR__.'/Fixtures/response-functional/*.php') as $file) { + yield [pathinfo($file, PATHINFO_FILENAME)]; + } + } +} diff --git a/Tests/ResponseHeaderBagTest.php b/Tests/ResponseHeaderBagTest.php index 413656721..d85f6e112 100644 --- a/Tests/ResponseHeaderBagTest.php +++ b/Tests/ResponseHeaderBagTest.php @@ -12,109 +12,96 @@ namespace Symfony\Component\HttpFoundation\Tests; use PHPUnit\Framework\TestCase; -use Symfony\Component\HttpFoundation\ResponseHeaderBag; use Symfony\Component\HttpFoundation\Cookie; +use Symfony\Component\HttpFoundation\ResponseHeaderBag; /** * @group time-sensitive */ class ResponseHeaderBagTest extends TestCase { - /** - * @dataProvider provideAllPreserveCase - */ - public function testAllPreserveCase($headers, $expected) + public function testAllPreserveCase() { - $bag = new ResponseHeaderBag($headers); + $headers = [ + 'fOo' => 'BAR', + 'ETag' => 'xyzzy', + 'Content-MD5' => 'Q2hlY2sgSW50ZWdyaXR5IQ==', + 'P3P' => 'CP="CAO PSA OUR"', + 'WWW-Authenticate' => 'Basic realm="WallyWorld"', + 'X-UA-Compatible' => 'IE=edge,chrome=1', + 'X-XSS-Protection' => '1; mode=block', + ]; - $this->assertEquals($expected, $bag->allPreserveCase(), '->allPreserveCase() gets all input keys in original case'); - } + $bag = new ResponseHeaderBag($headers); + $allPreservedCase = $bag->allPreserveCase(); - public function provideAllPreserveCase() - { - return array( - array( - array('fOo' => 'BAR'), - array('fOo' => array('BAR'), 'Cache-Control' => array('no-cache, private')), - ), - array( - array('ETag' => 'xyzzy'), - array('ETag' => array('xyzzy'), 'Cache-Control' => array('private, must-revalidate')), - ), - array( - array('Content-MD5' => 'Q2hlY2sgSW50ZWdyaXR5IQ=='), - array('Content-MD5' => array('Q2hlY2sgSW50ZWdyaXR5IQ=='), 'Cache-Control' => array('no-cache, private')), - ), - array( - array('P3P' => 'CP="CAO PSA OUR"'), - array('P3P' => array('CP="CAO PSA OUR"'), 'Cache-Control' => array('no-cache, private')), - ), - array( - array('WWW-Authenticate' => 'Basic realm="WallyWorld"'), - array('WWW-Authenticate' => array('Basic realm="WallyWorld"'), 'Cache-Control' => array('no-cache, private')), - ), - array( - array('X-UA-Compatible' => 'IE=edge,chrome=1'), - array('X-UA-Compatible' => array('IE=edge,chrome=1'), 'Cache-Control' => array('no-cache, private')), - ), - array( - array('X-XSS-Protection' => '1; mode=block'), - array('X-XSS-Protection' => array('1; mode=block'), 'Cache-Control' => array('no-cache, private')), - ), - ); + foreach (array_keys($headers) as $headerName) { + $this->assertArrayHasKey($headerName, $allPreservedCase, '->allPreserveCase() gets all input keys in original case'); + } } public function testCacheControlHeader() { - $bag = new ResponseHeaderBag(array()); + $bag = new ResponseHeaderBag([]); $this->assertEquals('no-cache, private', $bag->get('Cache-Control')); $this->assertTrue($bag->hasCacheControlDirective('no-cache')); - $bag = new ResponseHeaderBag(array('Cache-Control' => 'public')); + $bag = new ResponseHeaderBag(['Cache-Control' => 'public']); $this->assertEquals('public', $bag->get('Cache-Control')); $this->assertTrue($bag->hasCacheControlDirective('public')); - $bag = new ResponseHeaderBag(array('ETag' => 'abcde')); + $bag = new ResponseHeaderBag(['ETag' => 'abcde']); $this->assertEquals('private, must-revalidate', $bag->get('Cache-Control')); $this->assertTrue($bag->hasCacheControlDirective('private')); $this->assertTrue($bag->hasCacheControlDirective('must-revalidate')); $this->assertFalse($bag->hasCacheControlDirective('max-age')); - $bag = new ResponseHeaderBag(array('Expires' => 'Wed, 16 Feb 2011 14:17:43 GMT')); + $bag = new ResponseHeaderBag(['Expires' => 'Wed, 16 Feb 2011 14:17:43 GMT']); $this->assertEquals('private, must-revalidate', $bag->get('Cache-Control')); - $bag = new ResponseHeaderBag(array( + $bag = new ResponseHeaderBag([ 'Expires' => 'Wed, 16 Feb 2011 14:17:43 GMT', 'Cache-Control' => 'max-age=3600', - )); + ]); $this->assertEquals('max-age=3600, private', $bag->get('Cache-Control')); - $bag = new ResponseHeaderBag(array('Last-Modified' => 'abcde')); + $bag = new ResponseHeaderBag(['Last-Modified' => 'abcde']); $this->assertEquals('private, must-revalidate', $bag->get('Cache-Control')); - $bag = new ResponseHeaderBag(array('Etag' => 'abcde', 'Last-Modified' => 'abcde')); + $bag = new ResponseHeaderBag(['Etag' => 'abcde', 'Last-Modified' => 'abcde']); $this->assertEquals('private, must-revalidate', $bag->get('Cache-Control')); - $bag = new ResponseHeaderBag(array('cache-control' => 'max-age=100')); + $bag = new ResponseHeaderBag(['cache-control' => 'max-age=100']); $this->assertEquals('max-age=100, private', $bag->get('Cache-Control')); - $bag = new ResponseHeaderBag(array('cache-control' => 's-maxage=100')); + $bag = new ResponseHeaderBag(['cache-control' => 's-maxage=100']); $this->assertEquals('s-maxage=100', $bag->get('Cache-Control')); - $bag = new ResponseHeaderBag(array('cache-control' => 'private, max-age=100')); + $bag = new ResponseHeaderBag(['cache-control' => 'private, max-age=100']); $this->assertEquals('max-age=100, private', $bag->get('Cache-Control')); - $bag = new ResponseHeaderBag(array('cache-control' => 'public, max-age=100')); + $bag = new ResponseHeaderBag(['cache-control' => 'public, max-age=100']); $this->assertEquals('max-age=100, public', $bag->get('Cache-Control')); $bag = new ResponseHeaderBag(); $bag->set('Last-Modified', 'abcde'); $this->assertEquals('private, must-revalidate', $bag->get('Cache-Control')); + + $bag = new ResponseHeaderBag(); + $bag->set('Cache-Control', ['public', 'must-revalidate']); + $this->assertCount(1, $bag->get('Cache-Control', null, false)); + $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->assertEquals('must-revalidate, public', $bag->get('Cache-Control')); } public function testCacheControlClone() { - $headers = array('foo' => 'bar'); + $headers = ['foo' => 'bar']; $bag1 = new ResponseHeaderBag($headers); $bag2 = new ResponseHeaderBag($bag1->allPreserveCase()); $this->assertEquals($bag1->allPreserveCase(), $bag2->allPreserveCase()); @@ -122,44 +109,44 @@ public function testCacheControlClone() public function testToStringIncludesCookieHeaders() { - $bag = new ResponseHeaderBag(array()); + $bag = new ResponseHeaderBag([]); $bag->setCookie(new Cookie('foo', 'bar')); $this->assertSetCookieHeader('foo=bar; path=/; httponly', $bag); $bag->clearCookie('foo'); - $this->assertSetCookieHeader('foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; max-age=-31536001; path=/; httponly', $bag); + $this->assertSetCookieHeader('foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; Max-Age=0; path=/; httponly', $bag); } public function testClearCookieSecureNotHttpOnly() { - $bag = new ResponseHeaderBag(array()); + $bag = new ResponseHeaderBag([]); $bag->clearCookie('foo', '/', null, true, false); - $this->assertSetCookieHeader('foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; max-age=-31536001; path=/; secure', $bag); + $this->assertSetCookieHeader('foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; Max-Age=0; path=/; secure', $bag); } public function testReplace() { - $bag = new ResponseHeaderBag(array()); + $bag = new ResponseHeaderBag([]); $this->assertEquals('no-cache, private', $bag->get('Cache-Control')); $this->assertTrue($bag->hasCacheControlDirective('no-cache')); - $bag->replace(array('Cache-Control' => 'public')); + $bag->replace(['Cache-Control' => 'public']); $this->assertEquals('public', $bag->get('Cache-Control')); $this->assertTrue($bag->hasCacheControlDirective('public')); } public function testReplaceWithRemove() { - $bag = new ResponseHeaderBag(array()); + $bag = new ResponseHeaderBag([]); $this->assertEquals('no-cache, private', $bag->get('Cache-Control')); $this->assertTrue($bag->hasCacheControlDirective('no-cache')); $bag->remove('Cache-Control'); - $bag->replace(array()); + $bag->replace([]); $this->assertEquals('no-cache, private', $bag->get('Cache-Control')); $this->assertTrue($bag->hasCacheControlDirective('no-cache')); } @@ -174,12 +161,12 @@ public function testCookiesWithSameNames() $this->assertCount(4, $bag->getCookies()); $this->assertEquals('foo=bar; path=/path/foo; domain=foo.bar; httponly', $bag->get('set-cookie')); - $this->assertEquals(array( + $this->assertEquals([ 'foo=bar; path=/path/foo; domain=foo.bar; httponly', 'foo=bar; path=/path/bar; domain=foo.bar; httponly', 'foo=bar; path=/path/bar; domain=bar.foo; httponly', 'foo=bar; path=/; httponly', - ), $bag->get('set-cookie', null, false)); + ], $bag->get('set-cookie', null, false)); $this->assertSetCookieHeader('foo=bar; path=/path/foo; domain=foo.bar; httponly', $bag); $this->assertSetCookieHeader('foo=bar; path=/path/bar; domain=foo.bar; httponly', $bag); @@ -188,10 +175,10 @@ public function testCookiesWithSameNames() $cookies = $bag->getCookies(ResponseHeaderBag::COOKIES_ARRAY); - $this->assertTrue(isset($cookies['foo.bar']['/path/foo']['foo'])); - $this->assertTrue(isset($cookies['foo.bar']['/path/bar']['foo'])); - $this->assertTrue(isset($cookies['bar.foo']['/path/bar']['foo'])); - $this->assertTrue(isset($cookies['']['/']['foo'])); + $this->assertArrayHasKey('foo', $cookies['foo.bar']['/path/foo']); + $this->assertArrayHasKey('foo', $cookies['foo.bar']['/path/bar']); + $this->assertArrayHasKey('foo', $cookies['bar.foo']['/path/bar']); + $this->assertArrayHasKey('foo', $cookies['']['/']); } public function testRemoveCookie() @@ -204,19 +191,19 @@ public function testRemoveCookie() $this->assertTrue($bag->has('set-cookie')); $cookies = $bag->getCookies(ResponseHeaderBag::COOKIES_ARRAY); - $this->assertTrue(isset($cookies['foo.bar']['/path/foo'])); + $this->assertArrayHasKey('/path/foo', $cookies['foo.bar']); $bag->removeCookie('foo', '/path/foo', 'foo.bar'); $this->assertTrue($bag->has('set-cookie')); $cookies = $bag->getCookies(ResponseHeaderBag::COOKIES_ARRAY); - $this->assertFalse(isset($cookies['foo.bar']['/path/foo'])); + $this->assertArrayNotHasKey('/path/foo', $cookies['foo.bar']); $bag->removeCookie('bar', '/path/bar', 'foo.bar'); $this->assertFalse($bag->has('set-cookie')); $cookies = $bag->getCookies(ResponseHeaderBag::COOKIES_ARRAY); - $this->assertFalse(isset($cookies['foo.bar'])); + $this->assertArrayNotHasKey('foo.bar', $cookies); } public function testRemoveCookieWithNullRemove() @@ -226,11 +213,11 @@ public function testRemoveCookieWithNullRemove() $bag->setCookie(new Cookie('bar', 'foo', 0)); $cookies = $bag->getCookies(ResponseHeaderBag::COOKIES_ARRAY); - $this->assertTrue(isset($cookies['']['/'])); + $this->assertArrayHasKey('/', $cookies['']); $bag->removeCookie('foo', null); $cookies = $bag->getCookies(ResponseHeaderBag::COOKIES_ARRAY); - $this->assertFalse(isset($cookies['']['/']['foo'])); + $this->assertArrayNotHasKey('foo', $cookies['']['/']); $bag->removeCookie('bar', null); $cookies = $bag->getCookies(ResponseHeaderBag::COOKIES_ARRAY); @@ -241,33 +228,29 @@ public function testSetCookieHeader() { $bag = new ResponseHeaderBag(); $bag->set('set-cookie', 'foo=bar'); - $this->assertEquals(array(new Cookie('foo', 'bar', 0, '/', null, false, false, true)), $bag->getCookies()); + $this->assertEquals([new Cookie('foo', 'bar', 0, '/', null, false, false, true)], $bag->getCookies()); $bag->set('set-cookie', 'foo2=bar2', false); - $this->assertEquals(array( + $this->assertEquals([ new Cookie('foo', 'bar', 0, '/', null, false, false, true), new Cookie('foo2', 'bar2', 0, '/', null, false, false, true), - ), $bag->getCookies()); + ], $bag->getCookies()); $bag->remove('set-cookie'); - $this->assertEquals(array(), $bag->getCookies()); + $this->assertEquals([], $bag->getCookies()); } - /** - * @expectedException \InvalidArgumentException - */ public function testGetCookiesWithInvalidArgument() { + $this->expectException('InvalidArgumentException'); $bag = new ResponseHeaderBag(); $bag->getCookies('invalid_argument'); } - /** - * @expectedException \InvalidArgumentException - */ public function testMakeDispositionInvalidDisposition() { + $this->expectException('InvalidArgumentException'); $headers = new ResponseHeaderBag(); $headers->makeDisposition('invalid', 'foo.html'); @@ -293,28 +276,28 @@ public function testToStringDoesntMessUpHeaders() (string) $headers; $allHeaders = $headers->allPreserveCase(); - $this->assertEquals(array('http://www.symfony.com'), $allHeaders['Location']); - $this->assertEquals(array('text/html'), $allHeaders['Content-type']); + $this->assertEquals(['http://www.symfony.com'], $allHeaders['Location']); + $this->assertEquals(['text/html'], $allHeaders['Content-type']); } public function provideMakeDisposition() { - return array( - array('attachment', 'foo.html', 'foo.html', 'attachment; filename="foo.html"'), - array('attachment', 'foo.html', '', 'attachment; filename="foo.html"'), - array('attachment', 'foo bar.html', '', 'attachment; filename="foo bar.html"'), - array('attachment', 'foo "bar".html', '', 'attachment; filename="foo \\"bar\\".html"'), - array('attachment', 'foo%20bar.html', 'foo bar.html', 'attachment; filename="foo bar.html"; filename*=utf-8\'\'foo%2520bar.html'), - array('attachment', 'föö.html', 'foo.html', 'attachment; filename="foo.html"; filename*=utf-8\'\'f%C3%B6%C3%B6.html'), - ); + return [ + ['attachment', 'foo.html', 'foo.html', 'attachment; filename="foo.html"'], + ['attachment', 'foo.html', '', 'attachment; filename="foo.html"'], + ['attachment', 'foo bar.html', '', 'attachment; filename="foo bar.html"'], + ['attachment', 'foo "bar".html', '', 'attachment; filename="foo \\"bar\\".html"'], + ['attachment', 'foo%20bar.html', 'foo bar.html', 'attachment; filename="foo bar.html"; filename*=utf-8\'\'foo%2520bar.html'], + ['attachment', 'föö.html', 'foo.html', 'attachment; filename="foo.html"; filename*=utf-8\'\'f%C3%B6%C3%B6.html'], + ]; } /** * @dataProvider provideMakeDispositionFail - * @expectedException \InvalidArgumentException */ public function testMakeDispositionFail($disposition, $filename) { + $this->expectException('InvalidArgumentException'); $headers = new ResponseHeaderBag(); $headers->makeDisposition($disposition, $filename); @@ -322,14 +305,51 @@ public function testMakeDispositionFail($disposition, $filename) public function provideMakeDispositionFail() { - return array( - array('attachment', 'foo%20bar.html'), - array('attachment', 'foo/bar.html'), - array('attachment', '/foo.html'), - array('attachment', 'foo\bar.html'), - array('attachment', '\foo.html'), - array('attachment', 'föö.html'), - ); + return [ + ['attachment', 'foo%20bar.html'], + ['attachment', 'foo/bar.html'], + ['attachment', '/foo.html'], + ['attachment', 'foo\bar.html'], + ['attachment', '\foo.html'], + ['attachment', 'föö.html'], + ]; + } + + public function testDateHeaderAddedOnCreation() + { + $now = time(); + + $bag = new ResponseHeaderBag(); + $this->assertTrue($bag->has('Date')); + + $this->assertEquals($now, $bag->getDate('Date')->getTimestamp()); + } + + public function testDateHeaderCanBeSetOnCreation() + { + $someDate = 'Thu, 23 Mar 2017 09:15:12 GMT'; + $bag = new ResponseHeaderBag(['Date' => $someDate]); + + $this->assertEquals($someDate, $bag->get('Date')); + } + + public function testDateHeaderWillBeRecreatedWhenRemoved() + { + $someDate = 'Thu, 23 Mar 2017 09:15:12 GMT'; + $bag = new ResponseHeaderBag(['Date' => $someDate]); + $bag->remove('Date'); + + // a (new) Date header is still present + $this->assertTrue($bag->has('Date')); + $this->assertNotEquals($someDate, $bag->get('Date')); + } + + public function testDateHeaderWillBeRecreatedWhenHeadersAreReplaced() + { + $bag = new ResponseHeaderBag(); + $bag->replace([]); + + $this->assertTrue($bag->has('Date')); } private function assertSetCookieHeader($expected, ResponseHeaderBag $actual) diff --git a/Tests/ResponseTest.php b/Tests/ResponseTest.php index 62b8c6525..b846cdad3 100644 --- a/Tests/ResponseTest.php +++ b/Tests/ResponseTest.php @@ -21,7 +21,7 @@ class ResponseTest extends ResponseTestCase { public function testCreate() { - $response = Response::create('foo', 301, array('Foo' => 'bar')); + $response = Response::create('foo', 301, ['Foo' => 'bar']); $this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $response); $this->assertEquals(301, $response->getStatusCode()); @@ -126,7 +126,7 @@ public function testMustRevalidateWithProxyRevalidateCacheControlHeader() public function testSetNotModified() { - $response = new Response(); + $response = new Response('foo'); $modified = $response->setNotModified(); $this->assertObjectHasAttribute('headers', $modified); $this->assertObjectHasAttribute('content', $modified); @@ -135,6 +135,11 @@ public function testSetNotModified() $this->assertObjectHasAttribute('statusText', $modified); $this->assertObjectHasAttribute('charset', $modified); $this->assertEquals(304, $modified->getStatusCode()); + + ob_start(); + $modified->sendContent(); + $string = ob_get_clean(); + $this->assertEmpty($string); } public function testIsSuccessful() @@ -248,10 +253,10 @@ public function testIsNotModifiedIfModifiedSinceAndEtagWithoutLastModified() public function testIsValidateable() { - $response = new Response('', 200, array('Last-Modified' => $this->createDateTimeOneHourAgo()->format(DATE_RFC2822))); + $response = new Response('', 200, ['Last-Modified' => $this->createDateTimeOneHourAgo()->format(DATE_RFC2822)]); $this->assertTrue($response->isValidateable(), '->isValidateable() returns true if Last-Modified is present'); - $response = new Response('', 200, array('ETag' => '"12345"')); + $response = new Response('', 200, ['ETag' => '"12345"']); $this->assertTrue($response->isValidateable(), '->isValidateable() returns true if ETag is present'); $response = new Response(); @@ -261,7 +266,7 @@ public function testIsValidateable() public function testGetDate() { $oneHourAgo = $this->createDateTimeOneHourAgo(); - $response = new Response('', 200, array('Date' => $oneHourAgo->format(DATE_RFC2822))); + $response = new Response('', 200, ['Date' => $oneHourAgo->format(DATE_RFC2822)]); $date = $response->getDate(); $this->assertEquals($oneHourAgo->getTimestamp(), $date->getTimestamp(), '->getDate() returns the Date header if present'); @@ -269,7 +274,7 @@ public function testGetDate() $date = $response->getDate(); $this->assertEquals(time(), $date->getTimestamp(), '->getDate() returns the current Date if no Date header present'); - $response = new Response('', 200, array('Date' => $this->createDateTimeOneHourAgo()->format(DATE_RFC2822))); + $response = new Response('', 200, ['Date' => $this->createDateTimeOneHourAgo()->format(DATE_RFC2822)]); $now = $this->createDateTimeNow(); $response->headers->set('Date', $now->format(DATE_RFC2822)); $date = $response->getDate(); @@ -357,6 +362,17 @@ public function testExpire() $response->headers->set('Expires', -1); $response->expire(); $this->assertNull($response->headers->get('Age'), '->expire() does not set the Age when the response is expired'); + + $response = new Response(); + $response->headers->set('Expires', date(DATE_RFC2822, time() + 600)); + $response->expire(); + $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() @@ -404,21 +420,21 @@ public function testGetSetProtocolVersion() public function testGetVary() { $response = new Response(); - $this->assertEquals(array(), $response->getVary(), '->getVary() returns an empty array if no Vary header is present'); + $this->assertEquals([], $response->getVary(), '->getVary() returns an empty array if no Vary header is present'); $response = new Response(); $response->headers->set('Vary', 'Accept-Language'); - $this->assertEquals(array('Accept-Language'), $response->getVary(), '->getVary() parses a single header name value'); + $this->assertEquals(['Accept-Language'], $response->getVary(), '->getVary() parses a single header name value'); $response = new Response(); $response->headers->set('Vary', 'Accept-Language User-Agent X-Foo'); - $this->assertEquals(array('Accept-Language', 'User-Agent', 'X-Foo'), $response->getVary(), '->getVary() parses multiple header name values separated by spaces'); + $this->assertEquals(['Accept-Language', 'User-Agent', 'X-Foo'], $response->getVary(), '->getVary() parses multiple header name values separated by spaces'); $response = new Response(); $response->headers->set('Vary', 'Accept-Language,User-Agent, X-Foo'); - $this->assertEquals(array('Accept-Language', 'User-Agent', 'X-Foo'), $response->getVary(), '->getVary() parses multiple header name values separated by commas'); + $this->assertEquals(['Accept-Language', 'User-Agent', 'X-Foo'], $response->getVary(), '->getVary() parses multiple header name values separated by commas'); - $vary = array('Accept-Language', 'User-Agent', 'X-foo'); + $vary = ['Accept-Language', 'User-Agent', 'X-foo']; $response = new Response(); $response->headers->set('Vary', $vary); @@ -433,18 +449,18 @@ public function testSetVary() { $response = new Response(); $response->setVary('Accept-Language'); - $this->assertEquals(array('Accept-Language'), $response->getVary()); + $this->assertEquals(['Accept-Language'], $response->getVary()); $response->setVary('Accept-Language, User-Agent'); - $this->assertEquals(array('Accept-Language', 'User-Agent'), $response->getVary(), '->setVary() replace the vary header by default'); + $this->assertEquals(['Accept-Language', 'User-Agent'], $response->getVary(), '->setVary() replace the vary header by default'); $response->setVary('X-Foo', false); - $this->assertEquals(array('Accept-Language', 'User-Agent', 'X-Foo'), $response->getVary(), '->setVary() doesn\'t wipe out earlier Vary headers if replace is set to false'); + $this->assertEquals(['Accept-Language', 'User-Agent', 'X-Foo'], $response->getVary(), '->setVary() doesn\'t wipe out earlier Vary headers if replace is set to false'); } public function testDefaultContentType() { - $headerMock = $this->getMockBuilder('Symfony\Component\HttpFoundation\ResponseHeaderBag')->setMethods(array('set'))->getMock(); + $headerMock = $this->getMockBuilder('Symfony\Component\HttpFoundation\ResponseHeaderBag')->setMethods(['set'])->getMock(); $headerMock->expects($this->at(0)) ->method('set') ->with('Content-Type', 'text/html'); @@ -522,7 +538,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); @@ -566,50 +581,56 @@ public function testPrepareSetsPragmaOnHttp10Only() public function testSetCache() { $response = new Response(); - //array('etag', 'last_modified', 'max_age', 's_maxage', 'private', 'public') + // ['etag', 'last_modified', 'max_age', 's_maxage', 'private', 'public'] try { - $response->setCache(array('wrong option' => 'value')); + $response->setCache(['wrong option' => 'value']); $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 = array('etag' => '"whatever"'); + $options = ['etag' => '"whatever"']; $response->setCache($options); $this->assertEquals($response->getEtag(), '"whatever"'); $now = $this->createDateTimeNow(); - $options = array('last_modified' => $now); + $options = ['last_modified' => $now]; $response->setCache($options); $this->assertEquals($response->getLastModified()->getTimestamp(), $now->getTimestamp()); - $options = array('max_age' => 100); + $options = ['max_age' => 100]; $response->setCache($options); $this->assertEquals($response->getMaxAge(), 100); - $options = array('s_maxage' => 200); + $options = ['s_maxage' => 200]; $response->setCache($options); $this->assertEquals($response->getMaxAge(), 200); $this->assertTrue($response->headers->hasCacheControlDirective('public')); $this->assertFalse($response->headers->hasCacheControlDirective('private')); - $response->setCache(array('public' => true)); + $response->setCache(['public' => true]); $this->assertTrue($response->headers->hasCacheControlDirective('public')); $this->assertFalse($response->headers->hasCacheControlDirective('private')); - $response->setCache(array('public' => false)); + $response->setCache(['public' => false]); $this->assertFalse($response->headers->hasCacheControlDirective('public')); $this->assertTrue($response->headers->hasCacheControlDirective('private')); - $response->setCache(array('private' => true)); + $response->setCache(['private' => true]); $this->assertFalse($response->headers->hasCacheControlDirective('public')); $this->assertTrue($response->headers->hasCacheControlDirective('private')); - $response->setCache(array('private' => false)); + $response->setCache(['private' => false]); $this->assertTrue($response->headers->hasCacheControlDirective('public')); $this->assertFalse($response->headers->hasCacheControlDirective('private')); + + $response->setCache(['immutable' => true]); + $this->assertTrue($response->headers->hasCacheControlDirective('immutable')); + + $response->setCache(['immutable' => false]); + $this->assertFalse($response->headers->hasCacheControlDirective('immutable')); } public function testSendContent() @@ -619,7 +640,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() @@ -631,6 +652,22 @@ public function testSetPublic() $this->assertFalse($response->headers->hasCacheControlDirective('private')); } + public function testSetImmutable() + { + $response = new Response(); + $response->setImmutable(); + + $this->assertTrue($response->headers->hasCacheControlDirective('immutable')); + } + + public function testIsImmutable() + { + $response = new Response(); + $response->setImmutable(); + + $this->assertTrue($response->isImmutable()); + } + public function testSetExpires() { $response = new Response(); @@ -693,14 +730,14 @@ public function testSetStatusCode($code, $text, $expectedText) public function getStatusCodeFixtures() { - return array( - array('200', null, 'OK'), - array('200', false, ''), - array('200', 'foo', 'foo'), - array('199', null, 'unknown status'), - array('199', false, ''), - array('199', 'foo', 'foo'), - ); + return [ + ['200', null, 'OK'], + ['200', false, ''], + ['200', 'foo', 'foo'], + ['199', null, 'unknown status'], + ['199', false, ''], + ['199', 'foo', 'foo'], + ]; } public function testIsInformational() @@ -714,7 +751,7 @@ public function testIsInformational() public function testIsRedirectRedirection() { - foreach (array(301, 302, 303, 307) as $code) { + foreach ([301, 302, 303, 307] as $code) { $response = new Response('', $code); $this->assertTrue($response->isRedirection()); $this->assertTrue($response->isRedirect()); @@ -732,7 +769,7 @@ public function testIsRedirectRedirection() $this->assertFalse($response->isRedirection()); $this->assertFalse($response->isRedirect()); - $response = new Response('', 301, array('Location' => '/good-uri')); + $response = new Response('', 301, ['Location' => '/good-uri']); $this->assertFalse($response->isRedirect('/bad-uri')); $this->assertTrue($response->isRedirect('/good-uri')); } @@ -748,7 +785,7 @@ public function testIsNotFound() public function testIsEmpty() { - foreach (array(204, 304) as $code) { + foreach ([204, 304] as $code) { $response = new Response('', $code); $this->assertTrue($response->isEmpty()); } @@ -797,7 +834,7 @@ public function testHasVary() public function testSetEtag() { - $response = new Response('', 200, array('ETag' => '"12345"')); + $response = new Response('', 200, ['ETag' => '"12345"']); $response->setEtag(); $this->assertNull($response->headers->get('Etag'), '->setEtag() removes Etags when call with null'); @@ -814,11 +851,11 @@ public function testSetContent($content) } /** - * @expectedException \UnexpectedValueException * @dataProvider invalidContentProvider */ public function testSetContentInvalid($content) { + $this->expectException('UnexpectedValueException'); $response = new Response(); $response->setContent($content); } @@ -827,7 +864,7 @@ public function testSettersAreChainable() { $response = new Response(); - $setters = array( + $setters = [ 'setProtocolVersion' => '1.0', 'setCharset' => 'UTF-8', 'setPublic' => null, @@ -838,7 +875,7 @@ public function testSettersAreChainable() 'setSharedMaxAge' => 1, 'setTtl' => 1, 'setClientTtl' => 1, - ); + ]; foreach ($setters as $setter => $arg) { $this->assertEquals($response, $response->{$setter}($arg)); @@ -857,20 +894,20 @@ public function testNoDeprecationsAreTriggered() public function validContentProvider() { - return array( - 'obj' => array(new StringableObject()), - 'string' => array('Foo'), - 'int' => array(2), - ); + return [ + 'obj' => [new StringableObject()], + 'string' => ['Foo'], + 'int' => [2], + ]; } public function invalidContentProvider() { - return array( - 'obj' => array(new \stdClass()), - 'array' => array(array()), - 'bool' => array(true, '1'), - ); + return [ + 'obj' => [new \stdClass()], + 'array' => [[]], + 'bool' => [true, '1'], + ]; } protected function createDateTimeOneHourAgo() @@ -896,35 +933,36 @@ 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() { - if (!in_array('https', stream_get_wrappers(), true)) { + if (!\in_array('https', stream_get_wrappers(), true)) { $this->markTestSkipped('The "https" wrapper is not available'); } $ianaHttpStatusCodes = new \DOMDocument(); - libxml_set_streams_context(stream_context_create(array( - 'http' => array( + $context = stream_context_create([ + 'http' => [ 'method' => 'GET', 'timeout' => 30, - ), - ))); + 'user_agent' => __METHOD__, + ], + ]); - $ianaHttpStatusCodes->load('https://www.iana.org/assignments/http-status-codes/http-status-codes.xml'); + $ianaHttpStatusCodes->loadXML(file_get_contents('https://www.iana.org/assignments/http-status-codes/http-status-codes.xml', false, $context)); if (!$ianaHttpStatusCodes->relaxNGValidate(__DIR__.'/schema/http-status-codes.rng')) { self::fail('Invalid IANA\'s HTTP status code list.'); } - $ianaCodesReasonPhrases = array(); + $ianaCodesReasonPhrases = []; - $xpath = new \DomXPath($ianaHttpStatusCodes); + $xpath = new \DOMXPath($ianaHttpStatusCodes); $xpath->registerNamespace('ns', 'http://www.iana.org/assignments'); $records = $xpath->query('//ns:record'); @@ -932,16 +970,16 @@ public function ianaCodesReasonPhrasesProvider() $value = $xpath->query('.//ns:value', $record)->item(0)->nodeValue; $description = $xpath->query('.//ns:description', $record)->item(0)->nodeValue; - if (in_array($description, array('Unassigned', '(Unused)'), true)) { + if (\in_array($description, ['Unassigned', '(Unused)'], true)) { continue; } if (preg_match('/^([0-9]+)\s*\-\s*([0-9]+)$/', $value, $matches)) { for ($value = $matches[1]; $value <= $matches[2]; ++$value) { - $ianaCodesReasonPhrases[] = array($value, $description); + $ianaCodesReasonPhrases[] = [$value, $description]; } } else { - $ianaCodesReasonPhrases[] = array($value, $description); + $ianaCodesReasonPhrases[] = [$value, $description]; } } @@ -968,14 +1006,3 @@ public function __toString() class DefaultResponse extends Response { } - -class ExtendedResponse extends Response -{ - public function setLastModified(\DateTime $date = null) - { - } - - public function getDate() - { - } -} diff --git a/Tests/ServerBagTest.php b/Tests/ServerBagTest.php index c1d9d12a6..0663b118e 100644 --- a/Tests/ServerBagTest.php +++ b/Tests/ServerBagTest.php @@ -23,7 +23,7 @@ class ServerBagTest extends TestCase { public function testShouldExtractHeadersFromServerArray() { - $server = array( + $server = [ 'SOME_SERVER_VARIABLE' => 'value', 'SOME_SERVER_VARIABLE2' => 'value', 'ROOT' => 'value', @@ -32,125 +32,125 @@ public function testShouldExtractHeadersFromServerArray() 'HTTP_ETAG' => 'asdf', 'PHP_AUTH_USER' => 'foo', 'PHP_AUTH_PW' => 'bar', - ); + ]; $bag = new ServerBag($server); - $this->assertEquals(array( + $this->assertEquals([ 'CONTENT_TYPE' => 'text/html', 'CONTENT_LENGTH' => '0', 'ETAG' => 'asdf', 'AUTHORIZATION' => 'Basic '.base64_encode('foo:bar'), 'PHP_AUTH_USER' => 'foo', 'PHP_AUTH_PW' => 'bar', - ), $bag->getHeaders()); + ], $bag->getHeaders()); } public function testHttpPasswordIsOptional() { - $bag = new ServerBag(array('PHP_AUTH_USER' => 'foo')); + $bag = new ServerBag(['PHP_AUTH_USER' => 'foo']); - $this->assertEquals(array( + $this->assertEquals([ 'AUTHORIZATION' => 'Basic '.base64_encode('foo:'), 'PHP_AUTH_USER' => 'foo', 'PHP_AUTH_PW' => '', - ), $bag->getHeaders()); + ], $bag->getHeaders()); } public function testHttpBasicAuthWithPhpCgi() { - $bag = new ServerBag(array('HTTP_AUTHORIZATION' => 'Basic '.base64_encode('foo:bar'))); + $bag = new ServerBag(['HTTP_AUTHORIZATION' => 'Basic '.base64_encode('foo:bar')]); - $this->assertEquals(array( + $this->assertEquals([ 'AUTHORIZATION' => 'Basic '.base64_encode('foo:bar'), 'PHP_AUTH_USER' => 'foo', 'PHP_AUTH_PW' => 'bar', - ), $bag->getHeaders()); + ], $bag->getHeaders()); } public function testHttpBasicAuthWithPhpCgiBogus() { - $bag = new ServerBag(array('HTTP_AUTHORIZATION' => 'Basic_'.base64_encode('foo:bar'))); + $bag = new ServerBag(['HTTP_AUTHORIZATION' => 'Basic_'.base64_encode('foo:bar')]); // Username and passwords should not be set as the header is bogus $headers = $bag->getHeaders(); - $this->assertFalse(isset($headers['PHP_AUTH_USER'])); - $this->assertFalse(isset($headers['PHP_AUTH_PW'])); + $this->assertArrayNotHasKey('PHP_AUTH_USER', $headers); + $this->assertArrayNotHasKey('PHP_AUTH_PW', $headers); } public function testHttpBasicAuthWithPhpCgiRedirect() { - $bag = new ServerBag(array('REDIRECT_HTTP_AUTHORIZATION' => 'Basic '.base64_encode('username:pass:word'))); + $bag = new ServerBag(['REDIRECT_HTTP_AUTHORIZATION' => 'Basic '.base64_encode('username:pass:word')]); - $this->assertEquals(array( + $this->assertEquals([ 'AUTHORIZATION' => 'Basic '.base64_encode('username:pass:word'), 'PHP_AUTH_USER' => 'username', 'PHP_AUTH_PW' => 'pass:word', - ), $bag->getHeaders()); + ], $bag->getHeaders()); } public function testHttpBasicAuthWithPhpCgiEmptyPassword() { - $bag = new ServerBag(array('HTTP_AUTHORIZATION' => 'Basic '.base64_encode('foo:'))); + $bag = new ServerBag(['HTTP_AUTHORIZATION' => 'Basic '.base64_encode('foo:')]); - $this->assertEquals(array( + $this->assertEquals([ 'AUTHORIZATION' => 'Basic '.base64_encode('foo:'), 'PHP_AUTH_USER' => 'foo', 'PHP_AUTH_PW' => '', - ), $bag->getHeaders()); + ], $bag->getHeaders()); } public function testHttpDigestAuthWithPhpCgi() { $digest = 'Digest username="foo", realm="acme", nonce="'.md5('secret').'", uri="/protected, qop="auth"'; - $bag = new ServerBag(array('HTTP_AUTHORIZATION' => $digest)); + $bag = new ServerBag(['HTTP_AUTHORIZATION' => $digest]); - $this->assertEquals(array( + $this->assertEquals([ 'AUTHORIZATION' => $digest, 'PHP_AUTH_DIGEST' => $digest, - ), $bag->getHeaders()); + ], $bag->getHeaders()); } public function testHttpDigestAuthWithPhpCgiBogus() { $digest = 'Digest_username="foo", realm="acme", nonce="'.md5('secret').'", uri="/protected, qop="auth"'; - $bag = new ServerBag(array('HTTP_AUTHORIZATION' => $digest)); + $bag = new ServerBag(['HTTP_AUTHORIZATION' => $digest]); // Username and passwords should not be set as the header is bogus $headers = $bag->getHeaders(); - $this->assertFalse(isset($headers['PHP_AUTH_USER'])); - $this->assertFalse(isset($headers['PHP_AUTH_PW'])); + $this->assertArrayNotHasKey('PHP_AUTH_USER', $headers); + $this->assertArrayNotHasKey('PHP_AUTH_PW', $headers); } public function testHttpDigestAuthWithPhpCgiRedirect() { $digest = 'Digest username="foo", realm="acme", nonce="'.md5('secret').'", uri="/protected, qop="auth"'; - $bag = new ServerBag(array('REDIRECT_HTTP_AUTHORIZATION' => $digest)); + $bag = new ServerBag(['REDIRECT_HTTP_AUTHORIZATION' => $digest]); - $this->assertEquals(array( + $this->assertEquals([ 'AUTHORIZATION' => $digest, 'PHP_AUTH_DIGEST' => $digest, - ), $bag->getHeaders()); + ], $bag->getHeaders()); } public function testOAuthBearerAuth() { $headerContent = 'Bearer L-yLEOr9zhmUYRkzN1jwwxwQ-PBNiKDc8dgfB4hTfvo'; - $bag = new ServerBag(array('HTTP_AUTHORIZATION' => $headerContent)); + $bag = new ServerBag(['HTTP_AUTHORIZATION' => $headerContent]); - $this->assertEquals(array( + $this->assertEquals([ 'AUTHORIZATION' => $headerContent, - ), $bag->getHeaders()); + ], $bag->getHeaders()); } public function testOAuthBearerAuthWithRedirect() { $headerContent = 'Bearer L-yLEOr9zhmUYRkzN1jwwxwQ-PBNiKDc8dgfB4hTfvo'; - $bag = new ServerBag(array('REDIRECT_HTTP_AUTHORIZATION' => $headerContent)); + $bag = new ServerBag(['REDIRECT_HTTP_AUTHORIZATION' => $headerContent]); - $this->assertEquals(array( + $this->assertEquals([ 'AUTHORIZATION' => $headerContent, - ), $bag->getHeaders()); + ], $bag->getHeaders()); } /** @@ -159,12 +159,12 @@ public function testOAuthBearerAuthWithRedirect() public function testItDoesNotOverwriteTheAuthorizationHeaderIfItIsAlreadySet() { $headerContent = 'Bearer L-yLEOr9zhmUYRkzN1jwwxwQ-PBNiKDc8dgfB4hTfvo'; - $bag = new ServerBag(array('PHP_AUTH_USER' => 'foo', 'HTTP_AUTHORIZATION' => $headerContent)); + $bag = new ServerBag(['PHP_AUTH_USER' => 'foo', 'HTTP_AUTHORIZATION' => $headerContent]); - $this->assertEquals(array( + $this->assertEquals([ 'AUTHORIZATION' => $headerContent, 'PHP_AUTH_USER' => 'foo', 'PHP_AUTH_PW' => '', - ), $bag->getHeaders()); + ], $bag->getHeaders()); } } diff --git a/Tests/Session/Attribute/AttributeBagTest.php b/Tests/Session/Attribute/AttributeBagTest.php index 8c148b58f..3f2f7b3c8 100644 --- a/Tests/Session/Attribute/AttributeBagTest.php +++ b/Tests/Session/Attribute/AttributeBagTest.php @@ -21,10 +21,7 @@ */ class AttributeBagTest extends TestCase { - /** - * @var array - */ - private $array; + private $array = []; /** * @var AttributeBag @@ -33,21 +30,21 @@ class AttributeBagTest extends TestCase protected function setUp() { - $this->array = array( + $this->array = [ 'hello' => 'world', 'always' => 'be happy', 'user.login' => 'drak', - 'csrf.token' => array( + 'csrf.token' => [ 'a' => '1234', 'b' => '4321', - ), - 'category' => array( - 'fishing' => array( + ], + 'category' => [ + 'fishing' => [ 'first' => 'cod', 'second' => 'sole', - ), - ), - ); + ], + ], + ]; $this->bag = new AttributeBag('_sf2'); $this->bag->initialize($this->array); } @@ -55,7 +52,7 @@ protected function setUp() protected function tearDown() { $this->bag = null; - $this->array = array(); + $this->array = []; } public function testInitialize() @@ -63,7 +60,7 @@ public function testInitialize() $bag = new AttributeBag(); $bag->initialize($this->array); $this->assertEquals($this->array, $bag->all()); - $array = array('should' => 'change'); + $array = ['should' => 'change']; $bag->initialize($array); $this->assertEquals($array, $bag->all()); } @@ -125,7 +122,7 @@ public function testAll() public function testReplace() { - $array = array(); + $array = []; $array['name'] = 'jack'; $array['foo.bar'] = 'beep'; $this->bag->replace($array); @@ -153,22 +150,22 @@ public function testRemove() public function testClear() { $this->bag->clear(); - $this->assertEquals(array(), $this->bag->all()); + $this->assertEquals([], $this->bag->all()); } public function attributesProvider() { - return array( - array('hello', 'world', true), - array('always', 'be happy', true), - array('user.login', 'drak', true), - array('csrf.token', array('a' => '1234', 'b' => '4321'), true), - array('category', array('fishing' => array('first' => 'cod', 'second' => 'sole')), true), - array('user2.login', null, false), - array('never', null, false), - array('bye', null, false), - array('bye/for/now', null, false), - ); + return [ + ['hello', 'world', true], + ['always', 'be happy', true], + ['user.login', 'drak', true], + ['csrf.token', ['a' => '1234', 'b' => '4321'], true], + ['category', ['fishing' => ['first' => 'cod', 'second' => 'sole']], true], + ['user2.login', null, false], + ['never', null, false], + ['bye', null, false], + ['bye/for/now', null, false], + ]; } public function testGetIterator() @@ -179,11 +176,11 @@ public function testGetIterator() ++$i; } - $this->assertEquals(count($this->array), $i); + $this->assertEquals(\count($this->array), $i); } public function testCount() { - $this->assertEquals(count($this->array), count($this->bag)); + $this->assertCount(\count($this->array), $this->bag); } } diff --git a/Tests/Session/Attribute/NamespacedAttributeBagTest.php b/Tests/Session/Attribute/NamespacedAttributeBagTest.php index d9d9eb7fb..6b4bb17d6 100644 --- a/Tests/Session/Attribute/NamespacedAttributeBagTest.php +++ b/Tests/Session/Attribute/NamespacedAttributeBagTest.php @@ -21,10 +21,7 @@ */ class NamespacedAttributeBagTest extends TestCase { - /** - * @var array - */ - private $array; + private $array = []; /** * @var NamespacedAttributeBag @@ -33,21 +30,21 @@ class NamespacedAttributeBagTest extends TestCase protected function setUp() { - $this->array = array( + $this->array = [ 'hello' => 'world', 'always' => 'be happy', 'user.login' => 'drak', - 'csrf.token' => array( + 'csrf.token' => [ 'a' => '1234', 'b' => '4321', - ), - 'category' => array( - 'fishing' => array( + ], + 'category' => [ + 'fishing' => [ 'first' => 'cod', 'second' => 'sole', - ), - ), - ); + ], + ], + ]; $this->bag = new NamespacedAttributeBag('_sf2', '/'); $this->bag->initialize($this->array); } @@ -55,7 +52,7 @@ protected function setUp() protected function tearDown() { $this->bag = null; - $this->array = array(); + $this->array = []; } public function testInitialize() @@ -63,7 +60,7 @@ public function testInitialize() $bag = new NamespacedAttributeBag(); $bag->initialize($this->array); $this->assertEquals($this->array, $this->bag->all()); - $array = array('should' => 'not stick'); + $array = ['should' => 'not stick']; $bag->initialize($array); // should have remained the same @@ -85,6 +82,17 @@ public function testHas($key, $value, $exists) $this->assertEquals($exists, $this->bag->has($key)); } + /** + * @dataProvider attributesProvider + */ + public function testHasNoSideEffect($key, $value, $expected) + { + $expected = json_encode($this->bag->all()); + $this->bag->has($key); + + $this->assertEquals($expected, json_encode($this->bag->all())); + } + /** * @dataProvider attributesProvider */ @@ -99,6 +107,17 @@ public function testGetDefaults() $this->assertEquals('default', $this->bag->get('user2.login', 'default')); } + /** + * @dataProvider attributesProvider + */ + public function testGetNoSideEffect($key, $value, $expected) + { + $expected = json_encode($this->bag->all()); + $this->bag->get($key); + + $this->assertEquals($expected, json_encode($this->bag->all())); + } + /** * @dataProvider attributesProvider */ @@ -120,7 +139,7 @@ public function testAll() public function testReplace() { - $array = array(); + $array = []; $array['name'] = 'jack'; $array['foo.bar'] = 'beep'; $this->bag->replace($array); @@ -158,28 +177,28 @@ public function testRemoveNonexistingNamespacedAttribute() public function testClear() { $this->bag->clear(); - $this->assertEquals(array(), $this->bag->all()); + $this->assertEquals([], $this->bag->all()); } public function attributesProvider() { - return array( - array('hello', 'world', true), - array('always', 'be happy', true), - array('user.login', 'drak', true), - array('csrf.token', array('a' => '1234', 'b' => '4321'), true), - array('csrf.token/a', '1234', true), - array('csrf.token/b', '4321', true), - array('category', array('fishing' => array('first' => 'cod', 'second' => 'sole')), true), - array('category/fishing', array('first' => 'cod', 'second' => 'sole'), true), - array('category/fishing/missing/first', null, false), - array('category/fishing/first', 'cod', true), - array('category/fishing/second', 'sole', true), - array('category/fishing/missing/second', null, false), - array('user2.login', null, false), - array('never', null, false), - array('bye', null, false), - array('bye/for/now', null, false), - ); + return [ + ['hello', 'world', true], + ['always', 'be happy', true], + ['user.login', 'drak', true], + ['csrf.token', ['a' => '1234', 'b' => '4321'], true], + ['csrf.token/a', '1234', true], + ['csrf.token/b', '4321', true], + ['category', ['fishing' => ['first' => 'cod', 'second' => 'sole']], true], + ['category/fishing', ['first' => 'cod', 'second' => 'sole'], true], + ['category/fishing/missing/first', null, false], + ['category/fishing/first', 'cod', true], + ['category/fishing/second', 'sole', true], + ['category/fishing/missing/second', null, false], + ['user2.login', null, false], + ['never', null, false], + ['bye', null, false], + ['bye/for/now', null, false], + ]; } } diff --git a/Tests/Session/Flash/AutoExpireFlashBagTest.php b/Tests/Session/Flash/AutoExpireFlashBagTest.php index 4eb200afa..b4e2c3a5a 100644 --- a/Tests/Session/Flash/AutoExpireFlashBagTest.php +++ b/Tests/Session/Flash/AutoExpireFlashBagTest.php @@ -26,16 +26,13 @@ class AutoExpireFlashBagTest extends TestCase */ private $bag; - /** - * @var array - */ - protected $array = array(); + protected $array = []; protected function setUp() { parent::setUp(); $this->bag = new FlashBag(); - $this->array = array('new' => array('notice' => array('A previous flash message'))); + $this->array = ['new' => ['notice' => ['A previous flash message']]]; $this->bag->initialize($this->array); } @@ -48,21 +45,21 @@ protected function tearDown() public function testInitialize() { $bag = new FlashBag(); - $array = array('new' => array('notice' => array('A previous flash message'))); + $array = ['new' => ['notice' => ['A previous flash message']]]; $bag->initialize($array); - $this->assertEquals(array('A previous flash message'), $bag->peek('notice')); - $array = array('new' => array( - 'notice' => array('Something else'), - 'error' => array('a'), - )); + $this->assertEquals(['A previous flash message'], $bag->peek('notice')); + $array = ['new' => [ + 'notice' => ['Something else'], + 'error' => ['a'], + ]]; $bag->initialize($array); - $this->assertEquals(array('Something else'), $bag->peek('notice')); - $this->assertEquals(array('a'), $bag->peek('error')); + $this->assertEquals(['Something else'], $bag->peek('notice')); + $this->assertEquals(['a'], $bag->peek('error')); } public function testGetStorageKey() { - $this->assertEquals('_sf2_flashes', $this->bag->getStorageKey()); + $this->assertEquals('_symfony_flashes', $this->bag->getStorageKey()); $attributeBag = new FlashBag('test'); $this->assertEquals('test', $attributeBag->getStorageKey()); } @@ -76,16 +73,16 @@ public function testGetSetName() public function testPeek() { - $this->assertEquals(array(), $this->bag->peek('non_existing')); - $this->assertEquals(array('default'), $this->bag->peek('non_existing', array('default'))); - $this->assertEquals(array('A previous flash message'), $this->bag->peek('notice')); - $this->assertEquals(array('A previous flash message'), $this->bag->peek('notice')); + $this->assertEquals([], $this->bag->peek('non_existing')); + $this->assertEquals(['default'], $this->bag->peek('non_existing', ['default'])); + $this->assertEquals(['A previous flash message'], $this->bag->peek('notice')); + $this->assertEquals(['A previous flash message'], $this->bag->peek('notice')); } public function testSet() { $this->bag->set('notice', 'Foo'); - $this->assertEquals(array('A previous flash message'), $this->bag->peek('notice')); + $this->assertEquals(['A previous flash message'], $this->bag->peek('notice')); } public function testHas() @@ -96,43 +93,43 @@ public function testHas() public function testKeys() { - $this->assertEquals(array('notice'), $this->bag->keys()); + $this->assertEquals(['notice'], $this->bag->keys()); } public function testPeekAll() { - $array = array( - 'new' => array( + $array = [ + 'new' => [ 'notice' => 'Foo', 'error' => 'Bar', - ), - ); + ], + ]; $this->bag->initialize($array); - $this->assertEquals(array( + $this->assertEquals([ 'notice' => 'Foo', 'error' => 'Bar', - ), $this->bag->peekAll() + ], $this->bag->peekAll() ); - $this->assertEquals(array( + $this->assertEquals([ 'notice' => 'Foo', 'error' => 'Bar', - ), $this->bag->peekAll() + ], $this->bag->peekAll() ); } public function testGet() { - $this->assertEquals(array(), $this->bag->get('non_existing')); - $this->assertEquals(array('default'), $this->bag->get('non_existing', array('default'))); - $this->assertEquals(array('A previous flash message'), $this->bag->get('notice')); - $this->assertEquals(array(), $this->bag->get('notice')); + $this->assertEquals([], $this->bag->get('non_existing')); + $this->assertEquals(['default'], $this->bag->get('non_existing', ['default'])); + $this->assertEquals(['A previous flash message'], $this->bag->get('notice')); + $this->assertEquals([], $this->bag->get('notice')); } public function testSetAll() { - $this->bag->setAll(array('a' => 'first', 'b' => 'second')); + $this->bag->setAll(['a' => 'first', 'b' => 'second']); $this->assertFalse($this->bag->has('a')); $this->assertFalse($this->bag->has('b')); } @@ -141,16 +138,24 @@ public function testAll() { $this->bag->set('notice', 'Foo'); $this->bag->set('error', 'Bar'); - $this->assertEquals(array( - 'notice' => array('A previous flash message'), - ), $this->bag->all() + $this->assertEquals([ + 'notice' => ['A previous flash message'], + ], $this->bag->all() ); - $this->assertEquals(array(), $this->bag->all()); + $this->assertEquals([], $this->bag->all()); } public function testClear() { - $this->assertEquals(array('notice' => array('A previous flash message')), $this->bag->clear()); + $this->assertEquals(['notice' => ['A previous flash message']], $this->bag->clear()); + } + + public function testDoNotRemoveTheNewFlashesWhenDisplayingTheExistingOnes() + { + $this->bag->add('success', 'Something'); + $this->bag->all(); + + $this->assertEquals(['new' => ['success' => ['Something']], 'display' => []], $this->array); } } diff --git a/Tests/Session/Flash/FlashBagTest.php b/Tests/Session/Flash/FlashBagTest.php index f0aa6a615..6d8619e07 100644 --- a/Tests/Session/Flash/FlashBagTest.php +++ b/Tests/Session/Flash/FlashBagTest.php @@ -26,16 +26,13 @@ class FlashBagTest extends TestCase */ private $bag; - /** - * @var array - */ - protected $array = array(); + protected $array = []; protected function setUp() { parent::setUp(); $this->bag = new FlashBag(); - $this->array = array('notice' => array('A previous flash message')); + $this->array = ['notice' => ['A previous flash message']]; $this->bag->initialize($this->array); } @@ -50,14 +47,14 @@ public function testInitialize() $bag = new FlashBag(); $bag->initialize($this->array); $this->assertEquals($this->array, $bag->peekAll()); - $array = array('should' => array('change')); + $array = ['should' => ['change']]; $bag->initialize($array); $this->assertEquals($array, $bag->peekAll()); } public function testGetStorageKey() { - $this->assertEquals('_sf2_flashes', $this->bag->getStorageKey()); + $this->assertEquals('_symfony_flashes', $this->bag->getStorageKey()); $attributeBag = new FlashBag('test'); $this->assertEquals('test', $attributeBag->getStorageKey()); } @@ -71,37 +68,49 @@ public function testGetSetName() public function testPeek() { - $this->assertEquals(array(), $this->bag->peek('non_existing')); - $this->assertEquals(array('default'), $this->bag->peek('not_existing', array('default'))); - $this->assertEquals(array('A previous flash message'), $this->bag->peek('notice')); - $this->assertEquals(array('A previous flash message'), $this->bag->peek('notice')); + $this->assertEquals([], $this->bag->peek('non_existing')); + $this->assertEquals(['default'], $this->bag->peek('not_existing', ['default'])); + $this->assertEquals(['A previous flash message'], $this->bag->peek('notice')); + $this->assertEquals(['A previous flash message'], $this->bag->peek('notice')); + } + + public function testAdd() + { + $tab = ['bar' => 'baz']; + $this->bag->add('string_message', 'lorem'); + $this->bag->add('object_message', new \stdClass()); + $this->bag->add('array_message', $tab); + + $this->assertEquals(['lorem'], $this->bag->get('string_message')); + $this->assertEquals([new \stdClass()], $this->bag->get('object_message')); + $this->assertEquals([$tab], $this->bag->get('array_message')); } public function testGet() { - $this->assertEquals(array(), $this->bag->get('non_existing')); - $this->assertEquals(array('default'), $this->bag->get('not_existing', array('default'))); - $this->assertEquals(array('A previous flash message'), $this->bag->get('notice')); - $this->assertEquals(array(), $this->bag->get('notice')); + $this->assertEquals([], $this->bag->get('non_existing')); + $this->assertEquals(['default'], $this->bag->get('not_existing', ['default'])); + $this->assertEquals(['A previous flash message'], $this->bag->get('notice')); + $this->assertEquals([], $this->bag->get('notice')); } public function testAll() { $this->bag->set('notice', 'Foo'); $this->bag->set('error', 'Bar'); - $this->assertEquals(array( - 'notice' => array('Foo'), - 'error' => array('Bar'), ), $this->bag->all() + $this->assertEquals([ + 'notice' => ['Foo'], + 'error' => ['Bar'], ], $this->bag->all() ); - $this->assertEquals(array(), $this->bag->all()); + $this->assertEquals([], $this->bag->all()); } public function testSet() { $this->bag->set('notice', 'Foo'); $this->bag->set('notice', 'Bar'); - $this->assertEquals(array('Bar'), $this->bag->peek('notice')); + $this->assertEquals(['Bar'], $this->bag->peek('notice')); } public function testHas() @@ -112,24 +121,37 @@ public function testHas() public function testKeys() { - $this->assertEquals(array('notice'), $this->bag->keys()); + $this->assertEquals(['notice'], $this->bag->keys()); + } + + public function testSetAll() + { + $this->bag->add('one_flash', 'Foo'); + $this->bag->add('another_flash', 'Bar'); + $this->assertTrue($this->bag->has('one_flash')); + $this->assertTrue($this->bag->has('another_flash')); + $this->bag->setAll(['unique_flash' => 'FooBar']); + $this->assertFalse($this->bag->has('one_flash')); + $this->assertFalse($this->bag->has('another_flash')); + $this->assertSame(['unique_flash' => 'FooBar'], $this->bag->all()); + $this->assertSame([], $this->bag->all()); } public function testPeekAll() { $this->bag->set('notice', 'Foo'); $this->bag->set('error', 'Bar'); - $this->assertEquals(array( - 'notice' => array('Foo'), - 'error' => array('Bar'), - ), $this->bag->peekAll() + $this->assertEquals([ + 'notice' => ['Foo'], + 'error' => ['Bar'], + ], $this->bag->peekAll() ); $this->assertTrue($this->bag->has('notice')); $this->assertTrue($this->bag->has('error')); - $this->assertEquals(array( - 'notice' => array('Foo'), - 'error' => array('Bar'), - ), $this->bag->peekAll() + $this->assertEquals([ + 'notice' => ['Foo'], + 'error' => ['Bar'], + ], $this->bag->peekAll() ); } } diff --git a/Tests/Session/SessionTest.php b/Tests/Session/SessionTest.php index fa93507a4..acb129984 100644 --- a/Tests/Session/SessionTest.php +++ b/Tests/Session/SessionTest.php @@ -12,9 +12,10 @@ namespace Symfony\Component\HttpFoundation\Tests\Session; use PHPUnit\Framework\TestCase; -use Symfony\Component\HttpFoundation\Session\Session; -use Symfony\Component\HttpFoundation\Session\Flash\FlashBag; use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag; +use Symfony\Component\HttpFoundation\Session\Flash\FlashBag; +use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\HttpFoundation\Session\SessionBagProxy; use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; /** @@ -70,6 +71,27 @@ public function testSetId() $this->assertEquals('0123456789abcdef', $this->session->getId()); } + public function testSetIdAfterStart() + { + $this->session->start(); + $id = $this->session->getId(); + + $e = null; + try { + $this->session->setId($id); + } catch (\Exception $e) { + } + + $this->assertNull($e); + + try { + $this->session->setId('different'); + } catch (\Exception $e) { + } + + $this->assertInstanceOf('\LogicException', $e); + } + public function testSetName() { $this->assertEquals('MOCKSESSID', $this->session->getName()); @@ -106,10 +128,10 @@ public function testHas($key, $value) public function testReplace() { - $this->session->replace(array('happiness' => 'be good', 'symfony' => 'awesome')); - $this->assertEquals(array('happiness' => 'be good', 'symfony' => 'awesome'), $this->session->all()); - $this->session->replace(array()); - $this->assertEquals(array(), $this->session->all()); + $this->session->replace(['happiness' => 'be good', 'symfony' => 'awesome']); + $this->assertEquals(['happiness' => 'be good', 'symfony' => 'awesome'], $this->session->all()); + $this->session->replace([]); + $this->assertEquals([], $this->session->all()); } /** @@ -129,16 +151,16 @@ public function testClear($key, $value) $this->session->set('hi', 'fabien'); $this->session->set($key, $value); $this->session->clear(); - $this->assertEquals(array(), $this->session->all()); + $this->assertEquals([], $this->session->all()); } public function setProvider() { - return array( - array('foo', 'bar', array('foo' => 'bar')), - array('foo.bar', 'too much beer', array('foo.bar' => 'too much beer')), - array('great', 'symfony is great', array('great' => 'symfony is great')), - ); + return [ + ['foo', 'bar', ['foo' => 'bar']], + ['foo.bar', 'too much beer', ['foo.bar' => 'too much beer']], + ['great', 'symfony is great', ['great' => 'symfony is great']], + ]; } /** @@ -149,14 +171,14 @@ public function testRemove($key, $value) $this->session->set('hi.world', 'have a nice day'); $this->session->set($key, $value); $this->session->remove($key); - $this->assertEquals(array('hi.world' => 'have a nice day'), $this->session->all()); + $this->assertEquals(['hi.world' => 'have a nice day'], $this->session->all()); } public function testInvalidate() { $this->session->set('invalidate', 123); $this->session->invalidate(); - $this->assertEquals(array(), $this->session->all()); + $this->assertEquals([], $this->session->all()); } public function testMigrate() @@ -195,7 +217,7 @@ public function testGetFlashBag() public function testGetIterator() { - $attributes = array('hello' => 'world', 'symfony' => 'rocks'); + $attributes = ['hello' => 'world', 'symfony' => 'rocks']; foreach ($attributes as $key => $val) { $this->session->set($key, $val); } @@ -206,7 +228,7 @@ public function testGetIterator() ++$i; } - $this->assertEquals(count($attributes), $i); + $this->assertEquals(\count($attributes), $i); } public function testGetCount() @@ -221,4 +243,46 @@ public function testGetMeta() { $this->assertInstanceOf('Symfony\Component\HttpFoundation\Session\Storage\MetadataBag', $this->session->getMetadataBag()); } + + public function testIsEmpty() + { + $this->assertTrue($this->session->isEmpty()); + + $this->session->set('hello', 'world'); + $this->assertFalse($this->session->isEmpty()); + + $this->session->remove('hello'); + $this->assertTrue($this->session->isEmpty()); + + $flash = $this->session->getFlashBag(); + $flash->set('hello', 'world'); + $this->assertFalse($this->session->isEmpty()); + + $flash->get('hello'); + $this->assertTrue($this->session->isEmpty()); + } + + public function testGetBagWithBagImplementingGetBag() + { + $bag = new AttributeBag(); + $bag->setName('foo'); + + $storage = new MockArraySessionStorage(); + $storage->registerBag($bag); + + $this->assertSame($bag, (new Session($storage))->getBag('foo')); + } + + public function testGetBagWithBagNotImplementingGetBag() + { + $data = []; + + $bag = new AttributeBag(); + $bag->setName('foo'); + + $storage = new MockArraySessionStorage(); + $storage->registerBag(new SessionBagProxy($bag, $data, $usageIndex)); + + $this->assertSame($bag, (new Session($storage))->getBag('foo')); + } } diff --git a/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php b/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php new file mode 100644 index 000000000..98bc67bca --- /dev/null +++ b/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler; + +use PHPUnit\Framework\TestCase; + +/** + * @requires PHP 7.0 + */ +class AbstractSessionHandlerTest extends TestCase +{ + private static $server; + + public static function setUpBeforeClass() + { + $spec = [ + 1 => ['file', '/dev/null', 'w'], + 2 => ['file', '/dev/null', 'w'], + ]; + if (!self::$server = @proc_open('exec php -S localhost:8053', $spec, $pipes, __DIR__.'/Fixtures')) { + self::markTestSkipped('PHP server unable to start.'); + } + sleep(1); + } + + public static function tearDownAfterClass() + { + if (self::$server) { + proc_terminate(self::$server); + proc_close(self::$server); + } + } + + /** + * @dataProvider provideSession + */ + public function testSession($fixture) + { + $context = ['http' => ['header' => "Cookie: sid=123abc\r\n"]]; + $context = stream_context_create($context); + $result = file_get_contents(sprintf('http://localhost:8053/%s.php', $fixture), false, $context); + + $this->assertStringEqualsFile(__DIR__.sprintf('/Fixtures/%s.expected', $fixture), $result); + } + + public function provideSession() + { + foreach (glob(__DIR__.'/Fixtures/*.php') as $file) { + yield [pathinfo($file, PATHINFO_FILENAME)]; + } + } +} diff --git a/Tests/Session/Storage/Handler/Fixtures/common.inc b/Tests/Session/Storage/Handler/Fixtures/common.inc new file mode 100644 index 000000000..7a064c7f3 --- /dev/null +++ b/Tests/Session/Storage/Handler/Fixtures/common.inc @@ -0,0 +1,151 @@ +data = $data; + } + + public function open($path, $name) + { + echo __FUNCTION__, "\n"; + + return parent::open($path, $name); + } + + public function validateId($sessionId) + { + echo __FUNCTION__, "\n"; + + return parent::validateId($sessionId); + } + + /** + * {@inheritdoc} + */ + public function read($sessionId) + { + echo __FUNCTION__, "\n"; + + return parent::read($sessionId); + } + + /** + * {@inheritdoc} + */ + public function updateTimestamp($sessionId, $data) + { + echo __FUNCTION__, "\n"; + + return true; + } + + /** + * {@inheritdoc} + */ + public function write($sessionId, $data) + { + echo __FUNCTION__, "\n"; + + return parent::write($sessionId, $data); + } + + /** + * {@inheritdoc} + */ + public function destroy($sessionId) + { + echo __FUNCTION__, "\n"; + + return parent::destroy($sessionId); + } + + public function close() + { + echo __FUNCTION__, "\n"; + + return true; + } + + public function gc($maxLifetime) + { + echo __FUNCTION__, "\n"; + + return true; + } + + protected function doRead($sessionId) + { + echo __FUNCTION__.': ', $this->data, "\n"; + + return $this->data; + } + + protected function doWrite($sessionId, $data) + { + echo __FUNCTION__.': ', $data, "\n"; + + return true; + } + + protected function doDestroy($sessionId) + { + echo __FUNCTION__, "\n"; + + return true; + } +} diff --git a/Tests/Session/Storage/Handler/Fixtures/empty_destroys.expected b/Tests/Session/Storage/Handler/Fixtures/empty_destroys.expected new file mode 100644 index 000000000..820371474 --- /dev/null +++ b/Tests/Session/Storage/Handler/Fixtures/empty_destroys.expected @@ -0,0 +1,17 @@ +open +validateId +read +doRead: abc|i:123; +read + +write +destroy +doDestroy +close +Array +( + [0] => Content-Type: text/plain; charset=utf-8 + [1] => Cache-Control: max-age=10800, 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/empty_destroys.php b/Tests/Session/Storage/Handler/Fixtures/empty_destroys.php new file mode 100644 index 000000000..3cfc1250a --- /dev/null +++ b/Tests/Session/Storage/Handler/Fixtures/empty_destroys.php @@ -0,0 +1,8 @@ + Content-Type: text/plain; charset=utf-8 + [1] => Cache-Control: max-age=10800, private, must-revalidate +) +shutdown diff --git a/Tests/Session/Storage/Handler/Fixtures/read_only.php b/Tests/Session/Storage/Handler/Fixtures/read_only.php new file mode 100644 index 000000000..3e62fb9ec --- /dev/null +++ b/Tests/Session/Storage/Handler/Fixtures/read_only.php @@ -0,0 +1,8 @@ + Content-Type: text/plain; charset=utf-8 + [1] => Cache-Control: max-age=10800, private, must-revalidate + [2] => Set-Cookie: sid=random_session_id; path=/; secure; HttpOnly +) +shutdown diff --git a/Tests/Session/Storage/Handler/Fixtures/regenerate.php b/Tests/Session/Storage/Handler/Fixtures/regenerate.php new file mode 100644 index 000000000..a0f635c87 --- /dev/null +++ b/Tests/Session/Storage/Handler/Fixtures/regenerate.php @@ -0,0 +1,10 @@ + bar +) +$_SESSION is not empty +write +destroy +close +$_SESSION is not empty +Array +( + [0] => Content-Type: text/plain; charset=utf-8 + [1] => Cache-Control: max-age=0, private, must-revalidate +) +shutdown diff --git a/Tests/Session/Storage/Handler/Fixtures/storage.php b/Tests/Session/Storage/Handler/Fixtures/storage.php new file mode 100644 index 000000000..96dca3c2c --- /dev/null +++ b/Tests/Session/Storage/Handler/Fixtures/storage.php @@ -0,0 +1,24 @@ +setSaveHandler(new TestSessionHandler()); +$flash = new FlashBag(); +$storage->registerBag($flash); +$storage->start(); + +$flash->add('foo', 'bar'); + +print_r($flash->get('foo')); +echo empty($_SESSION) ? '$_SESSION is empty' : '$_SESSION is not empty'; +echo "\n"; + +$storage->save(); + +echo empty($_SESSION) ? '$_SESSION is empty' : '$_SESSION is not empty'; + +ob_start(function ($buffer) { return str_replace(session_id(), 'random_session_id', $buffer); }); diff --git a/Tests/Session/Storage/Handler/Fixtures/with_cookie.expected b/Tests/Session/Storage/Handler/Fixtures/with_cookie.expected new file mode 100644 index 000000000..33da0a5be --- /dev/null +++ b/Tests/Session/Storage/Handler/Fixtures/with_cookie.expected @@ -0,0 +1,15 @@ +open +validateId +read +doRead: abc|i:123; +read + +updateTimestamp +close +Array +( + [0] => Content-Type: text/plain; charset=utf-8 + [1] => Cache-Control: max-age=10800, private, must-revalidate + [2] => Set-Cookie: abc=def +) +shutdown diff --git a/Tests/Session/Storage/Handler/Fixtures/with_cookie.php b/Tests/Session/Storage/Handler/Fixtures/with_cookie.php new file mode 100644 index 000000000..ffb5b20a3 --- /dev/null +++ b/Tests/Session/Storage/Handler/Fixtures/with_cookie.php @@ -0,0 +1,8 @@ + Content-Type: text/plain; charset=utf-8 + [1] => Cache-Control: max-age=10800, private, must-revalidate + [2] => Set-Cookie: abc=def +) +shutdown diff --git a/Tests/Session/Storage/Handler/Fixtures/with_cookie_and_session.php b/Tests/Session/Storage/Handler/Fixtures/with_cookie_and_session.php new file mode 100644 index 000000000..ec5119323 --- /dev/null +++ b/Tests/Session/Storage/Handler/Fixtures/with_cookie_and_session.php @@ -0,0 +1,13 @@ +markTestSkipped('PHPUnit_MockObject cannot mock the Memcache class on HHVM. See https://github.com/sebastianbergmann/phpunit-mock-objects/pull/289'); } @@ -39,7 +41,7 @@ protected function setUp() $this->memcache = $this->getMockBuilder('Memcache')->getMock(); $this->storage = new MemcacheSessionHandler( $this->memcache, - array('prefix' => self::PREFIX, 'expiretime' => self::TTL) + ['prefix' => self::PREFIX, 'expiretime' => self::TTL] ); } @@ -77,7 +79,7 @@ public function testWriteSession() ->expects($this->once()) ->method('set') ->with(self::PREFIX.'id', 'data', 0, $this->equalTo(time() + self::TTL, 2)) - ->will($this->returnValue(true)) + ->willReturn(true) ; $this->assertTrue($this->storage->write('id', 'data')); @@ -89,7 +91,7 @@ public function testDestroySession() ->expects($this->once()) ->method('delete') ->with(self::PREFIX.'id') - ->will($this->returnValue(true)) + ->willReturn(true) ; $this->assertTrue($this->storage->destroy('id')); @@ -115,12 +117,12 @@ public function testSupportedOptions($options, $supported) public function getOptionFixtures() { - return array( - array(array('prefix' => 'session'), true), - array(array('expiretime' => 100), true), - array(array('prefix' => 'session', 'expiretime' => 200), true), - array(array('expiretime' => 100, 'foo' => 'bar'), false), - ); + return [ + [['prefix' => 'session'], true], + [['expiretime' => 100], true], + [['prefix' => 'session', 'expiretime' => 200], true], + [['expiretime' => 100, 'foo' => 'bar'], false], + ]; } public function testGetConnection() diff --git a/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php b/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php index 2e7be359e..c3deb7aed 100644 --- a/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php @@ -32,7 +32,7 @@ class MemcachedSessionHandlerTest extends TestCase protected function setUp() { - if (defined('HHVM_VERSION')) { + if (\defined('HHVM_VERSION')) { $this->markTestSkipped('PHPUnit_MockObject cannot mock the Memcached class on HHVM. See https://github.com/sebastianbergmann/phpunit-mock-objects/pull/289'); } @@ -45,7 +45,7 @@ protected function setUp() $this->memcached = $this->getMockBuilder('Memcached')->getMock(); $this->storage = new MemcachedSessionHandler( $this->memcached, - array('prefix' => self::PREFIX, 'expiretime' => self::TTL) + ['prefix' => self::PREFIX, 'expiretime' => self::TTL] ); } @@ -63,6 +63,12 @@ public function testOpenSession() public function testCloseSession() { + $this->memcached + ->expects($this->once()) + ->method('quit') + ->willReturn(true) + ; + $this->assertTrue($this->storage->close()); } @@ -83,7 +89,7 @@ public function testWriteSession() ->expects($this->once()) ->method('set') ->with(self::PREFIX.'id', 'data', $this->equalTo(time() + self::TTL, 2)) - ->will($this->returnValue(true)) + ->willReturn(true) ; $this->assertTrue($this->storage->write('id', 'data')); @@ -95,7 +101,7 @@ public function testDestroySession() ->expects($this->once()) ->method('delete') ->with(self::PREFIX.'id') - ->will($this->returnValue(true)) + ->willReturn(true) ; $this->assertTrue($this->storage->destroy('id')); @@ -121,12 +127,12 @@ public function testSupportedOptions($options, $supported) public function getOptionFixtures() { - return array( - array(array('prefix' => 'session'), true), - array(array('expiretime' => 100), true), - array(array('prefix' => 'session', 'expiretime' => 200), true), - array(array('expiretime' => 100, 'foo' => 'bar'), false), - ); + return [ + [['prefix' => 'session'], true], + [['expiretime' => 100], true], + [['prefix' => 'session', 'expiretime' => 200], true], + [['expiretime' => 100, 'foo' => 'bar'], false], + ]; } public function testGetConnection() diff --git a/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php b/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php index 74366863f..f0f43d05b 100644 --- a/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php @@ -11,17 +11,19 @@ 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; /** * @author Markus Bachmann * @group time-sensitive + * @group legacy */ class MongoDbSessionHandlerTest extends TestCase { /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ private $mongo; private $storage; @@ -31,11 +33,11 @@ protected function setUp() { parent::setUp(); - if (extension_loaded('mongodb')) { + if (\extension_loaded('mongodb')) { if (!class_exists('MongoDB\Client')) { $this->markTestSkipped('The mongodb/mongodb package is required.'); } - } elseif (!extension_loaded('mongo')) { + } elseif (!\extension_loaded('mongo')) { $this->markTestSkipped('The Mongo or MongoDB extension is required.'); } @@ -49,32 +51,28 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $this->options = array( + $this->options = [ 'id_field' => '_id', 'data_field' => 'data', 'time_field' => 'time', 'expiry_field' => 'expires_at', 'database' => 'sf2-test', 'collection' => 'session-test', - ); + ]; $this->storage = new MongoDbSessionHandler($this->mongo, $this->options); } - /** - * @expectedException \InvalidArgumentException - */ public function testConstructorShouldThrowExceptionForInvalidMongo() { + $this->expectException('InvalidArgumentException'); new MongoDbSessionHandler(new \stdClass(), $this->options); } - /** - * @expectedException \InvalidArgumentException - */ public function testConstructorShouldThrowExceptionForMissingOptions() { - new MongoDbSessionHandler($this->mongo, array()); + $this->expectException('InvalidArgumentException'); + new MongoDbSessionHandler($this->mongo, []); } public function testOpenMethodAlwaysReturnTrue() @@ -94,7 +92,7 @@ public function testRead() $this->mongo->expects($this->once()) ->method('selectCollection') ->with($this->options['database'], $this->options['collection']) - ->will($this->returnValue($collection)); + ->willReturn($collection); // defining the timeout before the actual method call // allows to test for "greater than" values in the $criteria @@ -102,7 +100,7 @@ public function testRead() $collection->expects($this->once()) ->method('findOne') - ->will($this->returnCallback(function ($criteria) use ($testTimeout) { + ->willReturnCallback(function ($criteria) use ($testTimeout) { $this->assertArrayHasKey($this->options['id_field'], $criteria); $this->assertEquals($criteria[$this->options['id_field']], 'foo'); @@ -117,9 +115,9 @@ public function testRead() $this->assertGreaterThanOrEqual($criteria[$this->options['expiry_field']]['$gte']->sec, $testTimeout); } - $fields = array( + $fields = [ $this->options['id_field'] => 'foo', - ); + ]; if (phpversion('mongodb')) { $fields[$this->options['data_field']] = new \MongoDB\BSON\Binary('bar', \MongoDB\BSON\Binary::TYPE_OLD_BINARY); @@ -130,7 +128,7 @@ public function testRead() } return $fields; - })); + }); $this->assertEquals('bar', $this->storage->read('foo')); } @@ -142,25 +140,25 @@ public function testWrite() $this->mongo->expects($this->once()) ->method('selectCollection') ->with($this->options['database'], $this->options['collection']) - ->will($this->returnValue($collection)); + ->willReturn($collection); - $data = array(); + $data = []; $methodName = phpversion('mongodb') ? 'updateOne' : 'update'; $collection->expects($this->once()) ->method($methodName) - ->will($this->returnCallback(function ($criteria, $updateData, $options) use (&$data) { - $this->assertEquals(array($this->options['id_field'] => 'foo'), $criteria); + ->willReturnCallback(function ($criteria, $updateData, $options) use (&$data) { + $this->assertEquals([$this->options['id_field'] => 'foo'], $criteria); if (phpversion('mongodb')) { - $this->assertEquals(array('upsert' => true), $options); + $this->assertEquals(['upsert' => true], $options); } else { - $this->assertEquals(array('upsert' => true, 'multiple' => false), $options); + $this->assertEquals(['upsert' => true, 'multiple' => false], $options); } $data = $updateData['$set']; - })); + }); $expectedExpiry = time() + (int) ini_get('session.gc_maxlifetime'); $this->assertTrue($this->storage->write('foo', 'bar')); @@ -180,14 +178,14 @@ public function testWrite() public function testWriteWhenUsingExpiresField() { - $this->options = array( + $this->options = [ 'id_field' => '_id', 'data_field' => 'data', 'time_field' => 'time', 'database' => 'sf2-test', 'collection' => 'session-test', 'expiry_field' => 'expiresAt', - ); + ]; $this->storage = new MongoDbSessionHandler($this->mongo, $this->options); @@ -196,25 +194,25 @@ public function testWriteWhenUsingExpiresField() $this->mongo->expects($this->once()) ->method('selectCollection') ->with($this->options['database'], $this->options['collection']) - ->will($this->returnValue($collection)); + ->willReturn($collection); - $data = array(); + $data = []; $methodName = phpversion('mongodb') ? 'updateOne' : 'update'; $collection->expects($this->once()) ->method($methodName) - ->will($this->returnCallback(function ($criteria, $updateData, $options) use (&$data) { - $this->assertEquals(array($this->options['id_field'] => 'foo'), $criteria); + ->willReturnCallback(function ($criteria, $updateData, $options) use (&$data) { + $this->assertEquals([$this->options['id_field'] => 'foo'], $criteria); if (phpversion('mongodb')) { - $this->assertEquals(array('upsert' => true), $options); + $this->assertEquals(['upsert' => true], $options); } else { - $this->assertEquals(array('upsert' => true, 'multiple' => false), $options); + $this->assertEquals(['upsert' => true, 'multiple' => false], $options); } $data = $updateData['$set']; - })); + }); $this->assertTrue($this->storage->write('foo', 'bar')); @@ -236,17 +234,17 @@ public function testReplaceSessionData() $this->mongo->expects($this->once()) ->method('selectCollection') ->with($this->options['database'], $this->options['collection']) - ->will($this->returnValue($collection)); + ->willReturn($collection); - $data = array(); + $data = []; $methodName = phpversion('mongodb') ? 'updateOne' : 'update'; $collection->expects($this->exactly(2)) ->method($methodName) - ->will($this->returnCallback(function ($criteria, $updateData, $options) use (&$data) { + ->willReturnCallback(function ($criteria, $updateData, $options) use (&$data) { $data = $updateData; - })); + }); $this->storage->write('foo', 'bar'); $this->storage->write('foo', 'foobar'); @@ -265,13 +263,13 @@ public function testDestroy() $this->mongo->expects($this->once()) ->method('selectCollection') ->with($this->options['database'], $this->options['collection']) - ->will($this->returnValue($collection)); + ->willReturn($collection); $methodName = phpversion('mongodb') ? 'deleteOne' : 'remove'; $collection->expects($this->once()) ->method($methodName) - ->with(array($this->options['id_field'] => 'foo')); + ->with([$this->options['id_field'] => 'foo']); $this->assertTrue($this->storage->destroy('foo')); } @@ -283,13 +281,13 @@ public function testGc() $this->mongo->expects($this->once()) ->method('selectCollection') ->with($this->options['database'], $this->options['collection']) - ->will($this->returnValue($collection)); + ->willReturn($collection); - $methodName = phpversion('mongodb') ? 'deleteOne' : 'remove'; + $methodName = phpversion('mongodb') ? 'deleteMany' : 'remove'; $collection->expects($this->once()) ->method($methodName) - ->will($this->returnCallback(function ($criteria) { + ->willReturnCallback(function ($criteria) { if (phpversion('mongodb')) { $this->assertInstanceOf('MongoDB\BSON\UTCDateTime', $criteria[$this->options['expiry_field']]['$lt']); $this->assertGreaterThanOrEqual(time() - 1, round((string) $criteria[$this->options['expiry_field']]['$lt'] / 1000)); @@ -297,7 +295,7 @@ public function testGc() $this->assertInstanceOf('MongoDate', $criteria[$this->options['expiry_field']]['$lt']); $this->assertGreaterThanOrEqual(time() - 1, $criteria[$this->options['expiry_field']]['$lt']->sec); } - })); + }); $this->assertTrue($this->storage->gc(1)); } diff --git a/Tests/Session/Storage/Handler/NativeFileSessionHandlerTest.php b/Tests/Session/Storage/Handler/NativeFileSessionHandlerTest.php index a6264e51d..7de55798a 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(array('name' => 'TESTING'), new NativeFileSessionHandler(sys_get_temp_dir())); + $storage = new NativeSessionStorage(['name' => 'TESTING'], new NativeFileSessionHandler(sys_get_temp_dir())); $this->assertEquals('files', $storage->getSaveHandler()->getSaveHandlerName()); $this->assertEquals('user', ini_get('session.save_handler')); @@ -41,9 +41,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); } @@ -52,25 +52,23 @@ public function savePathDataProvider() { $base = sys_get_temp_dir(); - return array( - array("$base/foo", "$base/foo", "$base/foo"), - array("5;$base/foo", "5;$base/foo", "$base/foo"), - array("5;0600;$base/foo", "5;0600;$base/foo", "$base/foo"), - ); + return [ + ["$base/foo", "$base/foo", "$base/foo"], + ["5;$base/foo", "5;$base/foo", "$base/foo"], + ["5;0600;$base/foo", "5;0600;$base/foo", "$base/foo"], + ]; } - /** - * @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(array('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/NativeSessionHandlerTest.php b/Tests/Session/Storage/Handler/NativeSessionHandlerTest.php index 5486b2d65..4a9fb600d 100644 --- a/Tests/Session/Storage/Handler/NativeSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/NativeSessionHandlerTest.php @@ -21,14 +21,18 @@ * * @runTestsInSeparateProcesses * @preserveGlobalState disabled + * @group legacy */ class NativeSessionHandlerTest extends TestCase { + /** + * @expectedDeprecation The Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeSessionHandler class is deprecated since Symfony 3.4 and will be removed in 4.0. Use the \SessionHandler class instead. + */ public function testConstruct() { $handler = new NativeSessionHandler(); - $this->assertTrue($handler instanceof \SessionHandler); + $this->assertInstanceOf('SessionHandler', $handler); $this->assertTrue($handler instanceof NativeSessionHandler); } } diff --git a/Tests/Session/Storage/Handler/NullSessionHandlerTest.php b/Tests/Session/Storage/Handler/NullSessionHandlerTest.php index 718fd0f83..f793db144 100644 --- a/Tests/Session/Storage/Handler/NullSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/NullSessionHandlerTest.php @@ -12,9 +12,9 @@ namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler; use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\Storage\Handler\NullSessionHandler; use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; -use Symfony\Component\HttpFoundation\Session\Session; /** * Test class for NullSessionHandler. @@ -28,7 +28,7 @@ class NullSessionHandlerTest extends TestCase { public function testSaveHandlers() { - $storage = $this->getStorage(); + $this->getStorage(); $this->assertEquals('user', ini_get('session.save_handler')); } @@ -54,6 +54,6 @@ public function testNothingIsPersisted() public function getStorage() { - return new NativeSessionStorage(array(), new NullSessionHandler()); + return new NativeSessionStorage([], new NullSessionHandler()); } } diff --git a/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php b/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php index a47120f18..e710dca92 100644 --- a/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php @@ -48,34 +48,28 @@ 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() { - $storage = new PdoSessionHandler($this->getMemorySqlitePdo(), array('db_table' => 'inexistent_table')); + $this->expectException('RuntimeException'); + $storage = new PdoSessionHandler($this->getMemorySqlitePdo(), ['db_table' => 'inexistent_table']); $storage->open('', 'sid'); $storage->read('id'); $storage->write('id', 'data'); $storage->close(); } - /** - * @expectedException \RuntimeException - */ public function testCreateTableTwice() { + $this->expectException('RuntimeException'); $storage = new PdoSessionHandler($this->getMemorySqlitePdo()); $storage->createTable(); } @@ -136,7 +130,7 @@ public function testReadWriteReadWithNullByte() public function testReadConvertsStreamToString() { - if (defined('HHVM_VERSION')) { + if (\defined('HHVM_VERSION')) { $this->markTestSkipped('PHPUnit_MockObject cannot mock the PDOStatement class on HHVM. See https://github.com/sebastianbergmann/phpunit-mock-objects/pull/289'); } @@ -147,7 +141,7 @@ public function testReadConvertsStreamToString() $stream = $this->createStream($content); $pdo->prepareResult->expects($this->once())->method('fetchAll') - ->will($this->returnValue(array(array($stream, 42, time())))); + ->willReturn([[$stream, 42, time()]]); $storage = new PdoSessionHandler($pdo); $result = $storage->read('foo'); @@ -157,9 +151,12 @@ public function testReadConvertsStreamToString() public function testReadLockedConvertsStreamToString() { - if (defined('HHVM_VERSION')) { + if (\defined('HHVM_VERSION')) { $this->markTestSkipped('PHPUnit_MockObject cannot mock the PDOStatement class on HHVM. See https://github.com/sebastianbergmann/phpunit-mock-objects/pull/289'); } + if (filter_var(ini_get('session.use_strict_mode'), FILTER_VALIDATE_BOOLEAN)) { + $this->markTestSkipped('Strict mode needs no locking for new sessions.'); + } $pdo = new MockPdo('pgsql'); $selectStmt = $this->getMockBuilder('PDOStatement')->getMock(); @@ -174,14 +171,14 @@ public function testReadLockedConvertsStreamToString() $exception = null; $selectStmt->expects($this->atLeast(2))->method('fetchAll') - ->will($this->returnCallback(function () use (&$exception, $stream) { - return $exception ? array(array($stream, 42, time())) : array(); - })); + ->willReturnCallback(function () use (&$exception, $stream) { + return $exception ? [[$stream, 42, time()]] : []; + }); $insertStmt->expects($this->once())->method('execute') - ->will($this->returnCallback(function () use (&$exception) { + ->willReturnCallback(function () use (&$exception) { throw $exception = new \PDOException('', '23'); - })); + }); $storage = new PdoSessionHandler($pdo); $result = $storage->read('foo'); @@ -269,6 +266,9 @@ public function testSessionDestroy() $this->assertSame('', $data, 'Destroyed session returns empty string'); } + /** + * @runInSeparateProcess + */ public function testSessionGC() { $previousLifeTime = ini_set('session.gc_maxlifetime', 1000); @@ -318,6 +318,41 @@ public function testGetConnectionConnectsIfNeeded() $this->assertInstanceOf('\PDO', $method->invoke($storage)); } + /** + * @dataProvider provideUrlDsnPairs + */ + public function testUrlDsn($url, $expectedDsn, $expectedUser = null, $expectedPassword = null) + { + $storage = new PdoSessionHandler($url); + $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)); + } + } + + public function provideUrlDsnPairs() + { + yield ['mysql://localhost/test', 'mysql:host=localhost;dbname=test;']; + yield ['mysql://localhost:56/test', 'mysql:host=localhost;port=56;dbname=test;']; + yield ['mysql2://root:pwd@localhost/test', 'mysql:host=localhost;dbname=test;', 'root', 'pwd']; + yield ['postgres://localhost/test', 'pgsql:host=localhost;dbname=test;']; + yield ['postgresql://localhost:5634/test', 'pgsql:host=localhost;port=5634;dbname=test;']; + yield ['postgres://root:pwd@localhost/test', 'pgsql:host=localhost;dbname=test;', 'root', 'pwd']; + yield 'sqlite relative path' => ['sqlite://localhost/tmp/test', 'sqlite:tmp/test']; + yield 'sqlite absolute path' => ['sqlite://localhost//tmp/test', 'sqlite:/tmp/test']; + yield 'sqlite relative path without host' => ['sqlite:///tmp/test', 'sqlite:tmp/test']; + yield 'sqlite absolute path without host' => ['sqlite3:////tmp/test', 'sqlite:/tmp/test']; + yield ['sqlite://localhost/:memory:', 'sqlite::memory:']; + yield ['mssql://localhost/test', 'sqlsrv:server=localhost;Database=test']; + yield ['mssql://localhost:56/test', 'sqlsrv:server=localhost,56;Database=test']; + } + private function createStream($content) { $stream = tmpfile(); @@ -353,10 +388,10 @@ public function getAttribute($attribute) return parent::getAttribute($attribute); } - public function prepare($statement, $driverOptions = array()) + public function prepare($statement, $driverOptions = []) { - return is_callable($this->prepareResult) - ? call_user_func($this->prepareResult, $statement, $driverOptions) + return \is_callable($this->prepareResult) + ? \call_user_func($this->prepareResult, $statement, $driverOptions) : $this->prepareResult; } diff --git a/Tests/Session/Storage/Handler/StrictSessionHandlerTest.php b/Tests/Session/Storage/Handler/StrictSessionHandlerTest.php new file mode 100644 index 000000000..6a0d16876 --- /dev/null +++ b/Tests/Session/Storage/Handler/StrictSessionHandlerTest.php @@ -0,0 +1,189 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\AbstractSessionHandler; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler; + +class StrictSessionHandlerTest extends TestCase +{ + public function testOpen() + { + $handler = $this->getMockBuilder('SessionHandlerInterface')->getMock(); + $handler->expects($this->once())->method('open') + ->with('path', 'name')->willReturn(true); + $proxy = new StrictSessionHandler($handler); + + $this->assertInstanceOf('SessionUpdateTimestampHandlerInterface', $proxy); + $this->assertInstanceOf(AbstractSessionHandler::class, $proxy); + $this->assertTrue($proxy->open('path', 'name')); + } + + public function testCloseSession() + { + $handler = $this->getMockBuilder('SessionHandlerInterface')->getMock(); + $handler->expects($this->once())->method('close') + ->willReturn(true); + $proxy = new StrictSessionHandler($handler); + + $this->assertTrue($proxy->close()); + } + + public function testValidateIdOK() + { + $handler = $this->getMockBuilder('SessionHandlerInterface')->getMock(); + $handler->expects($this->once())->method('read') + ->with('id')->willReturn('data'); + $proxy = new StrictSessionHandler($handler); + + $this->assertTrue($proxy->validateId('id')); + } + + public function testValidateIdKO() + { + $handler = $this->getMockBuilder('SessionHandlerInterface')->getMock(); + $handler->expects($this->once())->method('read') + ->with('id')->willReturn(''); + $proxy = new StrictSessionHandler($handler); + + $this->assertFalse($proxy->validateId('id')); + } + + public function testRead() + { + $handler = $this->getMockBuilder('SessionHandlerInterface')->getMock(); + $handler->expects($this->once())->method('read') + ->with('id')->willReturn('data'); + $proxy = new StrictSessionHandler($handler); + + $this->assertSame('data', $proxy->read('id')); + } + + public function testReadWithValidateIdOK() + { + $handler = $this->getMockBuilder('SessionHandlerInterface')->getMock(); + $handler->expects($this->once())->method('read') + ->with('id')->willReturn('data'); + $proxy = new StrictSessionHandler($handler); + + $this->assertTrue($proxy->validateId('id')); + $this->assertSame('data', $proxy->read('id')); + } + + public function testReadWithValidateIdMismatch() + { + $handler = $this->getMockBuilder('SessionHandlerInterface')->getMock(); + $handler->expects($this->exactly(2))->method('read') + ->withConsecutive(['id1'], ['id2']) + ->will($this->onConsecutiveCalls('data1', 'data2')); + $proxy = new StrictSessionHandler($handler); + + $this->assertTrue($proxy->validateId('id1')); + $this->assertSame('data2', $proxy->read('id2')); + } + + public function testUpdateTimestamp() + { + $handler = $this->getMockBuilder('SessionHandlerInterface')->getMock(); + $handler->expects($this->once())->method('write') + ->with('id', 'data')->willReturn(true); + $proxy = new StrictSessionHandler($handler); + + $this->assertTrue($proxy->updateTimestamp('id', 'data')); + } + + public function testWrite() + { + $handler = $this->getMockBuilder('SessionHandlerInterface')->getMock(); + $handler->expects($this->once())->method('write') + ->with('id', 'data')->willReturn(true); + $proxy = new StrictSessionHandler($handler); + + $this->assertTrue($proxy->write('id', 'data')); + } + + public function testWriteEmptyNewSession() + { + $handler = $this->getMockBuilder('SessionHandlerInterface')->getMock(); + $handler->expects($this->once())->method('read') + ->with('id')->willReturn(''); + $handler->expects($this->never())->method('write'); + $handler->expects($this->once())->method('destroy')->willReturn(true); + $proxy = new StrictSessionHandler($handler); + + $this->assertFalse($proxy->validateId('id')); + $this->assertSame('', $proxy->read('id')); + $this->assertTrue($proxy->write('id', '')); + } + + public function testWriteEmptyExistingSession() + { + $handler = $this->getMockBuilder('SessionHandlerInterface')->getMock(); + $handler->expects($this->once())->method('read') + ->with('id')->willReturn('data'); + $handler->expects($this->never())->method('write'); + $handler->expects($this->once())->method('destroy')->willReturn(true); + $proxy = new StrictSessionHandler($handler); + + $this->assertSame('data', $proxy->read('id')); + $this->assertTrue($proxy->write('id', '')); + } + + public function testDestroy() + { + $handler = $this->getMockBuilder('SessionHandlerInterface')->getMock(); + $handler->expects($this->once())->method('destroy') + ->with('id')->willReturn(true); + $proxy = new StrictSessionHandler($handler); + + $this->assertTrue($proxy->destroy('id')); + } + + public function testDestroyNewSession() + { + $handler = $this->getMockBuilder('SessionHandlerInterface')->getMock(); + $handler->expects($this->once())->method('read') + ->with('id')->willReturn(''); + $handler->expects($this->once())->method('destroy')->willReturn(true); + $proxy = new StrictSessionHandler($handler); + + $this->assertSame('', $proxy->read('id')); + $this->assertTrue($proxy->destroy('id')); + } + + public function testDestroyNonEmptyNewSession() + { + $handler = $this->getMockBuilder('SessionHandlerInterface')->getMock(); + $handler->expects($this->once())->method('read') + ->with('id')->willReturn(''); + $handler->expects($this->once())->method('write') + ->with('id', 'data')->willReturn(true); + $handler->expects($this->once())->method('destroy') + ->with('id')->willReturn(true); + $proxy = new StrictSessionHandler($handler); + + $this->assertSame('', $proxy->read('id')); + $this->assertTrue($proxy->write('id', 'data')); + $this->assertTrue($proxy->destroy('id')); + } + + public function testGc() + { + $handler = $this->getMockBuilder('SessionHandlerInterface')->getMock(); + $handler->expects($this->once())->method('gc') + ->with(123)->willReturn(true); + $proxy = new StrictSessionHandler($handler); + + $this->assertTrue($proxy->gc(123)); + } +} diff --git a/Tests/Session/Storage/Handler/WriteCheckSessionHandlerTest.php b/Tests/Session/Storage/Handler/WriteCheckSessionHandlerTest.php index 5e41a4743..b89454f87 100644 --- a/Tests/Session/Storage/Handler/WriteCheckSessionHandlerTest.php +++ b/Tests/Session/Storage/Handler/WriteCheckSessionHandlerTest.php @@ -16,6 +16,8 @@ /** * @author Adrien Brault + * + * @group legacy */ class WriteCheckSessionHandlerTest extends TestCase { @@ -28,7 +30,7 @@ public function test() ->expects($this->once()) ->method('close') ->with() - ->will($this->returnValue(true)) + ->willReturn(true) ; $this->assertTrue($writeCheckSessionHandler->close()); @@ -43,7 +45,7 @@ public function testWrite() ->expects($this->once()) ->method('write') ->with('foo', 'bar') - ->will($this->returnValue(true)) + ->willReturn(true) ; $this->assertTrue($writeCheckSessionHandler->write('foo', 'bar')); @@ -58,7 +60,7 @@ public function testSkippedWrite() ->expects($this->once()) ->method('read') ->with('foo') - ->will($this->returnValue('bar')) + ->willReturn('bar') ; $wrappedSessionHandlerMock @@ -79,14 +81,14 @@ public function testNonSkippedWrite() ->expects($this->once()) ->method('read') ->with('foo') - ->will($this->returnValue('bar')) + ->willReturn('bar') ; $wrappedSessionHandlerMock ->expects($this->once()) ->method('write') ->with('foo', 'baZZZ') - ->will($this->returnValue(true)) + ->willReturn(true) ; $this->assertEquals('bar', $writeCheckSessionHandler->read('foo')); diff --git a/Tests/Session/Storage/MetadataBagTest.php b/Tests/Session/Storage/MetadataBagTest.php index 159e62114..2c4758b91 100644 --- a/Tests/Session/Storage/MetadataBagTest.php +++ b/Tests/Session/Storage/MetadataBagTest.php @@ -26,29 +26,26 @@ class MetadataBagTest extends TestCase */ protected $bag; - /** - * @var array - */ - protected $array = array(); + protected $array = []; protected function setUp() { parent::setUp(); $this->bag = new MetadataBag(); - $this->array = array(MetadataBag::CREATED => 1234567, MetadataBag::UPDATED => 12345678, MetadataBag::LIFETIME => 0); + $this->array = [MetadataBag::CREATED => 1234567, MetadataBag::UPDATED => 12345678, MetadataBag::LIFETIME => 0]; $this->bag->initialize($this->array); } protected function tearDown() { - $this->array = array(); + $this->array = []; $this->bag = null; parent::tearDown(); } public function testInitialize() { - $sessionMetadata = array(); + $sessionMetadata = []; $bag1 = new MetadataBag(); $bag1->initialize($sessionMetadata); @@ -85,7 +82,7 @@ public function testGetStorageKey() public function testGetLifetime() { $bag = new MetadataBag(); - $array = array(MetadataBag::CREATED => 1234567, MetadataBag::UPDATED => 12345678, MetadataBag::LIFETIME => 1000); + $array = [MetadataBag::CREATED => 1234567, MetadataBag::UPDATED => 12345678, MetadataBag::LIFETIME => 1000]; $bag->initialize($array); $this->assertEquals(1000, $bag->getLifetime()); } @@ -114,11 +111,11 @@ public function testSkipLastUsedUpdate() $timeStamp = time(); $created = $timeStamp - 15; - $sessionMetadata = array( + $sessionMetadata = [ MetadataBag::CREATED => $created, MetadataBag::UPDATED => $created, MetadataBag::LIFETIME => 1000, - ); + ]; $bag->initialize($sessionMetadata); $this->assertEquals($created, $sessionMetadata[MetadataBag::UPDATED]); @@ -130,11 +127,11 @@ public function testDoesNotSkipLastUsedUpdate() $timeStamp = time(); $created = $timeStamp - 45; - $sessionMetadata = array( + $sessionMetadata = [ MetadataBag::CREATED => $created, MetadataBag::UPDATED => $created, MetadataBag::LIFETIME => 1000, - ); + ]; $bag->initialize($sessionMetadata); $this->assertEquals($timeStamp, $sessionMetadata[MetadataBag::UPDATED]); diff --git a/Tests/Session/Storage/MockArraySessionStorageTest.php b/Tests/Session/Storage/MockArraySessionStorageTest.php index 82df5543e..7e0d303b9 100644 --- a/Tests/Session/Storage/MockArraySessionStorageTest.php +++ b/Tests/Session/Storage/MockArraySessionStorageTest.php @@ -12,9 +12,9 @@ namespace Symfony\Component\HttpFoundation\Tests\Session\Storage; use PHPUnit\Framework\TestCase; -use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag; use Symfony\Component\HttpFoundation\Session\Flash\FlashBag; +use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; /** * Test class for MockArraySessionStorage. @@ -45,10 +45,10 @@ protected function setUp() $this->attributes = new AttributeBag(); $this->flashes = new FlashBag(); - $this->data = array( - $this->attributes->getStorageKey() => array('foo' => 'bar'), - $this->flashes->getStorageKey() => array('notice' => 'hello'), - ); + $this->data = [ + $this->attributes->getStorageKey() => ['foo' => 'bar'], + $this->flashes->getStorageKey() => ['notice' => 'hello'], + ]; $this->storage = new MockArraySessionStorage(); $this->storage->registerBag($this->flashes); @@ -80,14 +80,14 @@ public function testRegenerate() $id = $this->storage->getId(); $this->storage->regenerate(); $this->assertNotEquals($id, $this->storage->getId()); - $this->assertEquals(array('foo' => 'bar'), $this->storage->getBag('attributes')->all()); - $this->assertEquals(array('notice' => 'hello'), $this->storage->getBag('flashes')->peekAll()); + $this->assertEquals(['foo' => 'bar'], $this->storage->getBag('attributes')->all()); + $this->assertEquals(['notice' => 'hello'], $this->storage->getBag('flashes')->peekAll()); $id = $this->storage->getId(); $this->storage->regenerate(true); $this->assertNotEquals($id, $this->storage->getId()); - $this->assertEquals(array('foo' => 'bar'), $this->storage->getBag('attributes')->all()); - $this->assertEquals(array('notice' => 'hello'), $this->storage->getBag('flashes')->peekAll()); + $this->assertEquals(['foo' => 'bar'], $this->storage->getBag('attributes')->all()); + $this->assertEquals(['notice' => 'hello'], $this->storage->getBag('flashes')->peekAll()); } public function testGetId() @@ -101,8 +101,8 @@ public function testClearClearsBags() { $this->storage->clear(); - $this->assertSame(array(), $this->storage->getBag('attributes')->all()); - $this->assertSame(array(), $this->storage->getBag('flashes')->peekAll()); + $this->assertSame([], $this->storage->getBag('attributes')->all()); + $this->assertSame([], $this->storage->getBag('flashes')->peekAll()); } public function testClearStartsSession() @@ -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 53accd384..d6bd1823f 100644 --- a/Tests/Session/Storage/MockFileSessionStorageTest.php +++ b/Tests/Session/Storage/MockFileSessionStorageTest.php @@ -12,9 +12,9 @@ namespace Symfony\Component\HttpFoundation\Tests\Session\Storage; use PHPUnit\Framework\TestCase; -use Symfony\Component\HttpFoundation\Session\Storage\MockFileSessionStorage; -use Symfony\Component\HttpFoundation\Session\Flash\FlashBag; use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag; +use Symfony\Component\HttpFoundation\Session\Flash\FlashBag; +use Symfony\Component\HttpFoundation\Session\Storage\MockFileSessionStorage; /** * Test class for MockFileSessionStorage. @@ -41,12 +41,12 @@ protected function setUp() protected function tearDown() { - $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() @@ -91,7 +91,7 @@ public function testSave() $storage->start(); $this->assertEquals('108', $storage->getBag('attributes')->get('new')); $this->assertTrue($storage->getBag('flashes')->has('newkey')); - $this->assertEquals(array('test'), $storage->getBag('flashes')->peek('newkey')); + $this->assertEquals(['test'], $storage->getBag('flashes')->peek('newkey')); } public function testMultipleInstances() @@ -107,11 +107,9 @@ 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(); } diff --git a/Tests/Session/Storage/NativeSessionStorageTest.php b/Tests/Session/Storage/NativeSessionStorageTest.php index 818c63a9d..9ce8108da 100644 --- a/Tests/Session/Storage/NativeSessionStorageTest.php +++ b/Tests/Session/Storage/NativeSessionStorageTest.php @@ -14,7 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag; use Symfony\Component\HttpFoundation\Session\Flash\FlashBag; -use Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeSessionHandler; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeFileSessionHandler; use Symfony\Component\HttpFoundation\Session\Storage\Handler\NullSessionHandler; use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy; @@ -54,11 +54,9 @@ protected function tearDown() } /** - * @param array $options - * * @return NativeSessionStorage */ - protected function getStorage(array $options = array()) + protected function getStorage(array $options = []) { $storage = new NativeSessionStorage($options); $storage->registerBag(new AttributeBag()); @@ -74,20 +72,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()); @@ -100,7 +94,7 @@ public function testGetId() $storage->start(); $id = $storage->getId(); - $this->assertInternalType('string', $id); + $this->assertIsString($id); $this->assertNotSame('', $id); $storage->save(); @@ -151,7 +145,7 @@ public function testDefaultSessionCacheLimiter() { $this->iniSet('session.cache_limiter', 'nocache'); - $storage = new NativeSessionStorage(); + new NativeSessionStorage(); $this->assertEquals('', ini_get('session.cache_limiter')); } @@ -159,36 +153,53 @@ public function testExplicitSessionCacheLimiter() { $this->iniSet('session.cache_limiter', 'nocache'); - $storage = new NativeSessionStorage(array('cache_limiter' => 'public')); + new NativeSessionStorage(['cache_limiter' => 'public']); $this->assertEquals('public', ini_get('session.cache_limiter')); } public function testCookieOptions() { - $options = array( + $options = [ 'cookie_lifetime' => 123456, 'cookie_path' => '/my/cookie/path', 'cookie_domain' => 'symfony.example.com', 'cookie_secure' => true, 'cookie_httponly' => false, - ); + ]; $this->getStorage($options); $temp = session_get_cookie_params(); - $gco = array(); + $gco = []; foreach ($temp as $key => $value) { $gco['cookie_'.$key] = $value; } + unset($gco['cookie_samesite']); + $this->assertEquals($options, $gco); } - /** - * @expectedException \InvalidArgumentException - */ + public function testSessionOptions() + { + if (\defined('HHVM_VERSION')) { + $this->markTestSkipped('HHVM is not handled in this test case.'); + } + + $options = [ + 'url_rewriter.tags' => 'a=href', + 'cache_expire' => '200', + ]; + + $this->getStorage($options); + + $this->assertSame('a=href', ini_get('url_rewriter.tags')); + $this->assertSame('200', ini_get('session.cache_expire')); + } + public function testSetSaveHandlerException() { + $this->expectException('InvalidArgumentException'); $storage = $this->getStorage(); $storage->setSaveHandler(new \stdClass()); } @@ -201,9 +212,9 @@ public function testSetSaveHandler() $this->assertInstanceOf('Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy', $storage->getSaveHandler()); $storage->setSaveHandler(null); $this->assertInstanceOf('Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy', $storage->getSaveHandler()); - $storage->setSaveHandler(new SessionHandlerProxy(new NativeSessionHandler())); + $storage->setSaveHandler(new SessionHandlerProxy(new NativeFileSessionHandler())); $this->assertInstanceOf('Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy', $storage->getSaveHandler()); - $storage->setSaveHandler(new NativeSessionHandler()); + $storage->setSaveHandler(new NativeFileSessionHandler()); $this->assertInstanceOf('Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy', $storage->getSaveHandler()); $storage->setSaveHandler(new SessionHandlerProxy(new NullSessionHandler())); $this->assertInstanceOf('Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy', $storage->getSaveHandler()); @@ -211,11 +222,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()); @@ -229,7 +238,7 @@ public function testStarted() $this->assertFalse($storage->isStarted()); $key = $storage->getMetadataBag()->getStorageKey(); - $this->assertFalse(isset($_SESSION[$key])); + $this->assertArrayNotHasKey($key, $_SESSION); $storage->start(); } @@ -244,4 +253,36 @@ public function testRestart() $this->assertSame($id, $storage->getId(), 'Same session ID after restarting'); $this->assertSame(7, $storage->getBag('attributes')->get('lucky'), 'Data still available'); } + + public function testCanCreateNativeSessionStorageWhenSessionAlreadyStarted() + { + session_start(); + $this->getStorage(); + + // Assert no exception has been thrown by `getStorage()` + $this->addToAssertionCount(1); + } + + public function testSetSessionOptionsOnceSessionStartedIsIgnored() + { + session_start(); + $this->getStorage([ + 'name' => 'something-else', + ]); + + // Assert no exception has been thrown by `getStorage()` + $this->addToAssertionCount(1); + } + + public function testGetBagsOnceSessionStartedIsIgnored() + { + session_start(); + $bag = new AttributeBag(); + $bag->setName('flashes'); + + $storage = $this->getStorage(); + $storage->registerBag($bag); + + $this->assertEquals($storage->getBag('flashes'), $bag); + } } diff --git a/Tests/Session/Storage/PhpBridgeSessionStorageTest.php b/Tests/Session/Storage/PhpBridgeSessionStorageTest.php index b8b98386c..752be618b 100644 --- a/Tests/Session/Storage/PhpBridgeSessionStorageTest.php +++ b/Tests/Session/Storage/PhpBridgeSessionStorageTest.php @@ -12,8 +12,8 @@ namespace Symfony\Component\HttpFoundation\Tests\Session\Storage; use PHPUnit\Framework\TestCase; -use Symfony\Component\HttpFoundation\Session\Storage\PhpBridgeSessionStorage; use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag; +use Symfony\Component\HttpFoundation\Session\Storage\PhpBridgeSessionStorage; /** * Test class for PhpSessionStorage. @@ -75,9 +75,9 @@ public function testPhpSession() $this->assertFalse($storage->isStarted()); $key = $storage->getMetadataBag()->getStorageKey(); - $this->assertFalse(isset($_SESSION[$key])); + $this->assertArrayNotHasKey($key, $_SESSION); $storage->start(); - $this->assertTrue(isset($_SESSION[$key])); + $this->assertArrayHasKey($key, $_SESSION); } public function testClear() @@ -87,10 +87,10 @@ public function testClear() $_SESSION['drak'] = 'loves symfony'; $storage->getBag('attributes')->set('symfony', 'greatness'); $key = $storage->getBag('attributes')->getStorageKey(); - $this->assertEquals($_SESSION[$key], array('symfony' => 'greatness')); + $this->assertEquals($_SESSION[$key], ['symfony' => 'greatness']); $this->assertEquals($_SESSION['drak'], 'loves symfony'); $storage->clear(); - $this->assertEquals($_SESSION[$key], array()); + $this->assertEquals($_SESSION[$key], []); $this->assertEquals($_SESSION['drak'], 'loves symfony'); } } diff --git a/Tests/Session/Storage/Proxy/AbstractProxyTest.php b/Tests/Session/Storage/Proxy/AbstractProxyTest.php index ef1da130a..ae40f2c29 100644 --- a/Tests/Session/Storage/Proxy/AbstractProxyTest.php +++ b/Tests/Session/Storage/Proxy/AbstractProxyTest.php @@ -13,39 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy; - -// Note until PHPUnit_Mock_Objects 1.2 is released you cannot mock abstracts due to -// https://github.com/sebastianbergmann/phpunit-mock-objects/issues/73 -class ConcreteProxy extends AbstractProxy -{ -} - -class ConcreteSessionHandlerInterfaceProxy extends AbstractProxy implements \SessionHandlerInterface -{ - public function open($savePath, $sessionName) - { - } - - public function close() - { - } - - public function read($id) - { - } - - public function write($id, $data) - { - } - - public function destroy($id) - { - } - - public function gc($maxlifetime) - { - } -} +use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy; /** * Test class for AbstractProxy. @@ -61,7 +29,7 @@ class AbstractProxyTest extends TestCase protected function setUp() { - $this->proxy = new ConcreteProxy(); + $this->proxy = $this->getMockForAbstractClass(AbstractProxy::class); } protected function tearDown() @@ -77,7 +45,7 @@ public function testGetSaveHandlerName() public function testIsSessionHandlerInterface() { $this->assertFalse($this->proxy->isSessionHandlerInterface()); - $sh = new ConcreteSessionHandlerInterfaceProxy(); + $sh = new SessionHandlerProxy(new \SessionHandler()); $this->assertTrue($sh->isSessionHandlerInterface()); } @@ -112,10 +80,10 @@ public function testName() /** * @runInSeparateProcess * @preserveGlobalState disabled - * @expectedException \LogicException */ public function testNameException() { + $this->expectException('LogicException'); session_start(); $this->proxy->setName('foo'); } @@ -135,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/NativeProxyTest.php b/Tests/Session/Storage/Proxy/NativeProxyTest.php index 8ec305344..ed4fee6bf 100644 --- a/Tests/Session/Storage/Proxy/NativeProxyTest.php +++ b/Tests/Session/Storage/Proxy/NativeProxyTest.php @@ -17,6 +17,8 @@ /** * Test class for NativeProxy. * + * @group legacy + * * @author Drak */ class NativeProxyTest extends TestCase diff --git a/Tests/Session/Storage/Proxy/SessionHandlerProxyTest.php b/Tests/Session/Storage/Proxy/SessionHandlerProxyTest.php index 682825356..1457ebd70 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; @@ -50,7 +50,7 @@ public function testOpenTrue() { $this->mock->expects($this->once()) ->method('open') - ->will($this->returnValue(true)); + ->willReturn(true); $this->assertFalse($this->proxy->isActive()); $this->proxy->open('name', 'id'); @@ -61,7 +61,7 @@ public function testOpenFalse() { $this->mock->expects($this->once()) ->method('open') - ->will($this->returnValue(false)); + ->willReturn(false); $this->assertFalse($this->proxy->isActive()); $this->proxy->open('name', 'id'); @@ -72,7 +72,7 @@ public function testClose() { $this->mock->expects($this->once()) ->method('close') - ->will($this->returnValue(true)); + ->willReturn(true); $this->assertFalse($this->proxy->isActive()); $this->proxy->close(); @@ -83,7 +83,7 @@ public function testCloseFalse() { $this->mock->expects($this->once()) ->method('close') - ->will($this->returnValue(false)); + ->willReturn(false); $this->assertFalse($this->proxy->isActive()); $this->proxy->close(); @@ -121,4 +121,38 @@ public function testGc() $this->proxy->gc(86400); } + + /** + * @requires PHPUnit 5.1 + */ + public function testValidateId() + { + $mock = $this->getMockBuilder(['SessionHandlerInterface', 'SessionUpdateTimestampHandlerInterface'])->getMock(); + $mock->expects($this->once()) + ->method('validateId'); + + $proxy = new SessionHandlerProxy($mock); + $proxy->validateId('id'); + + $this->assertTrue($this->proxy->validateId('id')); + } + + /** + * @requires PHPUnit 5.1 + */ + public function testUpdateTimestamp() + { + $mock = $this->getMockBuilder(['SessionHandlerInterface', 'SessionUpdateTimestampHandlerInterface'])->getMock(); + $mock->expects($this->once()) + ->method('updateTimestamp') + ->willReturn(false); + + $proxy = new SessionHandlerProxy($mock); + $proxy->updateTimestamp('id', 'data'); + + $this->mock->expects($this->once()) + ->method('write'); + + $this->proxy->updateTimestamp('id', 'data'); + } } diff --git a/Tests/StreamedResponseTest.php b/Tests/StreamedResponseTest.php index 1e35eb88b..a084e917d 100644 --- a/Tests/StreamedResponseTest.php +++ b/Tests/StreamedResponseTest.php @@ -19,7 +19,7 @@ class StreamedResponseTest extends TestCase { public function testConstructor() { - $response = new StreamedResponse(function () { echo 'foo'; }, 404, array('Content-Type' => 'text/plain')); + $response = new StreamedResponse(function () { echo 'foo'; }, 404, ['Content-Type' => 'text/plain']); $this->assertEquals(404, $response->getStatusCode()); $this->assertEquals('text/plain', $response->headers->get('Content-Type')); @@ -51,7 +51,7 @@ public function testPrepareWith10Protocol() public function testPrepareWithHeadRequest() { - $response = new StreamedResponse(function () { echo 'foo'; }, 200, array('Content-Length' => '123')); + $response = new StreamedResponse(function () { echo 'foo'; }, 200, ['Content-Length' => '123']); $request = Request::create('/', 'HEAD'); $response->prepare($request); @@ -61,7 +61,7 @@ public function testPrepareWithHeadRequest() public function testPrepareWithCacheHeaders() { - $response = new StreamedResponse(function () { echo 'foo'; }, 200, array('Cache-Control' => 'max-age=600, public')); + $response = new StreamedResponse(function () { echo 'foo'; }, 200, ['Cache-Control' => 'max-age=600, public']); $request = Request::create('/', 'GET'); $response->prepare($request); @@ -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'); } @@ -112,4 +108,33 @@ public function testCreate() $this->assertInstanceOf('Symfony\Component\HttpFoundation\StreamedResponse', $response); $this->assertEquals(204, $response->getStatusCode()); } + + public function testReturnThis() + { + $response = new StreamedResponse(function () {}); + $this->assertInstanceOf('Symfony\Component\HttpFoundation\StreamedResponse', $response->sendContent()); + $this->assertInstanceOf('Symfony\Component\HttpFoundation\StreamedResponse', $response->sendContent()); + + $response = new StreamedResponse(function () {}); + $this->assertInstanceOf('Symfony\Component\HttpFoundation\StreamedResponse', $response->sendHeaders()); + $this->assertInstanceOf('Symfony\Component\HttpFoundation\StreamedResponse', $response->sendHeaders()); + } + + public function testSetNotModified() + { + $response = new StreamedResponse(function () { echo 'foo'; }); + $modified = $response->setNotModified(); + $this->assertObjectHasAttribute('headers', $modified); + $this->assertObjectHasAttribute('content', $modified); + $this->assertObjectHasAttribute('version', $modified); + $this->assertObjectHasAttribute('statusCode', $modified); + $this->assertObjectHasAttribute('statusText', $modified); + $this->assertObjectHasAttribute('charset', $modified); + $this->assertEquals(304, $modified->getStatusCode()); + + ob_start(); + $modified->sendContent(); + $string = ob_get_clean(); + $this->assertEmpty($string); + } } diff --git a/composer.json b/composer.json index a964975ec..f6c6f2e62 100644 --- a/composer.json +++ b/composer.json @@ -17,10 +17,11 @@ ], "require": { "php": "^5.5.9|>=7.0.8", - "symfony/polyfill-mbstring": "~1.1" + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php70": "~1.6" }, "require-dev": { - "symfony/expression-language": "~2.8|~3.0" + "symfony/expression-language": "~2.8|~3.0|~4.0" }, "autoload": { "psr-4": { "Symfony\\Component\\HttpFoundation\\": "" }, @@ -31,7 +32,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "3.3-dev" + "dev-master": "3.4-dev" } } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index c1d61f8bf..f57bc9e62 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,7 +1,7 @@