Commit b9cf8dfb authored by Taylor Otwell's avatar Taylor Otwell

Upgrade to latest Symfony HttpFoundation tag. Closes #1865.

parent 92f602ed
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation;
/**
* Represents an Accept-* header.
*
* An accept header is compound with a list of items,
* sorted by descending quality.
*
* @author Jean-François Simon <contact@jfsimon.fr>
*/
class AcceptHeader
{
/**
* @var AcceptHeaderItem[]
*/
private $items = array();
/**
* @var bool
*/
private $sorted = true;
/**
* Constructor.
*
* @param AcceptHeaderItem[] $items
*/
public function __construct(array $items)
{
foreach ($items as $item) {
$this->add($item);
}
}
/**
* Builds an AcceptHeader instance from a string.
*
* @param string $headerValue
*
* @return AcceptHeader
*/
public static function fromString($headerValue)
{
$index = 0;
return new self(array_map(function ($itemValue) use (&$index) {
$item = AcceptHeaderItem::fromString($itemValue);
$item->setIndex($index++);
return $item;
}, preg_split('/\s*(?:,*("[^"]+"),*|,*(\'[^\']+\'),*|,+)\s*/', $headerValue, 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE)));
}
/**
* Returns header value's string representation.
*
* @return string
*/
public function __toString()
{
return implode(',', $this->items);
}
/**
* Tests if header has given value.
*
* @param string $value
*
* @return Boolean
*/
public function has($value)
{
return isset($this->items[$value]);
}
/**
* Returns given value's item, if exists.
*
* @param string $value
*
* @return AcceptHeaderItem|null
*/
public function get($value)
{
return isset($this->items[$value]) ? $this->items[$value] : null;
}
/**
* Adds an item.
*
* @param AcceptHeaderItem $item
*
* @return AcceptHeader
*/
public function add(AcceptHeaderItem $item)
{
$this->items[$item->getValue()] = $item;
$this->sorted = false;
return $this;
}
/**
* Returns all items.
*
* @return AcceptHeaderItem[]
*/
public function all()
{
$this->sort();
return $this->items;
}
/**
* Filters items on their value using given regex.
*
* @param string $pattern
*
* @return AcceptHeader
*/
public function filter($pattern)
{
return new self(array_filter($this->items, function (AcceptHeaderItem $item) use ($pattern) {
return preg_match($pattern, $item->getValue());
}));
}
/**
* Returns first item.
*
* @return AcceptHeaderItem|null
*/
public function first()
{
$this->sort();
return !empty($this->items) ? reset($this->items) : null;
}
/**
* Sorts items by descending quality
*/
private function sort()
{
if (!$this->sorted) {
uasort($this->items, function ($a, $b) {
$qA = $a->getQuality();
$qB = $b->getQuality();
if ($qA === $qB) {
return $a->getIndex() > $b->getIndex() ? 1 : -1;
}
return $qA > $qB ? -1 : 1;
});
$this->sorted = true;
}
}
}
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation;
/**
* Represents an Accept-* header item.
*
* @author Jean-François Simon <contact@jfsimon.fr>
*/
class AcceptHeaderItem
{
/**
* @var string
*/
private $value;
/**
* @var float
*/
private $quality = 1.0;
/**
* @var int
*/
private $index = 0;
/**
* @var array
*/
private $attributes = array();
/**
* Constructor.
*
* @param string $value
* @param array $attributes
*/
public function __construct($value, array $attributes = array())
{
$this->value = $value;
foreach ($attributes as $name => $value) {
$this->setAttribute($name, $value);
}
}
/**
* Builds an AcceptHeaderInstance instance from a string.
*
* @param string $itemValue
*
* @return AcceptHeaderItem
*/
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();
$lastNullAttribute = null;
foreach ($bits as $bit) {
if (($start = substr($bit, 0, 1)) === ($end = substr($bit, -1)) && ($start === '"' || $start === '\'')) {
$attributes[$lastNullAttribute] = substr($bit, 1, -1);
} elseif ('=' === $end) {
$lastNullAttribute = $bit = substr($bit, 0, -1);
$attributes[$bit] = null;
} else {
$parts = explode('=', $bit);
$attributes[$parts[0]] = isset($parts[1]) && strlen($parts[1]) > 0 ? $parts[1] : '';
}
}
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.
*
* @return string
*/
public function __toString()
{
$string = $this->value.($this->quality < 1 ? ';q='.$this->quality : '');
if (count($this->attributes) > 0) {
$string .= ';'.implode(';', array_map(function($name, $value) {
return sprintf(preg_match('/[,;=]/', $value) ? '%s="%s"' : '%s=%s', $name, $value);
}, array_keys($this->attributes), $this->attributes));
}
return $string;
}
/**
* Set the item value.
*
* @param string $value
*
* @return AcceptHeaderItem
*/
public function setValue($value)
{
$this->value = $value;
return $this;
}
/**
* Returns the item value.
*
* @return string
*/
public function getValue()
{
return $this->value;
}
/**
* Set the item quality.
*
* @param float $quality
*
* @return AcceptHeaderItem
*/
public function setQuality($quality)
{
$this->quality = $quality;
return $this;
}
/**
* Returns the item quality.
*
* @return float
*/
public function getQuality()
{
return $this->quality;
}
/**
* Set the item index.
*
* @param int $index
*
* @return AcceptHeaderItem
*/
public function setIndex($index)
{
$this->index = $index;
return $this;
}
/**
* Returns the item index.
*
* @return int
*/
public function getIndex()
{
return $this->index;
}
/**
* Tests if an attribute exists.
*
* @param string $name
*
* @return Boolean
*/
public function hasAttribute($name)
{
return isset($this->attributes[$name]);
}
/**
* Returns an attribute by its name.
*
* @param string $name
* @param mixed $default
*
* @return mixed
*/
public function getAttribute($name, $default = null)
{
return isset($this->attributes[$name]) ? $this->attributes[$name] : $default;
}
/**
* Returns all attributes.
*
* @return array
*/
public function getAttributes()
{
return $this->attributes;
}
/**
* Set an attribute.
*
* @param string $name
* @param string $value
*
* @return AcceptHeaderItem
*/
public function setAttribute($name, $value)
{
if ('q' === $name) {
$this->quality = (float) $value;
} else {
$this->attributes[$name] = (string) $value;
}
return $this;
}
}
<?php
/**
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
/**
* BinaryFileResponse represents an HTTP response delivering a file.
*
* @author Niklas Fiekas <niklas.fiekas@tu-clausthal.de>
* @author stealth35 <stealth35-php@live.fr>
* @author Igor Wiedler <igor@wiedler.ch>
* @author Jordan Alliot <jordan.alliot@gmail.com>
* @author Sergey Linnik <linniksa@gmail.com>
*/
class BinaryFileResponse extends Response
{
protected static $trustXSendfileTypeHeader = false;
protected $file;
protected $offset;
protected $maxlen;
/**
* Constructor.
*
* @param SplFileInfo|string $file The file to stream
* @param integer $status The response status code
* @param array $headers An array of response headers
* @param boolean $public Files are public by default
* @param null|string $contentDisposition The type of Content-Disposition to set automatically with the filename
* @param boolean $autoEtag Whether the ETag header should be automatically set
* @param boolean $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)
{
parent::__construct(null, $status, $headers);
$this->setFile($file, $contentDisposition, $autoEtag, $autoLastModified);
if ($public) {
$this->setPublic();
}
}
/**
* {@inheritdoc}
*/
public static function create($file = null, $status = 200, $headers = array(), $public = true, $contentDisposition = null, $autoEtag = false, $autoLastModified = true)
{
return new static($file, $status, $headers, $public, $contentDisposition, $autoEtag, $autoLastModified);
}
/**
* Sets the file to stream.
*
* @param SplFileInfo|string $file The file to stream
* @param string $contentDisposition
* @param Boolean $autoEtag
* @param Boolean $autoLastModified
*
* @return BinaryFileResponse
*
* @throws FileException
*/
public function setFile($file, $contentDisposition = null, $autoEtag = false, $autoLastModified = true)
{
$file = new File((string) $file);
if (!$file->isReadable()) {
throw new FileException('File must be readable.');
}
$this->file = $file;
if ($autoEtag) {
$this->setAutoEtag();
}
if ($autoLastModified) {
$this->setAutoLastModified();
}
if ($contentDisposition) {
$this->setContentDisposition($contentDisposition);
}
return $this;
}
/**
* Gets the file.
*
* @return File The file to stream
*/
public function getFile()
{
return $this->file;
}
/**
* Automatically sets the Last-Modified header according the file modification date.
*/
public function setAutoLastModified()
{
$this->setLastModified(\DateTime::createFromFormat('U', $this->file->getMTime()));
return $this;
}
/**
* Automatically sets the ETag header according to the checksum of the file.
*/
public function setAutoEtag()
{
$this->setEtag(sha1_file($this->file->getPathname()));
return $this;
}
/**
* Sets the Content-Disposition header with the given filename.
*
* @param string $disposition ResponseHeaderBag::DISPOSITION_INLINE or ResponseHeaderBag::DISPOSITION_ATTACHMENT
* @param string $filename Optionally use this filename instead of the real name of the file
* @param string $filenameFallback A fallback filename, containing only ASCII characters. Defaults to an automatically encoded filename
*
* @return BinaryFileResponse
*/
public function setContentDisposition($disposition, $filename = '', $filenameFallback = '')
{
if ($filename === '') {
$filename = $this->file->getFilename();
}
$dispositionHeader = $this->headers->makeDisposition($disposition, $filename, $filenameFallback);
$this->headers->set('Content-Disposition', $dispositionHeader);
return $this;
}
/**
* {@inheritdoc}
*/
public function prepare(Request $request)
{
$this->headers->set('Content-Length', $this->file->getSize());
$this->headers->set('Accept-Ranges', 'bytes');
$this->headers->set('Content-Transfer-Encoding', 'binary');
if (!$this->headers->has('Content-Type')) {
$this->headers->set('Content-Type', $this->file->getMimeType() ?: 'application/octet-stream');
}
if ('HTTP/1.0' != $request->server->get('SERVER_PROTOCOL')) {
$this->setProtocolVersion('1.1');
}
$this->offset = 0;
$this->maxlen = -1;
if (self::$trustXSendfileTypeHeader && $request->headers->has('X-Sendfile-Type')) {
// Use X-Sendfile, do not send any content.
$type = $request->headers->get('X-Sendfile-Type');
$path = $this->file->getRealPath();
if (strtolower($type) == 'x-accel-redirect') {
// Do X-Accel-Mapping substitutions.
foreach (explode(',', $request->headers->get('X-Accel-Mapping', '')) as $mapping) {
$mapping = explode('=', $mapping, 2);
if (2 == count($mapping)) {
$location = trim($mapping[0]);
$pathPrefix = trim($mapping[1]);
if (substr($path, 0, strlen($pathPrefix)) == $pathPrefix) {
$path = $location . substr($path, strlen($pathPrefix));
break;
}
}
}
}
$this->headers->set($type, $path);
$this->maxlen = 0;
} elseif ($request->headers->has('Range')) {
// Process the range headers.
if (!$request->headers->has('If-Range') || $this->getEtag() == $request->headers->get('If-Range')) {
$range = $request->headers->get('Range');
$fileSize = $this->file->getSize();
list($start, $end) = explode('-', substr($range, 6), 2) + array(0);
$end = ('' === $end) ? $fileSize - 1 : (int) $end;
if ('' === $start) {
$start = $fileSize - $end;
$end = $fileSize - 1;
} else {
$start = (int) $start;
}
$start = max($start, 0);
$end = min($end, $fileSize - 1);
$this->maxlen = $end < $fileSize ? $end - $start + 1 : -1;
$this->offset = $start;
$this->setStatusCode(206);
$this->headers->set('Content-Range', sprintf('bytes %s-%s/%s', $start, $end, $fileSize));
}
}
return $this;
}
/**
* Sends the file.
*/
public function sendContent()
{
if (!$this->isSuccessful()) {
parent::sendContent();
return;
}
if (0 === $this->maxlen) {
return;
}
$out = fopen('php://output', 'wb');
$file = fopen($this->file->getPathname(), 'rb');
stream_copy_to_stream($file, $out, $this->maxlen, $this->offset);
fclose($out);
fclose($file);
}
/**
* {@inheritdoc}
*
* @throws \LogicException when the content is not null
*/
public function setContent($content)
{
if (null !== $content) {
throw new \LogicException('The content cannot be set on a BinaryFileResponse instance.');
}
}
/**
* {@inheritdoc}
*
* @return false
*/
public function getContent()
{
return false;
}
/**
* Trust X-Sendfile-Type header.
*/
public static function trustXSendfileTypeHeader()
{
self::$trustXSendfileTypeHeader = true;
}
}
CHANGELOG CHANGELOG
========= =========
2.2.0
-----
* fixed the Request::create() precedence (URI information always take precedence now)
* added Request::getTrustedProxies()
* deprecated Request::isProxyTrusted()
* [BC BREAK] JsonResponse does not turn a top level empty array to an object anymore, use an ArrayObject to enforce objects
* added a IpUtils class to check if an IP belongs to a CIDR
* added Request::getRealMethod() to get the "real" HTTP method (getMethod() returns the "intended" HTTP method)
* disabled _method request parameter support by default (call Request::enableHttpMethodParameterOverride() to
enable it, and Request::getHttpMethodParameterOverride() to check if it is supported)
* Request::splitHttpAcceptHeader() method is deprecated and will be removed in 2.3
* Deprecated Flashbag::count() and \Countable interface, will be removed in 2.3
2.1.0 2.1.0
----- -----
......
...@@ -39,6 +39,8 @@ class Cookie ...@@ -39,6 +39,8 @@ class Cookie
* @param Boolean $secure Whether the cookie should only be transmitted over a secure HTTPS connection from the client * @param Boolean $secure Whether the cookie should only be transmitted over a secure HTTPS connection from the client
* @param Boolean $httpOnly Whether the cookie will be made accessible only through the HTTP protocol * @param Boolean $httpOnly Whether the cookie will be made accessible only through the HTTP protocol
* *
* @throws \InvalidArgumentException
*
* @api * @api
*/ */
public function __construct($name, $value = null, $expire = 0, $path = '/', $domain = null, $secure = false, $httpOnly = true) public function __construct($name, $value = null, $expire = 0, $path = '/', $domain = null, $secure = false, $httpOnly = true)
......
...@@ -45,7 +45,7 @@ class FileBinaryMimeTypeGuesser implements MimeTypeGuesserInterface ...@@ -45,7 +45,7 @@ class FileBinaryMimeTypeGuesser implements MimeTypeGuesserInterface
*/ */
public static function isSupported() public static function isSupported()
{ {
return !defined('PHP_WINDOWS_VERSION_BUILD'); return !defined('PHP_WINDOWS_VERSION_BUILD') && function_exists('passthru') && function_exists('escapeshellarg');
} }
/** /**
...@@ -77,7 +77,7 @@ class FileBinaryMimeTypeGuesser implements MimeTypeGuesserInterface ...@@ -77,7 +77,7 @@ class FileBinaryMimeTypeGuesser implements MimeTypeGuesserInterface
$type = trim(ob_get_clean()); $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 // it's not a type, but an error message
return null; return null;
} }
......
...@@ -11,7 +11,6 @@ ...@@ -11,7 +11,6 @@
namespace Symfony\Component\HttpFoundation\File\MimeType; namespace Symfony\Component\HttpFoundation\File\MimeType;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException; use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException;
...@@ -99,7 +98,9 @@ class MimeTypeGuesser implements MimeTypeGuesserInterface ...@@ -99,7 +98,9 @@ class MimeTypeGuesser implements MimeTypeGuesserInterface
* *
* @return string The mime type or NULL, if none could be guessed * @return string The mime type or NULL, if none could be guessed
* *
* @throws FileException If the file does not exist * @throws \LogicException
* @throws FileNotFoundException
* @throws AccessDeniedException
*/ */
public function guess($path) public function guess($path)
{ {
......
...@@ -118,6 +118,19 @@ class UploadedFile extends File ...@@ -118,6 +118,19 @@ class UploadedFile extends File
return $this->originalName; return $this->originalName;
} }
/**
* Returns the original file extension
*
* It is extracted from the original file name that was uploaded.
* Then is should not be considered as a safe value.
*
* @return string The extension
*/
public function getClientOriginalExtension()
{
return pathinfo($this->originalName, PATHINFO_EXTENSION);
}
/** /**
* Returns the file mime type. * Returns the file mime type.
* *
......
...@@ -149,7 +149,7 @@ class HeaderBag implements \IteratorAggregate, \Countable ...@@ -149,7 +149,7 @@ class HeaderBag implements \IteratorAggregate, \Countable
* *
* @param string $key The key * @param string $key The key
* @param string|array $values The value or an array of values * @param string|array $values The value or an array of values
* @param Boolean $replace Whether to replace the actual value of not (true by default) * @param Boolean $replace Whether to replace the actual value or not (true by default)
* *
* @api * @api
*/ */
...@@ -223,7 +223,7 @@ class HeaderBag implements \IteratorAggregate, \Countable ...@@ -223,7 +223,7 @@ class HeaderBag implements \IteratorAggregate, \Countable
* @param string $key The parameter key * @param string $key The parameter key
* @param \DateTime $default The default value * @param \DateTime $default The default value
* *
* @return null|\DateTime The filtered value * @return null|\DateTime The parsed DateTime or the default value if the header does not exist
* *
* @throws \RuntimeException When the HTTP header is not parseable * @throws \RuntimeException When the HTTP header is not parseable
* *
......
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation;
/**
* Http utility functions.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class IpUtils
{
/**
* This class should not be instantiated
*/
private function __construct() {}
/**
* Validates an IPv4 or IPv6 address.
*
* @param string $requestIp
* @param string $ip
*
* @return boolean Whether the IP is valid
*/
public static function checkIp($requestIp, $ip)
{
if (false !== strpos($requestIp, ':')) {
return self::checkIp6($requestIp, $ip);
}
return self::checkIp4($requestIp, $ip);
}
/**
* Validates an IPv4 address.
*
* @param string $requestIp
* @param string $ip
*
* @return boolean Whether the IP is valid
*/
public static function checkIp4($requestIp, $ip)
{
if (false !== strpos($ip, '/')) {
list($address, $netmask) = explode('/', $ip, 2);
if ($netmask < 1 || $netmask > 32) {
return false;
}
} else {
$address = $ip;
$netmask = 32;
}
return 0 === substr_compare(sprintf('%032b', ip2long($requestIp)), sprintf('%032b', ip2long($address)), 0, $netmask);
}
/**
* Validates an IPv6 address.
*
* @author David Soria Parra <dsp at php dot net>
* @see https://github.com/dsp/v6tools
*
* @param string $requestIp
* @param string $ip
*
* @return boolean Whether the IP is valid
*
* @throws \RuntimeException When IPV6 support is not enabled
*/
public static function checkIp6($requestIp, $ip)
{
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 ($netmask < 1 || $netmask > 128) {
return false;
}
} else {
$address = $ip;
$netmask = 128;
}
$bytesAddr = unpack("n*", inet_pton($address));
$bytesTest = unpack("n*", inet_pton($requestIp));
for ($i = 1, $ceil = ceil($netmask / 16); $i <= $ceil; $i++) {
$left = $netmask - 16 * ($i-1);
$left = ($left <= 16) ? $left : 16;
$mask = ~(0xffff >> $left) & 0xffff;
if (($bytesAddr[$i] & $mask) != ($bytesTest[$i] & $mask)) {
return false;
}
}
return true;
}
}
...@@ -28,17 +28,20 @@ class JsonResponse extends Response ...@@ -28,17 +28,20 @@ class JsonResponse extends Response
* @param integer $status The response status code * @param integer $status The response status code
* @param array $headers An array of response headers * @param array $headers An array of response headers
*/ */
public function __construct($data = array(), $status = 200, $headers = array()) public function __construct($data = null, $status = 200, $headers = array())
{ {
parent::__construct('', $status, $headers); parent::__construct('', $status, $headers);
if (null === $data) {
$data = new \ArrayObject();
}
$this->setData($data); $this->setData($data);
} }
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public static function create($data = array(), $status = 200, $headers = array()) public static function create($data = null, $status = 200, $headers = array())
{ {
return new static($data, $status, $headers); return new static($data, $status, $headers);
} }
...@@ -49,6 +52,8 @@ class JsonResponse extends Response ...@@ -49,6 +52,8 @@ class JsonResponse extends Response
* @param string $callback * @param string $callback
* *
* @return JsonResponse * @return JsonResponse
*
* @throws \InvalidArgumentException
*/ */
public function setCallback($callback = null) public function setCallback($callback = null)
{ {
...@@ -77,11 +82,6 @@ class JsonResponse extends Response ...@@ -77,11 +82,6 @@ class JsonResponse extends Response
*/ */
public function setData($data = array()) public function setData($data = array())
{ {
// root should be JSON object, not array
if (is_array($data) && 0 === count($data)) {
$data = new \ArrayObject();
}
// Encode <, >, ', &, and " for RFC4627-compliant JSON, which may also be embedded into HTML. // Encode <, >, ', &, and " for RFC4627-compliant JSON, which may also be embedded into HTML.
$this->data = json_encode($data, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT); $this->data = json_encode($data, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT);
......
Copyright (c) 2004-2012 Fabien Potencier Copyright (c) 2004-2013 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
......
...@@ -96,6 +96,8 @@ class ParameterBag implements \IteratorAggregate, \Countable ...@@ -96,6 +96,8 @@ class ParameterBag implements \IteratorAggregate, \Countable
* *
* @return mixed * @return mixed
* *
* @throws \InvalidArgumentException
*
* @api * @api
*/ */
public function get($path, $default = null, $deep = false) public function get($path, $default = null, $deep = false)
......
...@@ -31,7 +31,7 @@ the HTTP specification. ...@@ -31,7 +31,7 @@ the HTTP specification.
Loading Loading
------- -------
If you are using PHP 5.3.x you must add the following to your autoloader: If you are not using Composer but are using PHP 5.3.x, you must add the following to your autoloader:
// SessionHandlerInterface // SessionHandlerInterface
if (!interface_exists('SessionHandlerInterface')) { if (!interface_exists('SessionHandlerInterface')) {
...@@ -43,4 +43,6 @@ Resources ...@@ -43,4 +43,6 @@ Resources
You can run the unit tests with the following command: You can run the unit tests with the following command:
phpunit $ cd path/to/Symfony/Component/HttpFoundation/
$ composer.phar install --dev
$ phpunit
...@@ -29,6 +29,8 @@ class RedirectResponse extends Response ...@@ -29,6 +29,8 @@ class RedirectResponse extends Response
* @param integer $status The status code (302 by default) * @param integer $status The status code (302 by default)
* @param array $headers The headers (Location is always set to the given url) * @param array $headers The headers (Location is always set to the given url)
* *
* @throws \InvalidArgumentException
*
* @see http://tools.ietf.org/html/rfc2616#section-10.3 * @see http://tools.ietf.org/html/rfc2616#section-10.3
* *
* @api * @api
...@@ -72,6 +74,8 @@ class RedirectResponse extends Response ...@@ -72,6 +74,8 @@ class RedirectResponse extends Response
* @param string $url The URL to redirect to * @param string $url The URL to redirect to
* *
* @return RedirectResponse The current response. * @return RedirectResponse The current response.
*
* @throws \InvalidArgumentException
*/ */
public function setTargetUrl($url) public function setTargetUrl($url)
{ {
......
...@@ -30,10 +30,10 @@ use Symfony\Component\HttpFoundation\Session\SessionInterface; ...@@ -30,10 +30,10 @@ use Symfony\Component\HttpFoundation\Session\SessionInterface;
*/ */
class Request class Request
{ {
const HEADER_CLIENT_IP = 'client_ip'; const HEADER_CLIENT_IP = 'client_ip';
const HEADER_CLIENT_HOST = 'client_host'; const HEADER_CLIENT_HOST = 'client_host';
const HEADER_CLIENT_PROTO = 'client_proto'; const HEADER_CLIENT_PROTO = 'client_proto';
const HEADER_CLIENT_PORT = 'client_port'; const HEADER_CLIENT_PORT = 'client_port';
protected static $trustProxy = false; protected static $trustProxy = false;
...@@ -53,6 +53,8 @@ class Request ...@@ -53,6 +53,8 @@ class Request
self::HEADER_CLIENT_PORT => 'X_FORWARDED_PORT', self::HEADER_CLIENT_PORT => 'X_FORWARDED_PORT',
); );
protected static $httpMethodParameterOverride = false;
/** /**
* @var \Symfony\Component\HttpFoundation\ParameterBag * @var \Symfony\Component\HttpFoundation\ParameterBag
* *
...@@ -251,6 +253,9 @@ class Request ...@@ -251,6 +253,9 @@ class Request
/** /**
* Creates a Request based on a given URI and configuration. * Creates a Request based on a given URI and configuration.
* *
* The information contained in the URI always take precedence
* over the other information (server and parameters).
*
* @param string $uri The URI * @param string $uri The URI
* @param string $method The HTTP method * @param string $method The HTTP method
* @param array $parameters The query (GET) or request (POST) parameters * @param array $parameters The query (GET) or request (POST) parameters
...@@ -265,7 +270,7 @@ class Request ...@@ -265,7 +270,7 @@ class Request
*/ */
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 = array(), $cookies = array(), $files = array(), $server = array(), $content = null)
{ {
$defaults = array( $server = array_replace(array(
'SERVER_NAME' => 'localhost', 'SERVER_NAME' => 'localhost',
'SERVER_PORT' => 80, 'SERVER_PORT' => 80,
'HTTP_HOST' => 'localhost', 'HTTP_HOST' => 'localhost',
...@@ -278,32 +283,38 @@ class Request ...@@ -278,32 +283,38 @@ class Request
'SCRIPT_FILENAME' => '', 'SCRIPT_FILENAME' => '',
'SERVER_PROTOCOL' => 'HTTP/1.1', 'SERVER_PROTOCOL' => 'HTTP/1.1',
'REQUEST_TIME' => time(), 'REQUEST_TIME' => time(),
); ), $server);
$server['PATH_INFO'] = '';
$server['REQUEST_METHOD'] = strtoupper($method);
$components = parse_url($uri); $components = parse_url($uri);
if (isset($components['host'])) { if (isset($components['host'])) {
$defaults['SERVER_NAME'] = $components['host']; $server['SERVER_NAME'] = $components['host'];
$defaults['HTTP_HOST'] = $components['host']; $server['HTTP_HOST'] = $components['host'];
} }
if (isset($components['scheme'])) { if (isset($components['scheme'])) {
if ('https' === $components['scheme']) { if ('https' === $components['scheme']) {
$defaults['HTTPS'] = 'on'; $server['HTTPS'] = 'on';
$defaults['SERVER_PORT'] = 443; $server['SERVER_PORT'] = 443;
} else {
unset($server['HTTPS']);
$server['SERVER_PORT'] = 80;
} }
} }
if (isset($components['port'])) { if (isset($components['port'])) {
$defaults['SERVER_PORT'] = $components['port']; $server['SERVER_PORT'] = $components['port'];
$defaults['HTTP_HOST'] = $defaults['HTTP_HOST'].':'.$components['port']; $server['HTTP_HOST'] = $server['HTTP_HOST'].':'.$components['port'];
} }
if (isset($components['user'])) { if (isset($components['user'])) {
$defaults['PHP_AUTH_USER'] = $components['user']; $server['PHP_AUTH_USER'] = $components['user'];
} }
if (isset($components['pass'])) { if (isset($components['pass'])) {
$defaults['PHP_AUTH_PW'] = $components['pass']; $server['PHP_AUTH_PW'] = $components['pass'];
} }
if (!isset($components['path'])) { if (!isset($components['path'])) {
...@@ -314,7 +325,9 @@ class Request ...@@ -314,7 +325,9 @@ class Request
case 'POST': case 'POST':
case 'PUT': case 'PUT':
case 'DELETE': case 'DELETE':
$defaults['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'; if (!isset($server['CONTENT_TYPE'])) {
$server['CONTENT_TYPE'] = 'application/x-www-form-urlencoded';
}
case 'PATCH': case 'PATCH':
$request = $parameters; $request = $parameters;
$query = array(); $query = array();
...@@ -331,14 +344,8 @@ class Request ...@@ -331,14 +344,8 @@ class Request
} }
$queryString = http_build_query($query, '', '&'); $queryString = http_build_query($query, '', '&');
$uri = $components['path'].('' !== $queryString ? '?'.$queryString : ''); $server['REQUEST_URI'] = $components['path'].('' !== $queryString ? '?'.$queryString : '');
$server['QUERY_STRING'] = $queryString;
$server = array_replace($defaults, $server, array(
'REQUEST_METHOD' => strtoupper($method),
'PATH_INFO' => '',
'REQUEST_URI' => $uri,
'QUERY_STRING' => $queryString,
));
return new static($query, $request, array(), $cookies, $files, $server, $content); return new static($query, $request, array(), $cookies, $files, $server, $content);
} }
...@@ -464,6 +471,8 @@ class Request ...@@ -464,6 +471,8 @@ class Request
*/ */
public static function trustProxyData() public static function trustProxyData()
{ {
trigger_error('trustProxyData() is deprecated since version 2.0 and will be removed in 2.3. Use setTrustedProxies() instead.', E_USER_DEPRECATED);
self::$trustProxy = true; self::$trustProxy = true;
} }
...@@ -482,6 +491,16 @@ class Request ...@@ -482,6 +491,16 @@ class Request
self::$trustProxy = $proxies ? true : false; self::$trustProxy = $proxies ? true : false;
} }
/**
* Gets the list of trusted proxies.
*
* @return array An array of trusted proxies.
*/
public static function getTrustedProxies()
{
return self::$trustedProxies;
}
/** /**
* Sets the name for trusted headers. * Sets the name for trusted headers.
* *
...@@ -496,6 +515,8 @@ class Request ...@@ -496,6 +515,8 @@ class Request
* *
* @param string $key The header key * @param string $key The header key
* @param string $value The header name * @param string $value The header name
*
* @throws \InvalidArgumentException
*/ */
public static function setTrustedHeaderName($key, $value) public static function setTrustedHeaderName($key, $value)
{ {
...@@ -511,6 +532,8 @@ class Request ...@@ -511,6 +532,8 @@ class Request
* false otherwise. * false otherwise.
* *
* @return boolean * @return boolean
*
* @deprecated Deprecated since version 2.2, to be removed in 2.3. Use getTrustedProxies instead.
*/ */
public static function isProxyTrusted() public static function isProxyTrusted()
{ {
...@@ -560,6 +583,29 @@ class Request ...@@ -560,6 +583,29 @@ class Request
return implode('&', $parts); return implode('&', $parts);
} }
/**
* Enables support for the _method request parameter to determine the intended HTTP method.
*
* Be warned that enabling this feature might lead to CSRF issues in your code.
* Check that you are using CSRF tokens when required.
*
* The HTTP method can only be overridden when the real HTTP method is POST.
*/
public static function enableHttpMethodParameterOverride()
{
self::$httpMethodParameterOverride = true;
}
/**
* Checks whether support for the _method request parameter is enabled.
*
* @return Boolean True when the _method request parameter is enabled, false otherwise
*/
public static function getHttpMethodParameterOverride()
{
return self::$httpMethodParameterOverride;
}
/** /**
* Gets a "parameter" value. * Gets a "parameter" value.
* *
...@@ -657,8 +703,6 @@ class Request ...@@ -657,8 +703,6 @@ class Request
* *
* @see http://en.wikipedia.org/wiki/X-Forwarded-For * @see http://en.wikipedia.org/wiki/X-Forwarded-For
* *
* @deprecated The proxy argument is deprecated since version 2.0 and will be removed in 2.3. Use setTrustedProxies instead.
*
* @api * @api
*/ */
public function getClientIp() public function getClientIp()
...@@ -703,7 +747,7 @@ class Request ...@@ -703,7 +747,7 @@ class Request
* *
* * http://localhost/mysite returns an empty string * * http://localhost/mysite returns an empty string
* * http://localhost/mysite/about returns '/about' * * http://localhost/mysite/about returns '/about'
* * htpp://localhost/mysite/enco%20ded returns '/enco%20ded' * * http://localhost/mysite/enco%20ded returns '/enco%20ded'
* * http://localhost/mysite/about?var=1 returns '/about' * * http://localhost/mysite/about?var=1 returns '/about'
* *
* @return string The raw path (i.e. not urldecoded) * @return string The raw path (i.e. not urldecoded)
...@@ -897,8 +941,7 @@ class Request ...@@ -897,8 +941,7 @@ class Request
*/ */
public function getUri() public function getUri()
{ {
$qs = $this->getQueryString(); if (null !== $qs = $this->getQueryString()) {
if (null !== $qs) {
$qs = '?'.$qs; $qs = '?'.$qs;
} }
...@@ -1017,26 +1060,51 @@ class Request ...@@ -1017,26 +1060,51 @@ class Request
} }
/** /**
* Gets the request method. * Gets the request "intended" method.
*
* If the X-HTTP-Method-Override header is set, and if the method is a POST,
* then it is used to determine the "real" intended HTTP method.
*
* The _method request parameter can also be used to determine the HTTP method,
* but only if enableHttpMethodParameterOverride() has been called.
* *
* The method is always an uppercased string. * The method is always an uppercased string.
* *
* @return string The request method * @return string The request method
* *
* @api * @api
*
* @see getRealMethod
*/ */
public function getMethod() public function getMethod()
{ {
if (null === $this->method) { if (null === $this->method) {
$this->method = strtoupper($this->server->get('REQUEST_METHOD', 'GET')); $this->method = strtoupper($this->server->get('REQUEST_METHOD', 'GET'));
if ('POST' === $this->method) { if ('POST' === $this->method) {
$this->method = strtoupper($this->headers->get('X-HTTP-METHOD-OVERRIDE', $this->request->get('_method', $this->query->get('_method', 'POST')))); 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')));
}
} }
} }
return $this->method; return $this->method;
} }
/**
* Gets the "real" request method.
*
* @return string The request method
*
* @see getMethod
*/
public function getRealMethod()
{
return strtoupper($this->server->get('REQUEST_METHOD', 'GET'));
}
/** /**
* Gets the mime type associated with the format. * Gets the mime type associated with the format.
* *
...@@ -1216,6 +1284,8 @@ class Request ...@@ -1216,6 +1284,8 @@ class Request
* @param Boolean $asResource If true, a resource will be returned * @param Boolean $asResource If true, a resource will be returned
* *
* @return string|resource The request body content or a resource to read the body stream. * @return string|resource The request body content or a resource to read the body stream.
*
* @throws \LogicException
*/ */
public function getContent($asResource = false) public function getContent($asResource = false)
{ {
...@@ -1275,7 +1345,18 @@ class Request ...@@ -1275,7 +1345,18 @@ class Request
return $locales[0]; return $locales[0];
} }
$preferredLanguages = array_values(array_intersect($preferredLanguages, $locales)); $extendedPreferredLanguages = array();
foreach ($preferredLanguages as $language) {
$extendedPreferredLanguages[] = $language;
if (false !== $position = strpos($language, '_')) {
$superLanguage = substr($language, 0, $position);
if (!in_array($superLanguage, $preferredLanguages)) {
$extendedPreferredLanguages[] = $superLanguage;
}
}
}
$preferredLanguages = array_values(array_intersect($extendedPreferredLanguages, $locales));
return isset($preferredLanguages[0]) ? $preferredLanguages[0] : $locales[0]; return isset($preferredLanguages[0]) ? $preferredLanguages[0] : $locales[0];
} }
...@@ -1293,9 +1374,9 @@ class Request ...@@ -1293,9 +1374,9 @@ class Request
return $this->languages; return $this->languages;
} }
$languages = $this->splitHttpAcceptHeader($this->headers->get('Accept-Language')); $languages = AcceptHeader::fromString($this->headers->get('Accept-Language'))->all();
$this->languages = array(); $this->languages = array();
foreach ($languages as $lang => $q) { foreach (array_keys($languages) as $lang) {
if (strstr($lang, '-')) { if (strstr($lang, '-')) {
$codes = explode('-', $lang); $codes = explode('-', $lang);
if ($codes[0] == 'i') { if ($codes[0] == 'i') {
...@@ -1335,7 +1416,7 @@ class Request ...@@ -1335,7 +1416,7 @@ class Request
return $this->charsets; return $this->charsets;
} }
return $this->charsets = array_keys($this->splitHttpAcceptHeader($this->headers->get('Accept-Charset'))); return $this->charsets = array_keys(AcceptHeader::fromString($this->headers->get('Accept-Charset'))->all());
} }
/** /**
...@@ -1351,14 +1432,15 @@ class Request ...@@ -1351,14 +1432,15 @@ class Request
return $this->acceptableContentTypes; return $this->acceptableContentTypes;
} }
return $this->acceptableContentTypes = array_keys($this->splitHttpAcceptHeader($this->headers->get('Accept'))); return $this->acceptableContentTypes = array_keys(AcceptHeader::fromString($this->headers->get('Accept'))->all());
} }
/** /**
* Returns true if the request is a XMLHttpRequest. * Returns true if the request is a XMLHttpRequest.
* *
* It works if your JavaScript library set an X-Requested-With HTTP header. * It works if your JavaScript library set an X-Requested-With HTTP header.
* It is known to work with Prototype, Mootools, jQuery. * It is known to work with common JavaScript frameworks:
* @link http://en.wikipedia.org/wiki/List_of_Ajax_frameworks#JavaScript
* *
* @return Boolean true if the request is an XMLHttpRequest, false otherwise * @return Boolean true if the request is an XMLHttpRequest, false otherwise
* *
...@@ -1375,40 +1457,23 @@ class Request ...@@ -1375,40 +1457,23 @@ class Request
* @param string $header Header to split * @param string $header Header to split
* *
* @return array Array indexed by the values of the Accept-* header in preferred order * @return array Array indexed by the values of the Accept-* header in preferred order
*
* @deprecated Deprecated since version 2.2, to be removed in 2.3.
*/ */
public function splitHttpAcceptHeader($header) public function splitHttpAcceptHeader($header)
{ {
if (!$header) { trigger_error('splitHttpAcceptHeader() is deprecated since version 2.2 and will be removed in 2.3.', E_USER_DEPRECATED);
return array();
}
$values = array();
$groups = array();
foreach (array_filter(explode(',', $header)) as $value) {
// Cut off any q-value that might come after a semi-colon
if (preg_match('/;\s*(q=.*$)/', $value, $match)) {
$q = substr(trim($match[1]), 2);
$value = trim(substr($value, 0, -strlen($match[0])));
} else {
$q = 1;
}
$groups[$q][] = $value;
}
krsort($groups);
foreach ($groups as $q => $items) { $headers = array();
$q = (float) $q; foreach (AcceptHeader::fromString($header)->all() as $item) {
$key = $item->getValue();
if (0 < $q) { foreach ($item->getAttributes() as $name => $value) {
foreach ($items as $value) { $key .= sprintf(';%s=%s', $name, $value);
$values[trim($value)] = $q;
}
} }
$headers[$key] = $item->getQuality();
} }
return $values; return $headers;
} }
/* /*
...@@ -1426,12 +1491,16 @@ class Request ...@@ -1426,12 +1491,16 @@ class Request
if ($this->headers->has('X_ORIGINAL_URL') && false !== stripos(PHP_OS, 'WIN')) { if ($this->headers->has('X_ORIGINAL_URL') && false !== stripos(PHP_OS, 'WIN')) {
// IIS with Microsoft Rewrite Module // IIS with Microsoft Rewrite Module
$requestUri = $this->headers->get('X_ORIGINAL_URL'); $requestUri = $this->headers->get('X_ORIGINAL_URL');
$this->headers->remove('X_ORIGINAL_URL');
} elseif ($this->headers->has('X_REWRITE_URL') && false !== stripos(PHP_OS, 'WIN')) { } elseif ($this->headers->has('X_REWRITE_URL') && false !== stripos(PHP_OS, 'WIN')) {
// IIS with ISAPI_Rewrite // IIS with ISAPI_Rewrite
$requestUri = $this->headers->get('X_REWRITE_URL'); $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') != '') { } elseif ($this->server->get('IIS_WasUrlRewritten') == '1' && $this->server->get('UNENCODED_URL') != '') {
// IIS7 with URL Rewrite: make sure we get the unencoded url (double slash problem) // IIS7 with URL Rewrite: make sure we get the unencoded url (double slash problem)
$requestUri = $this->server->get('UNENCODED_URL'); $requestUri = $this->server->get('UNENCODED_URL');
$this->server->remove('UNENCODED_URL');
$this->server->remove('IIS_WasUrlRewritten');
} elseif ($this->server->has('REQUEST_URI')) { } elseif ($this->server->has('REQUEST_URI')) {
$requestUri = $this->server->get('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 // HTTP proxy reqs setup request uri with scheme and host [and port] + the url path, only use url path
...@@ -1445,8 +1514,12 @@ class Request ...@@ -1445,8 +1514,12 @@ class Request
if ('' != $this->server->get('QUERY_STRING')) { if ('' != $this->server->get('QUERY_STRING')) {
$requestUri .= '?'.$this->server->get('QUERY_STRING'); $requestUri .= '?'.$this->server->get('QUERY_STRING');
} }
$this->server->remove('ORIG_PATH_INFO');
} }
// normalize the request URI to ease creating sub-requests from this request
$this->server->set('REQUEST_URI', $requestUri);
return $requestUri; return $requestUri;
} }
......
...@@ -143,95 +143,10 @@ class RequestMatcher implements RequestMatcherInterface ...@@ -143,95 +143,10 @@ class RequestMatcher implements RequestMatcherInterface
return false; return false;
} }
if (null !== $this->ip && !$this->checkIp($request->getClientIp(), $this->ip)) { if (null !== $this->ip && !IpUtils::checkIp($request->getClientIp(), $this->ip)) {
return false; return false;
} }
return true; return true;
} }
/**
* Validates an IP address.
*
* @param string $requestIp
* @param string $ip
*
* @return boolean True valid, false if not.
*/
protected function checkIp($requestIp, $ip)
{
// IPv6 address
if (false !== strpos($requestIp, ':')) {
return $this->checkIp6($requestIp, $ip);
} else {
return $this->checkIp4($requestIp, $ip);
}
}
/**
* Validates an IPv4 address.
*
* @param string $requestIp
* @param string $ip
*
* @return boolean True valid, false if not.
*/
protected function checkIp4($requestIp, $ip)
{
if (false !== strpos($ip, '/')) {
list($address, $netmask) = explode('/', $ip, 2);
if ($netmask < 1 || $netmask > 32) {
return false;
}
} else {
$address = $ip;
$netmask = 32;
}
return 0 === substr_compare(sprintf('%032b', ip2long($requestIp)), sprintf('%032b', ip2long($address)), 0, $netmask);
}
/**
* Validates an IPv6 address.
*
* @author David Soria Parra <dsp at php dot net>
* @see https://github.com/dsp/v6tools
*
* @param string $requestIp
* @param string $ip
*
* @return boolean True valid, false if not.
*/
protected function checkIp6($requestIp, $ip)
{
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 ($netmask < 1 || $netmask > 128) {
return false;
}
} else {
$address = $ip;
$netmask = 128;
}
$bytesAddr = unpack("n*", inet_pton($address));
$bytesTest = unpack("n*", inet_pton($requestIp));
for ($i = 1, $ceil = ceil($netmask / 16); $i <= $ceil; $i++) {
$left = $netmask - 16 * ($i-1);
$left = ($left <= 16) ? $left : 16;
$mask = ~(0xffff >> $left) & 0xffff;
if (($bytesAddr[$i] & $mask) != ($bytesTest[$i] & $mask)) {
return false;
}
}
return true;
}
} }
...@@ -131,6 +131,8 @@ class Response ...@@ -131,6 +131,8 @@ class Response
* @param integer $status The response status code * @param integer $status The response status code
* @param array $headers An array of response headers * @param array $headers An array of response headers
* *
* @throws \InvalidArgumentException When the HTTP status code is not valid
*
* @api * @api
*/ */
public function __construct($content = '', $status = 200, $headers = array()) public function __construct($content = '', $status = 200, $headers = array())
...@@ -231,7 +233,7 @@ class Response ...@@ -231,7 +233,7 @@ class Response
$headers->remove('Content-Length'); $headers->remove('Content-Length');
} }
if ('HEAD' === $request->getMethod()) { if ($request->isMethod('HEAD')) {
// cf. RFC2616 14.13 // cf. RFC2616 14.13
$length = $headers->get('Content-Length'); $length = $headers->get('Content-Length');
$this->setContent(null); $this->setContent(null);
...@@ -251,6 +253,16 @@ class Response ...@@ -251,6 +253,16 @@ class Response
$this->headers->set('expires', -1); $this->headers->set('expires', -1);
} }
/**
* Check if we need to remove Cache-Control for ssl encrypted downloads when using IE < 9
* @link http://support.microsoft.com/kb/323308
*/
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 (intval(preg_replace("/(MSIE )(.*?);/", "$2", $match[0])) < 9) {
$this->headers->remove('Cache-Control');
}
}
return $this; return $this;
} }
...@@ -270,7 +282,7 @@ class Response ...@@ -270,7 +282,7 @@ class Response
header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)); header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText));
// headers // headers
foreach ($this->headers->all() as $name => $values) { foreach ($this->headers->allPreserveCase() as $name => $values) {
foreach ($values as $value) { foreach ($values as $value) {
header($name.': '.$value, false); header($name.': '.$value, false);
} }
...@@ -336,6 +348,8 @@ class Response ...@@ -336,6 +348,8 @@ class Response
* *
* @return Response * @return Response
* *
* @throws \UnexpectedValueException
*
* @api * @api
*/ */
public function setContent($content) public function setContent($content)
...@@ -431,7 +445,7 @@ class Response ...@@ -431,7 +445,7 @@ class Response
/** /**
* Retrieves the status code for the current web response. * Retrieves the status code for the current web response.
* *
* @return string Status code * @return integer Status code
* *
* @api * @api
*/ */
...@@ -499,7 +513,7 @@ class Response ...@@ -499,7 +513,7 @@ class Response
* *
* Fresh responses may be served from cache without any interaction with the * Fresh responses may be served from cache without any interaction with the
* origin. A response is considered fresh when it includes a Cache-Control/max-age * origin. A response is considered fresh when it includes a Cache-Control/max-age
* indicator or Expiration header and the calculated age is less than the freshness lifetime. * indicator or Expires header and the calculated age is less than the freshness lifetime.
* *
* @return Boolean true if the response is fresh, false otherwise * @return Boolean true if the response is fresh, false otherwise
* *
...@@ -612,8 +626,8 @@ class Response ...@@ -612,8 +626,8 @@ class Response
*/ */
public function getAge() public function getAge()
{ {
if ($age = $this->headers->get('Age')) { if (null !== $age = $this->headers->get('Age')) {
return $age; return (int) $age;
} }
return max(time() - $this->getDate()->format('U'), 0); return max(time() - $this->getDate()->format('U'), 0);
...@@ -638,21 +652,26 @@ class Response ...@@ -638,21 +652,26 @@ class Response
/** /**
* Returns the value of the Expires header as a DateTime instance. * Returns the value of the Expires header as a DateTime instance.
* *
* @return \DateTime A DateTime instance * @return \DateTime|null A DateTime instance or null if the header does not exist
* *
* @api * @api
*/ */
public function getExpires() public function getExpires()
{ {
return $this->headers->getDate('Expires'); try {
return $this->headers->getDate('Expires');
} catch (\RuntimeException $e) {
// according to RFC 2616 invalid date formats (e.g. "0" and "-1") must be treated as in the past
return \DateTime::createFromFormat(DATE_RFC2822, 'Sat, 01 Jan 00 00:00:00 +0000');
}
} }
/** /**
* Sets the Expires HTTP header with a DateTime instance. * Sets the Expires HTTP header with a DateTime instance.
* *
* If passed a null value, it removes the header. * Passing null as value will remove the header.
* *
* @param \DateTime $date A \DateTime instance * @param \DateTime|null $date A \DateTime instance or null to remove the header
* *
* @return Response * @return Response
* *
...@@ -672,7 +691,7 @@ class Response ...@@ -672,7 +691,7 @@ class Response
} }
/** /**
* Sets the number of seconds after the time specified in the response's Date * Returns the number of seconds after the time specified in the response's Date
* header when the the response should no longer be considered fresh. * header when the the response should no longer be considered fresh.
* *
* First, it checks for a s-maxage directive, then a max-age directive, and then it falls * First, it checks for a s-maxage directive, then a max-age directive, and then it falls
...@@ -684,12 +703,12 @@ class Response ...@@ -684,12 +703,12 @@ class Response
*/ */
public function getMaxAge() public function getMaxAge()
{ {
if ($age = $this->headers->getCacheControlDirective('s-maxage')) { if ($this->headers->hasCacheControlDirective('s-maxage')) {
return $age; return (int) $this->headers->getCacheControlDirective('s-maxage');
} }
if ($age = $this->headers->getCacheControlDirective('max-age')) { if ($this->headers->hasCacheControlDirective('max-age')) {
return $age; return (int) $this->headers->getCacheControlDirective('max-age');
} }
if (null !== $this->getExpires()) { if (null !== $this->getExpires()) {
...@@ -750,7 +769,7 @@ class Response ...@@ -750,7 +769,7 @@ class Response
*/ */
public function getTtl() public function getTtl()
{ {
if ($maxAge = $this->getMaxAge()) { if (null !== $maxAge = $this->getMaxAge()) {
return $maxAge - $this->getAge(); return $maxAge - $this->getAge();
} }
...@@ -796,7 +815,9 @@ class Response ...@@ -796,7 +815,9 @@ class Response
/** /**
* Returns the Last-Modified HTTP header as a DateTime instance. * Returns the Last-Modified HTTP header as a DateTime instance.
* *
* @return \DateTime A DateTime instance * @return \DateTime|null A DateTime instance or null if the header does not exist
*
* @throws \RuntimeException When the HTTP header is not parseable
* *
* @api * @api
*/ */
...@@ -808,9 +829,9 @@ class Response ...@@ -808,9 +829,9 @@ class Response
/** /**
* Sets the Last-Modified HTTP header with a DateTime instance. * Sets the Last-Modified HTTP header with a DateTime instance.
* *
* If passed a null value, it removes the header. * Passing null as value will remove the header.
* *
* @param \DateTime $date A \DateTime instance * @param \DateTime|null $date A \DateTime instance or null to remove the header
* *
* @return Response * @return Response
* *
...@@ -832,7 +853,7 @@ class Response ...@@ -832,7 +853,7 @@ class Response
/** /**
* Returns the literal value of the ETag HTTP header. * Returns the literal value of the ETag HTTP header.
* *
* @return string The ETag HTTP header * @return string|null The ETag HTTP header or null if it does not exist
* *
* @api * @api
*/ */
...@@ -844,8 +865,8 @@ class Response ...@@ -844,8 +865,8 @@ class Response
/** /**
* Sets the ETag value. * Sets the ETag value.
* *
* @param string $etag The ETag unique identifier * @param string|null $etag The ETag unique identifier or null to remove the header
* @param Boolean $weak Whether you want a weak ETag or not * @param Boolean $weak Whether you want a weak ETag or not
* *
* @return Response * @return Response
* *
...@@ -875,6 +896,8 @@ class Response ...@@ -875,6 +896,8 @@ class Response
* *
* @return Response * @return Response
* *
* @throws \InvalidArgumentException
*
* @api * @api
*/ */
public function setCache(array $options) public function setCache(array $options)
...@@ -952,7 +975,7 @@ class Response ...@@ -952,7 +975,7 @@ class Response
*/ */
public function hasVary() public function hasVary()
{ {
return (Boolean) $this->headers->get('Vary'); return null !== $this->headers->get('Vary');
} }
/** /**
...@@ -1108,7 +1131,7 @@ class Response ...@@ -1108,7 +1131,7 @@ class Response
} }
/** /**
* Is the reponse forbidden? * Is the response forbidden?
* *
* @return Boolean * @return Boolean
* *
......
...@@ -36,6 +36,11 @@ class ResponseHeaderBag extends HeaderBag ...@@ -36,6 +36,11 @@ class ResponseHeaderBag extends HeaderBag
*/ */
protected $cookies = array(); protected $cookies = array();
/**
* @var array
*/
protected $headerNames = array();
/** /**
* Constructor. * Constructor.
* *
...@@ -48,7 +53,7 @@ class ResponseHeaderBag extends HeaderBag ...@@ -48,7 +53,7 @@ class ResponseHeaderBag extends HeaderBag
parent::__construct($headers); parent::__construct($headers);
if (!isset($this->headers['cache-control'])) { if (!isset($this->headers['cache-control'])) {
$this->set('cache-control', ''); $this->set('Cache-Control', '');
} }
} }
...@@ -62,9 +67,21 @@ class ResponseHeaderBag extends HeaderBag ...@@ -62,9 +67,21 @@ class ResponseHeaderBag extends HeaderBag
$cookies .= 'Set-Cookie: '.$cookie."\r\n"; $cookies .= 'Set-Cookie: '.$cookie."\r\n";
} }
ksort($this->headerNames);
return parent::__toString().$cookies; return parent::__toString().$cookies;
} }
/**
* Returns the headers, with original capitalizations.
*
* @return array An array of headers
*/
public function allPreserveCase()
{
return array_combine($this->headerNames, $this->headers);
}
/** /**
* {@inheritdoc} * {@inheritdoc}
* *
...@@ -72,10 +89,12 @@ class ResponseHeaderBag extends HeaderBag ...@@ -72,10 +89,12 @@ class ResponseHeaderBag extends HeaderBag
*/ */
public function replace(array $headers = array()) public function replace(array $headers = array())
{ {
$this->headerNames = array();
parent::replace($headers); parent::replace($headers);
if (!isset($this->headers['cache-control'])) { if (!isset($this->headers['cache-control'])) {
$this->set('cache-control', ''); $this->set('Cache-Control', '');
} }
} }
...@@ -88,10 +107,14 @@ class ResponseHeaderBag extends HeaderBag ...@@ -88,10 +107,14 @@ class ResponseHeaderBag extends HeaderBag
{ {
parent::set($key, $values, $replace); parent::set($key, $values, $replace);
$uniqueKey = strtr(strtolower($key), '_', '-');
$this->headerNames[$uniqueKey] = $key;
// ensure the cache-control header has sensible defaults // ensure the cache-control header has sensible defaults
if (in_array(strtr(strtolower($key), '_', '-'), array('cache-control', 'etag', 'last-modified', 'expires'))) { if (in_array($uniqueKey, array('cache-control', 'etag', 'last-modified', 'expires'))) {
$computed = $this->computeCacheControlValue(); $computed = $this->computeCacheControlValue();
$this->headers['cache-control'] = array($computed); $this->headers['cache-control'] = array($computed);
$this->headerNames['cache-control'] = 'Cache-Control';
$this->computedCacheControl = $this->parseCacheControl($computed); $this->computedCacheControl = $this->parseCacheControl($computed);
} }
} }
...@@ -105,7 +128,10 @@ class ResponseHeaderBag extends HeaderBag ...@@ -105,7 +128,10 @@ class ResponseHeaderBag extends HeaderBag
{ {
parent::remove($key); parent::remove($key);
if ('cache-control' === strtr(strtolower($key), '_', '-')) { $uniqueKey = strtr(strtolower($key), '_', '-');
unset($this->headerNames[$uniqueKey]);
if ('cache-control' === $uniqueKey) {
$this->computedCacheControl = array(); $this->computedCacheControl = array();
} }
} }
......
...@@ -31,7 +31,7 @@ class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Counta ...@@ -31,7 +31,7 @@ class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Counta
/** /**
* Constructor. * Constructor.
* *
* @param string $storageKey The key used to store flashes in the session. * @param string $storageKey The key used to store attributes in the session.
*/ */
public function __construct($storageKey = '_sf2_attributes') public function __construct($storageKey = '_sf2_attributes')
{ {
......
...@@ -177,10 +177,17 @@ class FlashBag implements FlashBagInterface, \IteratorAggregate, \Countable ...@@ -177,10 +177,17 @@ class FlashBag implements FlashBagInterface, \IteratorAggregate, \Countable
/** /**
* Returns the number of flashes. * Returns the number of flashes.
* *
* This method does not work.
*
* @deprecated in 2.2, removed in 2.3
* @see https://github.com/symfony/symfony/issues/6408
*
* @return int The number of flashes * @return int The number of flashes
*/ */
public function count() public function count()
{ {
trigger_error(sprintf('%s() is deprecated since 2.2 and will be removed in 2.3', __METHOD__), E_USER_DEPRECATED);
return count($this->flashes); return count($this->flashes);
} }
} }
...@@ -259,6 +259,8 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable ...@@ -259,6 +259,8 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
*/ */
public function getFlashes() public function getFlashes()
{ {
trigger_error('getFlashes() is deprecated since version 2.1 and will be removed in 2.3. Use the FlashBag instead.', E_USER_DEPRECATED);
$all = $this->getBag($this->flashName)->all(); $all = $this->getBag($this->flashName)->all();
$return = array(); $return = array();
...@@ -282,6 +284,8 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable ...@@ -282,6 +284,8 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
*/ */
public function setFlashes($values) public function setFlashes($values)
{ {
trigger_error('setFlashes() is deprecated since version 2.1 and will be removed in 2.3. Use the FlashBag instead.', E_USER_DEPRECATED);
foreach ($values as $name => $value) { foreach ($values as $name => $value) {
$this->getBag($this->flashName)->set($name, $value); $this->getBag($this->flashName)->set($name, $value);
} }
...@@ -297,6 +301,8 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable ...@@ -297,6 +301,8 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
*/ */
public function getFlash($name, $default = null) public function getFlash($name, $default = null)
{ {
trigger_error('getFlash() is deprecated since version 2.1 and will be removed in 2.3. Use the FlashBag instead.', E_USER_DEPRECATED);
$return = $this->getBag($this->flashName)->get($name); $return = $this->getBag($this->flashName)->get($name);
return empty($return) ? $default : reset($return); return empty($return) ? $default : reset($return);
...@@ -310,6 +316,8 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable ...@@ -310,6 +316,8 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
*/ */
public function setFlash($name, $value) public function setFlash($name, $value)
{ {
trigger_error('setFlash() is deprecated since version 2.1 and will be removed in 2.3. Use the FlashBag instead.', E_USER_DEPRECATED);
$this->getBag($this->flashName)->set($name, $value); $this->getBag($this->flashName)->set($name, $value);
} }
...@@ -322,6 +330,8 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable ...@@ -322,6 +330,8 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
*/ */
public function hasFlash($name) public function hasFlash($name)
{ {
trigger_error('hasFlash() is deprecated since version 2.1 and will be removed in 2.3. Use the FlashBag instead.', E_USER_DEPRECATED);
return $this->getBag($this->flashName)->has($name); return $this->getBag($this->flashName)->has($name);
} }
...@@ -332,6 +342,8 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable ...@@ -332,6 +342,8 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
*/ */
public function removeFlash($name) public function removeFlash($name)
{ {
trigger_error('removeFlash() is deprecated since version 2.1 and will be removed in 2.3. Use the FlashBag instead.', E_USER_DEPRECATED);
$this->getBag($this->flashName)->get($name); $this->getBag($this->flashName)->get($name);
} }
...@@ -342,6 +354,8 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable ...@@ -342,6 +354,8 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
*/ */
public function clearFlashes() public function clearFlashes()
{ {
trigger_error('clearFlashes() is deprecated since version 2.1 and will be removed in 2.3. Use the FlashBag instead.', E_USER_DEPRECATED);
return $this->getBag($this->flashName)->clear(); return $this->getBag($this->flashName)->clear();
} }
} }
...@@ -36,6 +36,13 @@ class MongoDbSessionHandler implements \SessionHandlerInterface ...@@ -36,6 +36,13 @@ class MongoDbSessionHandler implements \SessionHandlerInterface
/** /**
* Constructor. * Constructor.
* *
* List of available options:
* * database: The name of the database [required]
* * collection: The name of the collection [required]
* * 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]
*
* @param \Mongo|\MongoClient $mongo A MongoClient or Mongo instance * @param \Mongo|\MongoClient $mongo A MongoClient or Mongo instance
* @param array $options An associative array of field options * @param array $options An associative array of field options
* *
...@@ -55,9 +62,9 @@ class MongoDbSessionHandler implements \SessionHandlerInterface ...@@ -55,9 +62,9 @@ class MongoDbSessionHandler implements \SessionHandlerInterface
$this->mongo = $mongo; $this->mongo = $mongo;
$this->options = array_merge(array( $this->options = array_merge(array(
'id_field' => 'sess_id', 'id_field' => '_id',
'data_field' => 'sess_data', 'data_field' => 'data',
'time_field' => 'sess_time', 'time_field' => 'time',
), $options); ), $options);
} }
...@@ -82,10 +89,9 @@ class MongoDbSessionHandler implements \SessionHandlerInterface ...@@ -82,10 +89,9 @@ class MongoDbSessionHandler implements \SessionHandlerInterface
*/ */
public function destroy($sessionId) public function destroy($sessionId)
{ {
$this->getCollection()->remove( $this->getCollection()->remove(array(
array($this->options['id_field'] => $sessionId), $this->options['id_field'] => $sessionId
array('justOne' => true) ));
);
return true; return true;
} }
...@@ -95,11 +101,21 @@ class MongoDbSessionHandler implements \SessionHandlerInterface ...@@ -95,11 +101,21 @@ class MongoDbSessionHandler implements \SessionHandlerInterface
*/ */
public function gc($lifetime) public function gc($lifetime)
{ {
$time = new \MongoTimestamp(time() - $lifetime); /* Note: MongoDB 2.2+ supports TTL collections, which may be used in
* place of this method by indexing the "time_field" field with an
* "expireAfterSeconds" option. Regardless of whether TTL collections
* are used, consider indexing this field to make the remove query more
* efficient.
*
* See: http://docs.mongodb.org/manual/tutorial/expire-data/
*/
$time = new \MongoDate(time() - $lifetime);
$this->getCollection()->remove(array( $this->getCollection()->remove(array(
$this->options['time_field'] => array('$lt' => $time), $this->options['time_field'] => array('$lt' => $time),
)); ));
return true;
} }
/** /**
...@@ -107,16 +123,13 @@ class MongoDbSessionHandler implements \SessionHandlerInterface ...@@ -107,16 +123,13 @@ class MongoDbSessionHandler implements \SessionHandlerInterface
*/ */
public function write($sessionId, $data) public function write($sessionId, $data)
{ {
$data = array(
$this->options['id_field'] => $sessionId,
$this->options['data_field'] => new \MongoBinData($data, \MongoBinData::BYTE_ARRAY),
$this->options['time_field'] => new \MongoTimestamp()
);
$this->getCollection()->update( $this->getCollection()->update(
array($this->options['id_field'] => $sessionId), array($this->options['id_field'] => $sessionId),
array('$set' => $data), array('$set' => array(
array('upsert' => true) $this->options['data_field'] => new \MongoBinData($data, \MongoBinData::BYTE_ARRAY),
$this->options['time_field'] => new \MongoDate(),
)),
array('upsert' => true, 'multiple' => false)
); );
return true; return true;
......
...@@ -27,17 +27,17 @@ class NativeSessionStorage implements SessionStorageInterface ...@@ -27,17 +27,17 @@ class NativeSessionStorage implements SessionStorageInterface
/** /**
* Array of SessionBagInterface * Array of SessionBagInterface
* *
* @var array * @var SessionBagInterface[]
*/ */
protected $bags; protected $bags;
/** /**
* @var boolean * @var Boolean
*/ */
protected $started = false; protected $started = false;
/** /**
* @var boolean * @var Boolean
*/ */
protected $closed = false; protected $closed = false;
...@@ -332,9 +332,9 @@ class NativeSessionStorage implements SessionStorageInterface ...@@ -332,9 +332,9 @@ class NativeSessionStorage implements SessionStorageInterface
* Registers save handler as a PHP session handler. * Registers save handler as a PHP session handler.
* *
* To use internal PHP session save handlers, override this method using ini_set with * To use internal PHP session save handlers, override this method using ini_set with
* session.save_handlers and session.save_path e.g. * session.save_handler and session.save_path e.g.
* *
* ini_set('session.save_handlers', 'files'); * ini_set('session.save_handler', 'files');
* ini_set('session.save_path', /tmp'); * ini_set('session.save_path', /tmp');
* *
* @see http://php.net/session-set-save-handler * @see http://php.net/session-set-save-handler
......
...@@ -99,6 +99,8 @@ abstract class AbstractProxy ...@@ -99,6 +99,8 @@ abstract class AbstractProxy
* Sets the session ID. * Sets the session ID.
* *
* @param string $id * @param string $id
*
* @throws \LogicException
*/ */
public function setId($id) public function setId($id)
{ {
...@@ -123,6 +125,8 @@ abstract class AbstractProxy ...@@ -123,6 +125,8 @@ abstract class AbstractProxy
* Sets the session name. * Sets the session name.
* *
* @param string $name * @param string $name
*
* @throws \LogicException
*/ */
public function setName($name) public function setName($name)
{ {
......
...@@ -62,6 +62,8 @@ class StreamedResponse extends Response ...@@ -62,6 +62,8 @@ class StreamedResponse extends Response
* Sets the PHP callback associated with this Response. * Sets the PHP callback associated with this Response.
* *
* @param mixed $callback A valid PHP callback * @param mixed $callback A valid PHP callback
*
* @throws \LogicException
*/ */
public function setCallback($callback) public function setCallback($callback)
{ {
......
...@@ -19,11 +19,14 @@ ...@@ -19,11 +19,14 @@
"php": ">=5.3.3" "php": ">=5.3.3"
}, },
"autoload": { "autoload": {
"psr-0": { "psr-0": { "Symfony\\Component\\HttpFoundation\\": "" },
"Symfony\\Component\\HttpFoundation": "", "classmap": [ "Symfony/Component/HttpFoundation/Resources/stubs" ]
"SessionHandlerInterface": "Symfony/Component/HttpFoundation/Resources/stubs"
}
}, },
"target-dir": "Symfony/Component/HttpFoundation", "target-dir": "Symfony/Component/HttpFoundation",
"minimum-stability": "dev" "minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "2.2-dev"
}
}
} }
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
bootstrap="vendor/autoload.php"
>
<testsuites>
<testsuite name="Symfony HttpFoundation Component Test Suite">
<directory>./Tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./</directory>
<exclude>
<directory>./Resources</directory>
<directory>./Tests</directory>
<directory>./vendor</directory>
</exclude>
</whitelist>
</filter>
</phpunit>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment