Commit cb567c5e authored by Taylor Otwell's avatar Taylor Otwell

Updated Symfony HttpFoundation to 2.1.6.

parent f754e1fa
CHANGELOG
=========
2.1.0
-----
* added Request::getSchemeAndHttpHost() and Request::getUserInfo()
* added a fluent interface to the Response class
* added Request::isProxyTrusted()
* added JsonResponse
* added a getTargetUrl method to RedirectResponse
* added support for streamed responses
* made Response::prepare() method the place to enforce HTTP specification
* [BC BREAK] moved management of the locale from the Session class to the Request class
* added a generic access to the PHP built-in filter mechanism: ParameterBag::filter()
* made FileBinaryMimeTypeGuesser command configurable
* added Request::getUser() and Request::getPassword()
* added support for the PATCH method in Request
* removed the ContentTypeMimeTypeGuesser class as it is deprecated and never used on PHP 5.3
* added ResponseHeaderBag::makeDisposition() (implements RFC 6266)
* made mimetype to extension conversion configurable
* [BC BREAK] Moved all session related classes and interfaces into own namespace, as
`Symfony\Component\HttpFoundation\Session` and renamed classes accordingly.
Session handlers are located in the subnamespace `Symfony\Component\HttpFoundation\Session\Handler`.
* SessionHandlers must implement `\SessionHandlerInterface` or extend from the
`Symfony\Component\HttpFoundation\Storage\Handler\NativeSessionHandler` base class.
* Added internal storage driver proxy mechanism for forward compatibility with
PHP 5.4 `\SessionHandler` class.
* Added session handlers for custom Memcache, Memcached and Null session save handlers.
* [BC BREAK] Removed `NativeSessionStorage` and replaced with `NativeFileSessionHandler`.
* [BC BREAK] `SessionStorageInterface` methods removed: `write()`, `read()` and
`remove()`. Added `getBag()`, `registerBag()`. The `NativeSessionStorage` class
is a mediator for the session storage internals including the session handlers
which do the real work of participating in the internal PHP session workflow.
* [BC BREAK] Introduced mock implementations of `SessionStorage` to enable unit
and functional testing without starting real PHP sessions. Removed
`ArraySessionStorage`, and replaced with `MockArraySessionStorage` for unit
tests; removed `FilesystemSessionStorage`, and replaced with`MockFileSessionStorage`
for functional tests. These do not interact with global session ini
configuration values, session functions or `$_SESSION` superglobal. This means
they can be configured directly allowing multiple instances to work without
conflicting in the same PHP process.
* [BC BREAK] Removed the `close()` method from the `Session` class, as this is
now redundant.
* Deprecated the following methods from the Session class: `setFlash()`, `setFlashes()`
`getFlash()`, `hasFlash()`, and `removeFlash()`. Use `getFlashBag()` instead
which returns a `FlashBagInterface`.
* `Session->clear()` now only clears session attributes as before it cleared
flash messages and attributes. `Session->getFlashBag()->all()` clears flashes now.
* Session data is now managed by `SessionBagInterface` to better encapsulate
session data.
* Refactored session attribute and flash messages system to their own
`SessionBagInterface` implementations.
* 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
be retrieved by `get()` or `all()`.
* Added `Symfony\Component\HttpFoundation\Attribute\AttributeBag` to replicate
attributes storage behaviour 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
per flash type. The old `Session` class API remains without BC break as it
will allow single messages as before.
* Added basic session meta-data to the session to record session create time,
last updated time, and the lifetime of the session cookie that was provided
to the client.
* Request::getClientIp() method doesn't take a parameter anymore but bases
itself on the trustProxy parameter.
* Added isMethod() to Request object.
* [BC BREAK] The methods `getPathInfo()`, `getBaseUrl()` and `getBasePath()` of
a `Request` now all return a raw value (vs a urldecoded value before). Any call
to one of these methods must be checked and wrapped in a `rawurldecode()` if
needed.
......@@ -31,13 +31,13 @@ class Cookie
/**
* Constructor.
*
* @param string $name The name of the cookie
* @param string $value The value of the cookie
* @param integer|string|\DateTime $expire The time the cookie expires
* @param string $path The path on the server in which the cookie will be available on
* @param string $domain The domain that the cookie is available to
* @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 string $name The name of the cookie
* @param string $value The value of the cookie
* @param integer|string|\DateTime $expire The time the cookie expires
* @param string $path The path on the server in which the cookie will be available on
* @param string $domain The domain that the cookie is available to
* @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
*
* @api
*/
......@@ -72,6 +72,11 @@ class Cookie
$this->httpOnly = (Boolean) $httpOnly;
}
/**
* Returns the cookie as a string.
*
* @return string The cookie
*/
public function __toString()
{
$str = urlencode($this->getName()).'=';
......
......@@ -14,14 +14,14 @@ namespace Symfony\Component\HttpFoundation\File\Exception;
/**
* Thrown when the access on a file was denied.
*
* @author Bernhard Schussek <bernhard.schussek@symfony.com>
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class AccessDeniedException extends FileException
{
/**
* Constructor.
*
* @param string $path The path to the accessed file
* @param string $path The path to the accessed file
*/
public function __construct($path)
{
......
......@@ -14,7 +14,7 @@ namespace Symfony\Component\HttpFoundation\File\Exception;
/**
* Thrown when an error occurred in the component File
*
* @author Bernhard Schussek <bernhard.schussek@symfony.com>
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FileException extends \RuntimeException
{
......
......@@ -14,14 +14,14 @@ namespace Symfony\Component\HttpFoundation\File\Exception;
/**
* Thrown when a file was not found
*
* @author Bernhard Schussek <bernhard.schussek@symfony.com>
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FileNotFoundException extends FileException
{
/**
* Constructor.
*
* @param string $path The path to the file that was not found
* @param string $path The path to the file that was not found
*/
public function __construct($path)
{
......
......@@ -14,7 +14,7 @@ namespace Symfony\Component\HttpFoundation\File\Exception;
/**
* Thrown when an error occurred during file upload
*
* @author Bernhard Schussek <bernhard.schussek@symfony.com>
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class UploadException extends FileException
{
......
......@@ -19,7 +19,7 @@ use Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser;
/**
* A file in the file system.
*
* @author Bernhard Schussek <bernhard.schussek@symfony.com>
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @api
*/
......@@ -106,6 +106,20 @@ class File extends \SplFileInfo
* @api
*/
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'])));
}
@chmod($target, 0666 & ~umask());
return $target;
}
protected function getTargetFile($directory, $name = null)
{
if (!is_dir($directory)) {
if (false === @mkdir($directory, 0777, true)) {
......@@ -115,15 +129,24 @@ class File extends \SplFileInfo
throw new FileException(sprintf('Unable to write in the "%s" directory', $directory));
}
$target = $directory.DIRECTORY_SEPARATOR.(null === $name ? $this->getBasename() : basename($name));
$target = $directory.DIRECTORY_SEPARATOR.(null === $name ? $this->getBasename() : $this->getName($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'])));
}
return new File($target, false);
}
chmod($target, 0666);
/**
* Returns locale independent base name of the given path.
*
* @param string $name The new file name
*
* @return string containing
*/
protected function getName($name)
{
$originalName = str_replace('\\', '/', $name);
$pos = strrpos($originalName, '/');
$originalName = false === $pos ? $originalName : substr($originalName, $pos + 1);
return new File($target);
return $originalName;
}
}
......@@ -30,12 +30,14 @@ class ExtensionGuesser implements ExtensionGuesserInterface
{
/**
* The singleton instance
*
* @var ExtensionGuesser
*/
static private $instance = null;
private static $instance = null;
/**
* All registered ExtensionGuesserInterface instances
*
* @var array
*/
protected $guessers = array();
......@@ -45,7 +47,7 @@ class ExtensionGuesser implements ExtensionGuesserInterface
*
* @return ExtensionGuesser
*/
static public function getInstance()
public static function getInstance()
{
if (null === self::$instance) {
self::$instance = new self();
......@@ -82,8 +84,8 @@ class ExtensionGuesser implements ExtensionGuesserInterface
* returns a value that is not NULL, this method terminates and returns the
* value.
*
* @param string $mimeType The mime type
* @return string The guessed extension or NULL, if none could be guessed
* @param string $mimeType The mime type
* @return string The guessed extension or NULL, if none could be guessed
*/
public function guess($mimeType)
{
......
......@@ -19,8 +19,8 @@ interface ExtensionGuesserInterface
/**
* Makes a best guess for a file extension, given a mime type
*
* @param string $mimeType The mime type
* @return string The guessed extension or NULL, if none could be guessed
* @param string $mimeType The mime type
* @return string The guessed extension or NULL, if none could be guessed
*/
function guess($mimeType);
public function guess($mimeType);
}
......@@ -17,7 +17,7 @@ use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException;
/**
* Guesses the mime type with the binary "file" (only available on *nix)
*
* @author Bernhard Schussek <bernhard.schussek@symfony.com>
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FileBinaryMimeTypeGuesser implements MimeTypeGuesserInterface
{
......@@ -43,15 +43,13 @@ class FileBinaryMimeTypeGuesser implements MimeTypeGuesserInterface
*
* @return Boolean
*/
static public function isSupported()
public static function isSupported()
{
return !defined('PHP_WINDOWS_VERSION_BUILD');
}
/**
* Guesses the mime type of the file with the given path
*
* @see MimeTypeGuesserInterface::guess()
* {@inheritdoc}
*/
public function guess($path)
{
......
......@@ -17,7 +17,7 @@ use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException;
/**
* Guesses the mime type using the PECL extension FileInfo
*
* @author Bernhard Schussek <bernhard.schussek@symfony.com>
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FileinfoMimeTypeGuesser implements MimeTypeGuesserInterface
{
......@@ -26,15 +26,13 @@ class FileinfoMimeTypeGuesser implements MimeTypeGuesserInterface
*
* @return Boolean
*/
static public function isSupported()
public static function isSupported()
{
return function_exists('finfo_open');
}
/**
* Guesses the mime type of the file with the given path
*
* @see MimeTypeGuesserInterface::guess()
* {@inheritdoc}
*/
public function guess($path)
{
......
......@@ -9,7 +9,7 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\File\Mimetype;
namespace Symfony\Component\HttpFoundation\File\MimeType;
/**
* Provides a best-guess mapping of mime type to file extension.
......@@ -542,6 +542,7 @@ class MimeTypeExtensionGuesser implements ExtensionGuesserInterface
'application/x-pkcs7-certificates' => 'p7b',
'application/x-pkcs7-certreqresp' => 'p7r',
'application/x-rar-compressed' => 'rar',
'application/x-rar' => 'rar',
'application/x-sh' => 'sh',
'application/x-shar' => 'shar',
'application/x-shockwave-flash' => 'swf',
......@@ -730,11 +731,7 @@ class MimeTypeExtensionGuesser implements ExtensionGuesserInterface
);
/**
* Returns the extension based on the mime type.
*
* If the mime type is unknown, returns null.
*
* @return string|null The guessed extension or null if it cannot be guessed
* {@inheritdoc}
*/
public function guess($mimeType)
{
......
......@@ -11,6 +11,7 @@
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\AccessDeniedException;
......@@ -28,18 +29,20 @@ use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException;
*
* The last registered guesser is preferred over previously registered ones.
*
* @author Bernhard Schussek <bernhard.schussek@symfony.com>
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class MimeTypeGuesser implements MimeTypeGuesserInterface
{
/**
* The singleton instance
*
* @var MimeTypeGuesser
*/
static private $instance = null;
private static $instance = null;
/**
* All registered MimeTypeGuesserInterface instances
*
* @var array
*/
protected $guessers = array();
......@@ -49,7 +52,7 @@ class MimeTypeGuesser implements MimeTypeGuesserInterface
*
* @return MimeTypeGuesser
*/
static public function getInstance()
public static function getInstance()
{
if (null === self::$instance) {
self::$instance = new self();
......@@ -92,7 +95,7 @@ class MimeTypeGuesser implements MimeTypeGuesserInterface
* returns a value that is not NULL, this method terminates and returns the
* value.
*
* @param string $path The path to the file
* @param string $path The path to the file
*
* @return string The mime type or NULL, if none could be guessed
*
......
......@@ -11,22 +11,25 @@
namespace Symfony\Component\HttpFoundation\File\MimeType;
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException;
/**
* Guesses the mime type of a file
*
* @author Bernhard Schussek <bernhard.schussek@symfony.com>
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface MimeTypeGuesserInterface
{
/**
* Guesses the mime type of the file with the given path.
*
* @param string $path The path to the file
* @param string $path The path to the file
*
* @return string The mime type or NULL, if none could be guessed
*
* @throws FileNotFoundException If the file does not exist
* @throws AccessDeniedException If the file could not be read
*/
function guess($path);
public function guess($path);
}
......@@ -17,7 +17,7 @@ use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
/**
* A file uploaded through a form.
*
* @author Bernhard Schussek <bernhard.schussek@symfony.com>
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Florian Eckerstorfer <florian@eckerstorfer.org>
* @author Fabien Potencier <fabien@symfony.com>
*
......@@ -94,7 +94,7 @@ class UploadedFile extends File
throw new FileException(sprintf('Unable to create UploadedFile because "file_uploads" is disabled in your php.ini file (%s)', get_cfg_var('cfg_file_path')));
}
$this->originalName = basename($originalName);
$this->originalName = $this->getName($originalName);
$this->mimeType = $mimeType ?: 'application/octet-stream';
$this->size = $size;
$this->error = $error ?: UPLOAD_ERR_OK;
......@@ -166,7 +166,7 @@ class UploadedFile extends File
/**
* Returns whether the file was uploaded successfully.
*
* @return Boolean True if no error occurred during uploading
* @return Boolean True if no error occurred during uploading
*
* @api
*/
......@@ -189,8 +189,21 @@ class UploadedFile extends File
*/
public function move($directory, $name = null)
{
if ($this->isValid() && ($this->test || is_uploaded_file($this->getPathname()))) {
return parent::move($directory, $name);
if ($this->isValid()) {
if ($this->test) {
return parent::move($directory, $name);
} elseif (is_uploaded_file($this->getPathname())) {
$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'])));
}
@chmod($target, 0666 & ~umask());
return $target;
}
}
throw new FileException(sprintf('The file "%s" has not been uploaded via Http', $this->getPathname()));
......@@ -199,9 +212,9 @@ class UploadedFile extends File
/**
* Returns the maximum size of an uploaded file as configured in php.ini
*
* @return type The maximum size of an uploaded file in bytes
* @return int The maximum size of an uploaded file in bytes
*/
static public function getMaxFilesize()
public static function getMaxFilesize()
{
$max = trim(ini_get('upload_max_filesize'));
......
......@@ -23,7 +23,7 @@ use Symfony\Component\HttpFoundation\File\UploadedFile;
*/
class FileBag extends ParameterBag
{
static private $fileKeys = array('error', 'name', 'size', 'tmp_name', 'type');
private static $fileKeys = array('error', 'name', 'size', 'tmp_name', 'type');
/**
* Constructor.
......@@ -38,8 +38,7 @@ class FileBag extends ParameterBag
}
/**
* (non-PHPdoc)
* @see Symfony\Component\HttpFoundation\ParameterBag::replace()
* {@inheritdoc}
*
* @api
*/
......@@ -50,23 +49,21 @@ class FileBag extends ParameterBag
}
/**
* (non-PHPdoc)
* @see Symfony\Component\HttpFoundation\ParameterBag::set()
* {@inheritdoc}
*
* @api
*/
public function set($key, $value)
{
if (is_array($value) || $value instanceof UploadedFile) {
parent::set($key, $this->convertFileInformation($value));
} else {
if (!is_array($value) && !$value instanceof UploadedFile) {
throw new \InvalidArgumentException('An uploaded file must be an array or an instance of UploadedFile.');
}
parent::set($key, $this->convertFileInformation($value));
}
/**
* (non-PHPdoc)
* @see Symfony\Component\HttpFoundation\ParameterBag::add()
* {@inheritdoc}
*
* @api
*/
......@@ -80,7 +77,7 @@ class FileBag extends ParameterBag
/**
* Converts uploaded files to UploadedFile instances.
*
* @param array|UploadedFile $file A (multi-dimensional) array of uploaded file information
* @param array|UploadedFile $file A (multi-dimensional) array of uploaded file information
*
* @return array A (multi-dimensional) array of UploadedFile instances
*/
......@@ -121,7 +118,7 @@ class FileBag extends ParameterBag
* It's safe to pass an already converted array, in which case this method
* just returns the original array unmodified.
*
* @param array $data
* @param array $data
*
* @return array
*/
......
......@@ -18,7 +18,7 @@ namespace Symfony\Component\HttpFoundation;
*
* @api
*/
class HeaderBag
class HeaderBag implements \IteratorAggregate, \Countable
{
protected $headers;
protected $cacheControl;
......@@ -50,16 +50,13 @@ class HeaderBag
return '';
}
$beautifier = function ($name) {
return preg_replace_callback('/\-(.)/', function ($match) { return '-'.strtoupper($match[1]); }, ucfirst($name));
};
$max = max(array_map('strlen', array_keys($this->headers))) + 1;
$content = '';
ksort($this->headers);
foreach ($this->headers as $name => $values) {
$name = implode('-', array_map('ucfirst', explode('-', $name)));
foreach ($values as $value) {
$content .= sprintf("%-{$max}s %s\r\n", $beautifier($name).':', $value);
$content .= sprintf("%-{$max}s %s\r\n", $name.':', $value);
}
}
......@@ -93,7 +90,7 @@ class HeaderBag
/**
* Replaces the current HTTP headers by a new set.
*
* @param array $headers An array of HTTP headers
* @param array $headers An array of HTTP headers
*
* @api
*/
......@@ -106,7 +103,7 @@ class HeaderBag
/**
* Adds new headers the current HTTP headers set.
*
* @param array $headers An array of HTTP headers
* @param array $headers An array of HTTP headers
*
* @api
*/
......@@ -160,7 +157,7 @@ class HeaderBag
{
$key = strtr(strtolower($key), '_', '-');
$values = (array) $values;
$values = array_values((array) $values);
if (true === $replace || !isset($this->headers[$key])) {
$this->headers[$key] = $values;
......@@ -226,7 +223,9 @@ class HeaderBag
* @param string $key The parameter key
* @param \DateTime $default The default value
*
* @return \DateTime The filtered value
* @return null|\DateTime The filtered value
*
* @throws \RuntimeException When the HTTP header is not parseable
*
* @api
*/
......@@ -267,6 +266,26 @@ class HeaderBag
$this->set('Cache-Control', $this->getCacheControlHeader());
}
/**
* Returns an iterator for headers.
*
* @return \ArrayIterator An \ArrayIterator instance
*/
public function getIterator()
{
return new \ArrayIterator($this->headers);
}
/**
* Returns the number of headers.
*
* @return int The number of headers
*/
public function count()
{
return count($this->headers);
}
protected function getCacheControlHeader()
{
$parts = array();
......@@ -298,7 +317,7 @@ class HeaderBag
$cacheControl = array();
preg_match_all('#([a-zA-Z][a-zA-Z_-]*)\s*(?:=(?:"([^"]*)"|([^ \t",;]*)))?#', $header, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$cacheControl[strtolower($match[1])] = isset($match[2]) && $match[2] ? $match[2] : (isset($match[3]) ? $match[3] : true);
$cacheControl[strtolower($match[1])] = isset($match[3]) ? $match[3] : (isset($match[2]) ? $match[2] : true);
}
return $cacheControl;
......
......@@ -18,6 +18,9 @@ namespace Symfony\Component\HttpFoundation;
*/
class JsonResponse extends Response
{
protected $data;
protected $callback;
/**
* Constructor.
*
......@@ -26,24 +29,85 @@ class JsonResponse extends Response
* @param array $headers An array of response headers
*/
public function __construct($data = array(), $status = 200, $headers = array())
{
parent::__construct('', $status, $headers);
$this->setData($data);
}
/**
* {@inheritDoc}
*/
public static function create($data = array(), $status = 200, $headers = array())
{
return new static($data, $status, $headers);
}
/**
* Sets the JSONP callback.
*
* @param string $callback
*
* @return JsonResponse
*/
public function setCallback($callback = null)
{
if (null !== $callback) {
// taken from http://www.geekality.net/2011/08/03/valid-javascript-identifier/
$pattern = '/^[$_\p{L}][$_\p{L}\p{Mn}\p{Mc}\p{Nd}\p{Pc}\x{200C}\x{200D}]*+$/u';
$parts = explode('.', $callback);
foreach ($parts as $part) {
if (!preg_match($pattern, $part)) {
throw new \InvalidArgumentException('The callback name is not valid.');
}
}
}
$this->callback = $callback;
return $this->update();
}
/**
* Sets the data to be sent as json.
*
* @param mixed $data
*
* @return JsonResponse
*/
public function setData($data = array())
{
// root should be JSON object, not array
if (is_array($data) && 0 === count($data)) {
$data = new \ArrayObject();
}
parent::__construct(
json_encode($data),
$status,
array_merge(array('Content-Type' => 'application/json'), $headers)
);
// 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);
return $this->update();
}
/**
* {@inheritDoc}
* Updates the content and headers according to the json data and callback.
*
* @return JsonResponse
*/
static public function create($data = array(), $status = 200, $headers = array())
protected function update()
{
return new static($data, $status, $headers);
if (null !== $this->callback) {
// Not using application/javascript for compatibility reasons with older browsers.
$this->headers->set('Content-Type', 'text/javascript');
return $this->setContent(sprintf('%s(%s);', $this->callback, $this->data));
}
// Only set the header when there is none or when it equals 'text/javascript' (from a previous update with callback)
// in order to not overwrite a custom definition.
if (!$this->headers->has('Content-Type') || 'text/javascript' === $this->headers->get('Content-Type')) {
$this->headers->set('Content-Type', 'application/json');
}
return $this->setContent($this->data);
}
}
......@@ -18,7 +18,7 @@ namespace Symfony\Component\HttpFoundation;
*
* @api
*/
class ParameterBag
class ParameterBag implements \IteratorAggregate, \Countable
{
/**
* Parameter storage.
......@@ -92,7 +92,9 @@ class ParameterBag
*
* @param string $path The key
* @param mixed $default The default value if the parameter key does not exist
* @param boolean $deep If true, a path like foo[bar] will find deeper items
* @param boolean $deep If true, a path like foo[bar] will find deeper items
*
* @return mixed
*
* @api
*/
......@@ -109,7 +111,7 @@ class ParameterBag
$value = $this->parameters[$root];
$currentKey = null;
for ($i=$pos,$c=strlen($path); $i<$c; $i++) {
for ($i = $pos, $c = strlen($path); $i < $c; $i++) {
$char = $path[$i];
if ('[' === $char) {
......@@ -189,7 +191,7 @@ class ParameterBag
*
* @param string $key The parameter key
* @param mixed $default The default value if the parameter key does not exist
* @param boolean $deep If true, a path like foo[bar] will find deeper items
* @param boolean $deep If true, a path like foo[bar] will find deeper items
*
* @return string The filtered value
*
......@@ -205,7 +207,7 @@ class ParameterBag
*
* @param string $key The parameter key
* @param mixed $default The default value if the parameter key does not exist
* @param boolean $deep If true, a path like foo[bar] will find deeper items
* @param boolean $deep If true, a path like foo[bar] will find deeper items
*
* @return string The filtered value
*
......@@ -221,7 +223,7 @@ class ParameterBag
*
* @param string $key The parameter key
* @param mixed $default The default value if the parameter key does not exist
* @param boolean $deep If true, a path like foo[bar] will find deeper items
* @param boolean $deep If true, a path like foo[bar] will find deeper items
*
* @return string The filtered value
*
......@@ -238,9 +240,9 @@ class ParameterBag
*
* @param string $key The parameter key
* @param mixed $default The default value if the parameter key does not exist
* @param boolean $deep If true, a path like foo[bar] will find deeper items
* @param boolean $deep If true, a path like foo[bar] will find deeper items
*
* @return string The filtered value
* @return integer The filtered value
*
* @api
*/
......@@ -278,4 +280,24 @@ class ParameterBag
return filter_var($value, $filter, $options);
}
/**
* Returns an iterator for parameters.
*
* @return \ArrayIterator An \ArrayIterator instance
*/
public function getIterator()
{
return new \ArrayIterator($this->parameters);
}
/**
* Returns the number of parameters.
*
* @return int The number of parameters
*/
public function count()
{
return count($this->parameters);
}
}
......@@ -38,10 +38,9 @@ If you are using PHP 5.3.x you must add the following to your autoloader:
$loader->registerPrefixFallback(__DIR__.'/../vendor/symfony/src/Symfony/Component/HttpFoundation/Resources/stubs');
}
Resources
---------
Unit tests:
You can run the unit tests with the following command:
https://github.com/symfony/symfony/tree/master/tests/Symfony/Tests/Component/HttpFoundation
phpunit
......@@ -39,24 +39,9 @@ class RedirectResponse extends Response
throw new \InvalidArgumentException('Cannot redirect to an empty URL.');
}
$this->targetUrl = $url;
parent::__construct(
sprintf('<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="refresh" content="1;url=%1$s" />
parent::__construct('', $status, $headers);
<title>Redirecting to %1$s</title>
</head>
<body>
Redirecting to <a href="%1$s">%1$s</a>.
</body>
</html>', htmlspecialchars($url, ENT_QUOTES, 'UTF-8')),
$status,
array_merge($headers, array('Location' => $url))
);
$this->setTargetUrl($url);
if (!$this->isRedirect()) {
throw new \InvalidArgumentException(sprintf('The HTTP status code is not a redirect ("%s" given).', $status));
......@@ -66,7 +51,7 @@ class RedirectResponse extends Response
/**
* {@inheritDoc}
*/
static public function create($url = '', $status = 302, $headers = array())
public static function create($url = '', $status = 302, $headers = array())
{
return new static($url, $status, $headers);
}
......@@ -80,4 +65,38 @@ class RedirectResponse extends Response
{
return $this->targetUrl;
}
/**
* Sets the redirect target of this response.
*
* @param string $url The URL to redirect to
*
* @return RedirectResponse The current response.
*/
public function setTargetUrl($url)
{
if (empty($url)) {
throw new \InvalidArgumentException('Cannot redirect to an empty URL.');
}
$this->targetUrl = $url;
$this->setContent(
sprintf('<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="refresh" content="1;url=%1$s" />
<title>Redirecting to %1$s</title>
</head>
<body>
Redirecting to <a href="%1$s">%1$s</a>.
</body>
</html>', htmlspecialchars($url, ENT_QUOTES, 'UTF-8')));
$this->headers->set('Location', $url);
return $this;
}
}
......@@ -16,13 +16,42 @@ use Symfony\Component\HttpFoundation\Session\SessionInterface;
/**
* Request represents an HTTP request.
*
* The methods dealing with URL accept / return a raw path (% encoded):
* * getBasePath
* * getBaseUrl
* * getPathInfo
* * getRequestUri
* * getUri
* * getUriForPath
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @api
*/
class Request
{
static protected $trustProxy = false;
const HEADER_CLIENT_IP = 'client_ip';
const HEADER_CLIENT_HOST = 'client_host';
const HEADER_CLIENT_PROTO = 'client_proto';
const HEADER_CLIENT_PORT = 'client_port';
protected static $trustProxy = false;
protected static $trustedProxies = array();
/**
* Names for headers that can be trusted when
* using trusted proxies.
*
* The default names are non-standard, but widely used
* by popular reverse proxies (like Apache mod_proxy or Amazon EC2).
*/
protected static $trustedHeaders = array(
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',
);
/**
* @var \Symfony\Component\HttpFoundation\ParameterBag
......@@ -79,17 +108,17 @@ class Request
protected $content;
/**
* @var string
* @var array
*/
protected $languages;
/**
* @var string
* @var array
*/
protected $charsets;
/**
* @var string
* @var array
*/
protected $acceptableContentTypes;
......@@ -139,9 +168,9 @@ class Request
protected $defaultLocale = 'en';
/**
* @var string
* @var array
*/
static protected $formats;
protected static $formats;
/**
* Constructor.
......@@ -205,11 +234,11 @@ class Request
*
* @api
*/
static public function createFromGlobals()
public static function createFromGlobals()
{
$request = new static($_GET, $_POST, array(), $_COOKIE, $_FILES, $_SERVER);
if (0 === strpos($request->server->get('CONTENT_TYPE'), 'application/x-www-form-urlencoded')
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'))
) {
parse_str($request->getContent(), $data);
......@@ -224,7 +253,7 @@ class Request
*
* @param string $uri The URI
* @param string $method The HTTP method
* @param array $parameters The request (GET) or query (POST) parameters
* @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)
......@@ -234,7 +263,7 @@ class Request
*
* @api
*/
static public 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_NAME' => 'localhost',
......@@ -278,7 +307,7 @@ class Request
}
if (!isset($components['path'])) {
$components['path'] = '';
$components['path'] = '/';
}
switch (strtoupper($method)) {
......@@ -297,15 +326,12 @@ class Request
}
if (isset($components['query'])) {
$queryString = html_entity_decode($components['query']);
parse_str($queryString, $qs);
if (is_array($qs)) {
$query = array_replace($qs, $query);
}
parse_str(html_entity_decode($components['query']), $qs);
$query = array_replace($qs, $query);
}
$queryString = http_build_query($query);
$queryString = http_build_query($query, '', '&');
$uri = $components['path'].($queryString ? '?'.$queryString : '');
$uri = $components['path'].('' !== $queryString ? '?'.$queryString : '');
$server = array_replace($defaults, $server, array(
'REQUEST_METHOD' => strtoupper($method),
......@@ -327,6 +353,8 @@ class Request
* @param array $files The FILES parameters
* @param array $server The SERVER parameters
*
* @return Request The duplicated request
*
* @api
*/
public function duplicate(array $query = null, array $request = null, array $attributes = null, array $cookies = null, array $files = null, array $server = null)
......@@ -397,7 +425,8 @@ class Request
/**
* Overrides the PHP global variables according to this request instance.
*
* It overrides $_GET, $_POST, $_REQUEST, $_SERVER, $_COOKIE, and $_FILES.
* It overrides $_GET, $_POST, $_REQUEST, $_SERVER, $_COOKIE.
* $_FILES is never override, see rfc1867
*
* @api
*/
......@@ -407,7 +436,6 @@ class Request
$_POST = $this->request->all();
$_SERVER = $this->server->all();
$_COOKIE = $this->cookies->all();
// FIXME: populate $_FILES
foreach ($this->headers->all() as $key => $value) {
$key = strtoupper(str_replace('-', '_', $key));
......@@ -418,22 +446,64 @@ class Request
}
}
// FIXME: should read variables_order and request_order
// to know which globals to merge and in which order
$_REQUEST = array_merge($_GET, $_POST);
$request = array('g' => $_GET, 'p' => $_POST, 'c' => $_COOKIE);
$requestOrder = ini_get('request_order') ?: ini_get('variable_order');
$requestOrder = preg_replace('#[^cgp]#', '', strtolower($requestOrder)) ?: 'gp';
$_REQUEST = array();
foreach (str_split($requestOrder) as $order) {
$_REQUEST = array_merge($_REQUEST, $request[$order]);
}
}
/**
* Trusts $_SERVER entries coming from proxies.
*
* You should only call this method if your application
* is hosted behind a reverse proxy that you manage.
* @deprecated Deprecated since version 2.0, to be removed in 2.3. Use setTrustedProxies instead.
*/
public static function trustProxyData()
{
self::$trustProxy = true;
}
/**
* Sets a list of trusted proxies.
*
* You should only list the reverse proxies that you manage directly.
*
* @param array $proxies A list of trusted proxies
*
* @api
*/
static public function trustProxyData()
public static function setTrustedProxies(array $proxies)
{
self::$trustProxy = true;
self::$trustedProxies = $proxies;
self::$trustProxy = $proxies ? true : false;
}
/**
* Sets the name for trusted headers.
*
* The following header keys are supported:
*
* * Request::HEADER_CLIENT_IP: defaults to X-Forwarded-For (see getClientIp())
* * Request::HEADER_CLIENT_HOST: defaults to X-Forwarded-Host (see getClientHost())
* * Request::HEADER_CLIENT_PORT: defaults to X-Forwarded-Port (see getClientPort())
* * Request::HEADER_CLIENT_PROTO: defaults to X-Forwarded-Proto (see getScheme() and isSecure())
*
* Setting an empty value allows to disable the trusted header for the given key.
*
* @param string $key The header key
* @param string $value The header name
*/
public static function setTrustedHeaderName($key, $value)
{
if (!array_key_exists($key, self::$trustedHeaders)) {
throw new \InvalidArgumentException(sprintf('Unable to set the trusted header name for key "%s".', $key));
}
self::$trustedHeaders[$key] = $value;
}
/**
......@@ -442,29 +512,72 @@ class Request
*
* @return boolean
*/
static public function isProxyTrusted()
public static function isProxyTrusted()
{
return self::$trustProxy;
}
/**
* Normalizes a query string.
*
* It builds a normalized query string, where keys/value pairs are alphabetized,
* have consistent escaping and unneeded delimiters are removed.
*
* @param string $qs Query string
*
* @return string A normalized query string for the Request
*/
public static function normalizeQueryString($qs)
{
if ('' == $qs) {
return '';
}
$parts = array();
$order = array();
foreach (explode('&', $qs) as $param) {
if ('' === $param || '=' === $param[0]) {
// Ignore useless delimiters, e.g. "x=y&".
// Also 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.
continue;
}
$keyValuePair = explode('=', $param, 2);
// 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. This is why we use urldecode and then normalize to
// RFC 3986 with rawurlencode.
$parts[] = isset($keyValuePair[1]) ?
rawurlencode(urldecode($keyValuePair[0])).'='.rawurlencode(urldecode($keyValuePair[1])) :
rawurlencode(urldecode($keyValuePair[0]));
$order[] = urldecode($keyValuePair[0]);
}
array_multisort($order, SORT_ASC, $parts);
return implode('&', $parts);
}
/**
* Gets a "parameter" value.
*
* This method is mainly useful for libraries that want to provide some flexibility.
*
* Order of precedence: GET, PATH, POST, COOKIE
* Order of precedence: GET, PATH, POST
*
* Avoid using this method in controllers:
*
* * slow
* * prefer to get from a "named" source
*
* It is better to explicity get request parameters from the appropriate
* public property instead (query, request, attributes, ...).
* It is better to explicitly get request parameters from the appropriate
* public property instead (query, attributes, request).
*
* @param string $key the key
* @param mixed $default the default value
* @param type $deep is parameter deep in multidimensional array
* @param string $key the key
* @param mixed $default the default value
* @param Boolean $deep is parameter deep in multidimensional array
*
* @return mixed
*/
......@@ -489,22 +602,24 @@ class Request
* Whether the request contains a Session which was started in one of the
* previous requests.
*
* @return boolean
* @return Boolean
*
* @api
*/
public function hasPreviousSession()
{
// the check for $this->session avoids malicious users trying to fake a session cookie with proper name
$sessionName = $this->hasSession() ? $this->session->getName() : null;
return $this->cookies->has($sessionName) && $this->hasSession();
return $this->hasSession() && $this->cookies->has($this->session->getName());
}
/**
* Whether the request contains a Session object.
*
* @return boolean
* This method does not give any information about the state of the session object,
* like whether the session is started or not. It is just a way to check if this Request
* is associated with a Session instance.
*
* @return Boolean true when the Request contains a Session object, false otherwise
*
* @api
*/
......@@ -528,23 +643,43 @@ class Request
/**
* Returns the client IP address.
*
* This method can read the client IP address from the "X-Forwarded-For" header
* when trusted proxies were set via "setTrustedProxies()". The "X-Forwarded-For"
* header value is a comma+space separated list of IP addresses, the left-most
* being the original client, and each successive proxy that passed the request
* adding the IP address where it received the request from.
*
* If your reverse proxy uses a different header name than "X-Forwarded-For",
* ("Client-Ip" for instance), configure it via "setTrustedHeaderName()" with
* the "client-ip" key.
*
* @return string The client IP address
*
* @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
*/
public function getClientIp()
{
if (self::$trustProxy) {
if ($this->server->has('HTTP_CLIENT_IP')) {
return $this->server->get('HTTP_CLIENT_IP');
} elseif ($this->server->has('HTTP_X_FORWARDED_FOR')) {
$clientIp = explode(',', $this->server->get('HTTP_X_FORWARDED_FOR'), 2);
$ip = $this->server->get('REMOTE_ADDR');
return isset($clientIp[0]) ? trim($clientIp[0]) : '';
}
if (!self::$trustProxy) {
return $ip;
}
if (!self::$trustedHeaders[self::HEADER_CLIENT_IP] || !$this->headers->has(self::$trustedHeaders[self::HEADER_CLIENT_IP])) {
return $ip;
}
return $this->server->get('REMOTE_ADDR');
$clientIps = array_map('trim', explode(',', $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_IP])));
$clientIps[] = $ip;
$trustedProxies = self::$trustProxy && !self::$trustedProxies ? array($ip) : self::$trustedProxies;
$clientIps = array_diff($clientIps, $trustedProxies);
return array_pop($clientIps);
}
/**
......@@ -568,9 +703,10 @@ class Request
*
* * http://localhost/mysite returns an empty string
* * http://localhost/mysite/about returns '/about'
* * htpp://localhost/mysite/enco%20ded returns '/enco%20ded'
* * http://localhost/mysite/about?var=1 returns '/about'
*
* @return string
* @return string The raw path (i.e. not urldecoded)
*
* @api
*/
......@@ -588,11 +724,12 @@ class Request
*
* Suppose that an index.php file instantiates this request object:
*
* * http://localhost/index.php returns an empty string
* * http://localhost/index.php/page returns an empty string
* * http://localhost/web/index.php return '/web'
* * http://localhost/index.php returns an empty string
* * http://localhost/index.php/page returns an empty string
* * http://localhost/web/index.php returns '/web'
* * http://localhost/we%20b/index.php returns '/we%20b'
*
* @return string
* @return string The raw path (i.e. not urldecoded)
*
* @api
*/
......@@ -613,7 +750,7 @@ class Request
* This is similar to getBasePath(), except that it also includes the
* script filename (e.g. index.php) if one exists.
*
* @return string
* @return string The raw url (i.e. not urldecoded)
*
* @api
*/
......@@ -641,17 +778,25 @@ class Request
/**
* Returns the port on which the request is made.
*
* This method can read the client port from the "X-Forwarded-Port" header
* when trusted proxies were set via "setTrustedProxies()".
*
* The "X-Forwarded-Port" header must contain the client port.
*
* If your reverse proxy uses a different header name than "X-Forwarded-Port",
* configure it via "setTrustedHeaderName()" with the "client-port" key.
*
* @return string
*
* @api
*/
public function getPort()
{
if (self::$trustProxy && $this->headers->has('X-Forwarded-Port')) {
return $this->headers->get('X-Forwarded-Port');
} else {
return $this->server->get('SERVER_PORT');
if (self::$trustProxy && self::$trustedHeaders[self::HEADER_CLIENT_PORT] && $port = $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_PORT])) {
return $port;
}
return $this->server->get('SERVER_PORT');
}
/**
......@@ -674,6 +819,23 @@ class Request
return $this->server->get('PHP_AUTH_PW');
}
/**
* Gets the user info.
*
* @return string A user name and, optionally, scheme-specific information about how to gain authorization to access the server
*/
public function getUserInfo()
{
$userinfo = $this->getUser();
$pass = $this->getPassword();
if ('' != $pass) {
$userinfo .= ":$pass";
}
return $userinfo;
}
/**
* Returns the HTTP host being requested.
*
......@@ -698,7 +860,7 @@ class Request
/**
* Returns the requested URI.
*
* @return string
* @return string The raw URI (i.e. not urldecoded)
*
* @api
*/
......@@ -711,6 +873,19 @@ class Request
return $this->requestUri;
}
/**
* Gets the scheme and HTTP host.
*
* If the URL was called with basic authentication, the user
* and the password are not added to the generated string.
*
* @return string The scheme and HTTP host
*/
public function getSchemeAndHttpHost()
{
return $this->getScheme().'://'.$this->getHttpHost();
}
/**
* Generates a normalized URI for the Request.
*
......@@ -727,20 +902,7 @@ class Request
$qs = '?'.$qs;
}
$auth = '';
if ($user = $this->getUser()) {
$auth = $user;
}
if ($pass = $this->getPassword()) {
$auth .= ":$pass";
}
if ('' !== $auth) {
$auth .= '@';
}
return $this->getScheme().'://'.$auth.$this->getHttpHost().$this->getBaseUrl().$this->getPathInfo().$qs;
return $this->getSchemeAndHttpHost().$this->getBaseUrl().$this->getPathInfo().$qs;
}
/**
......@@ -754,7 +916,7 @@ class Request
*/
public function getUriForPath($path)
{
return $this->getScheme().'://'.$this->getHttpHost().$this->getBaseUrl().$path;
return $this->getSchemeAndHttpHost().$this->getBaseUrl().$path;
}
/**
......@@ -769,71 +931,76 @@ class Request
*/
public function getQueryString()
{
if (!$qs = $this->server->get('QUERY_STRING')) {
return null;
}
$parts = array();
$order = array();
foreach (explode('&', $qs) as $segment) {
if (false === strpos($segment, '=')) {
$parts[] = $segment;
$order[] = $segment;
} else {
$tmp = explode('=', rawurldecode($segment), 2);
$parts[] = rawurlencode($tmp[0]).'='.rawurlencode($tmp[1]);
$order[] = $tmp[0];
}
}
array_multisort($order, SORT_ASC, $parts);
$qs = static::normalizeQueryString($this->server->get('QUERY_STRING'));
return implode('&', $parts);
return '' === $qs ? null : $qs;
}
/**
* Checks whether the request is secure or not.
*
* This method can read the client port from the "X-Forwarded-Proto" header
* when trusted proxies were set via "setTrustedProxies()".
*
* The "X-Forwarded-Proto" header must contain the protocol: "https" or "http".
*
* If your reverse proxy uses a different header name than "X-Forwarded-Proto"
* ("SSL_HTTPS" for instance), configure it via "setTrustedHeaderName()" with
* the "client-proto" key.
*
* @return Boolean
*
* @api
*/
public function isSecure()
{
return (
(strtolower($this->server->get('HTTPS')) == 'on' || $this->server->get('HTTPS') == 1)
||
(self::$trustProxy && strtolower($this->headers->get('SSL_HTTPS')) == 'on' || $this->headers->get('SSL_HTTPS') == 1)
||
(self::$trustProxy && strtolower($this->headers->get('X_FORWARDED_PROTO')) == 'https')
);
if (self::$trustProxy && self::$trustedHeaders[self::HEADER_CLIENT_PROTO] && $proto = $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_PROTO])) {
return in_array(strtolower($proto), array('https', 'on', '1'));
}
return 'on' == strtolower($this->server->get('HTTPS')) || 1 == $this->server->get('HTTPS');
}
/**
* Returns the host name.
*
* This method can read the client port from the "X-Forwarded-Host" header
* when trusted proxies were set via "setTrustedProxies()".
*
* The "X-Forwarded-Host" header must contain the client host name.
*
* If your reverse proxy uses a different header name than "X-Forwarded-Host",
* configure it via "setTrustedHeaderName()" with the "client-host" key.
*
* @return string
*
* @throws \UnexpectedValueException when the host name is invalid
*
* @api
*/
public function getHost()
{
if (self::$trustProxy && $host = $this->headers->get('X_FORWARDED_HOST')) {
if (self::$trustProxy && self::$trustedHeaders[self::HEADER_CLIENT_HOST] && $host = $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_HOST])) {
$elements = explode(',', $host);
$host = trim($elements[count($elements) - 1]);
} else {
if (!$host = $this->headers->get('HOST')) {
if (!$host = $this->server->get('SERVER_NAME')) {
$host = $this->server->get('SERVER_ADDR', '');
}
$host = $elements[count($elements) - 1];
} elseif (!$host = $this->headers->get('HOST')) {
if (!$host = $this->server->get('SERVER_NAME')) {
$host = $this->server->get('SERVER_ADDR', '');
}
}
// Remove port number from host
$host = preg_replace('/:\d+$/', '', $host);
// trim and remove port number from host
// host is lowercase as per RFC 952/2181
$host = strtolower(preg_replace('/:\d+$/', '', trim($host)));
// as the host can come from the user (HTTP_HOST and depending on the configuration, SERVER_NAME too can come from the user)
// check that it does not contain forbidden characters (see RFC 952 and RFC 2181)
if ($host && !preg_match('/^\[?(?:[a-zA-Z0-9-:\]_]+\.?)+$/', $host)) {
throw new \UnexpectedValueException('Invalid Host');
}
return trim($host);
return $host;
}
/**
......@@ -863,7 +1030,7 @@ class Request
if (null === $this->method) {
$this->method = strtoupper($this->server->get('REQUEST_METHOD', 'GET'));
if ('POST' === $this->method) {
$this->method = strtoupper($this->headers->get('X-HTTP-METHOD-OVERRIDE', $this->request->get('_method', 'POST')));
$this->method = strtoupper($this->headers->get('X-HTTP-METHOD-OVERRIDE', $this->request->get('_method', $this->query->get('_method', 'POST'))));
}
}
......@@ -873,7 +1040,7 @@ class Request
/**
* Gets the mime type associated with the format.
*
* @param string $format The format
* @param string $format The format
*
* @return string The associated mime type (null if not found)
*
......@@ -891,9 +1058,9 @@ class Request
/**
* Gets the format associated with the mime type.
*
* @param string $mimeType The associated mime type
* @param string $mimeType The associated mime type
*
* @return string The format (null if not found)
* @return string|null The format (null if not found)
*
* @api
*/
......@@ -919,8 +1086,8 @@ class Request
/**
* Associates a format with mime types.
*
* @param string $format The format
* @param string|array $mimeTypes The associated mime types (the preferred one must be the first as it will be used as the content type)
* @param string $format The format
* @param string|array $mimeTypes The associated mime types (the preferred one must be the first as it will be used as the content type)
*
* @api
*/
......@@ -942,7 +1109,7 @@ class Request
* * _format request parameter
* * $default
*
* @param string $default The default format
* @param string $default The default format
*
* @return string The request format
*
......@@ -972,13 +1139,13 @@ class Request
/**
* Gets the format associated with the request.
*
* @return string The format (null if no content type is present)
* @return string|null The format (null if no content type is present)
*
* @api
*/
public function getContentType()
{
return $this->getFormat($this->server->get('CONTENT_TYPE'));
return $this->getFormat($this->headers->get('CONTENT_TYPE'));
}
/**
......@@ -990,7 +1157,11 @@ class Request
*/
public function setDefaultLocale($locale)
{
$this->setPhpDefaultLocale($this->defaultLocale = $locale);
$this->defaultLocale = $locale;
if (null === $this->locale) {
$this->setPhpDefaultLocale($locale);
}
}
/**
......@@ -1015,6 +1186,18 @@ class Request
return null === $this->locale ? $this->defaultLocale : $this->locale;
}
/**
* Checks if the request method is of specified type.
*
* @param string $method Uppercase request method (GET, POST etc).
*
* @return Boolean
*/
public function isMethod($method)
{
return $this->getMethod() === strtoupper($method);
}
/**
* Checks whether the method is safe or not.
*
......@@ -1030,7 +1213,7 @@ class Request
/**
* Returns the request body content.
*
* @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.
*/
......@@ -1063,6 +1246,9 @@ class Request
return preg_split('/\s*,\s*/', $this->headers->get('if_none_match'), null, PREG_SPLIT_NO_EMPTY);
}
/**
* @return Boolean
*/
public function isNoCache()
{
return $this->headers->hasCacheControlDirective('no-cache') || 'no-cache' == $this->headers->get('Pragma');
......@@ -1071,7 +1257,7 @@ class Request
/**
* Returns the preferred language.
*
* @param array $locales An array of ordered available locales
* @param array $locales An array of ordered available locales
*
* @return string|null The preferred locale
*
......@@ -1186,7 +1372,7 @@ class Request
/**
* Splits an Accept-* HTTP header.
*
* @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
*/
......@@ -1197,23 +1383,31 @@ class Request
}
$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 = (float) substr(trim($match[1]), 2);
$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) {
$q = (float) $q;
if (0 < $q) {
$values[trim($value)] = $q;
foreach ($items as $value) {
$values[trim($value)] = $q;
}
}
}
arsort($values);
reset($values);
return $values;
}
......@@ -1229,8 +1423,11 @@ class Request
{
$requestUri = '';
if ($this->headers->has('X_REWRITE_URL') && false !== stripos(PHP_OS, 'WIN')) {
// check this first so IIS will catch
if ($this->headers->has('X_ORIGINAL_URL') && false !== stripos(PHP_OS, 'WIN')) {
// IIS with Microsoft Rewrite Module
$requestUri = $this->headers->get('X_ORIGINAL_URL');
} elseif ($this->headers->has('X_REWRITE_URL') && false !== stripos(PHP_OS, 'WIN')) {
// IIS with ISAPI_Rewrite
$requestUri = $this->headers->get('X_REWRITE_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)
......@@ -1238,14 +1435,14 @@ class Request
} 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->getScheme().'://'.$this->getHttpHost();
$schemeAndHttpHost = $this->getSchemeAndHttpHost();
if (strpos($requestUri, $schemeAndHttpHost) === 0) {
$requestUri = substr($requestUri, strlen($schemeAndHttpHost));
}
} elseif ($this->server->has('ORIG_PATH_INFO')) {
// IIS 5.0, PHP as CGI
$requestUri = $this->server->get('ORIG_PATH_INFO');
if ($this->server->get('QUERY_STRING')) {
if ('' != $this->server->get('QUERY_STRING')) {
$requestUri .= '?'.$this->server->get('QUERY_STRING');
}
}
......@@ -1288,14 +1485,14 @@ class Request
// Does the baseUrl have anything in common with the request_uri?
$requestUri = $this->getRequestUri();
if ($baseUrl && 0 === strpos($requestUri, $baseUrl)) {
if ($baseUrl && false !== $prefix = $this->getUrlencodedPrefix($requestUri, $baseUrl)) {
// full $baseUrl matches
return $baseUrl;
return $prefix;
}
if ($baseUrl && 0 === strpos($requestUri, dirname($baseUrl))) {
if ($baseUrl && false !== $prefix = $this->getUrlencodedPrefix($requestUri, dirname($baseUrl))) {
// directory portion of $baseUrl matches
return rtrim(dirname($baseUrl), '/');
return rtrim($prefix, '/');
}
$truncatedRequestUri = $requestUri;
......@@ -1304,7 +1501,7 @@ class Request
}
$basename = basename($baseUrl);
if (empty($basename) || !strpos($truncatedRequestUri, $basename)) {
if (empty($basename) || !strpos(rawurldecode($truncatedRequestUri), $basename)) {
// no match whatsoever; set it blank
return '';
}
......@@ -1365,7 +1562,7 @@ class Request
$requestUri = substr($requestUri, 0, $pos);
}
if ((null !== $baseUrl) && (false === ($pathInfo = substr(urldecode($requestUri), strlen(urldecode($baseUrl)))))) {
if ((null !== $baseUrl) && (false === ($pathInfo = substr($requestUri, strlen($baseUrl))))) {
// If substr() returns false then PATH_INFO is set to an empty string
return '/';
} elseif (null === $baseUrl) {
......@@ -1378,7 +1575,7 @@ class Request
/**
* Initializes HTTP request formats.
*/
static protected function initializeFormats()
protected static function initializeFormats()
{
static::$formats = array(
'html' => array('text/html', 'application/xhtml+xml'),
......@@ -1410,4 +1607,28 @@ class Request
} catch (\Exception $e) {
}
}
/*
* Returns the prefix as encoded in the string when the string starts with
* the given prefix, false otherwise.
*
* @param string $string The urlencoded string
* @param string $prefix The prefix not encoded
*
* @return string|false The prefix as it is encoded in $string, or false
*/
private function getUrlencodedPrefix($string, $prefix)
{
if (0 !== strpos(rawurldecode($string), $prefix)) {
return false;
}
$len = strlen($prefix);
if (preg_match("#^(%[[:xdigit:]]{2}|.){{$len}}#", $string, $match)) {
return $match[0];
}
return false;
}
}
......@@ -31,9 +31,9 @@ class RequestMatcher implements RequestMatcherInterface
private $host;
/**
* @var string
* @var array
*/
private $methods;
private $methods = array();
/**
* @var string
......@@ -41,19 +41,26 @@ class RequestMatcher implements RequestMatcherInterface
private $ip;
/**
* Attributes.
*
* @var array
*/
private $attributes;
private $attributes = array();
/**
* @param string|null $path
* @param string|null $host
* @param string|string[]|null $methods
* @param string|null $ip
* @param array $attributes
*/
public function __construct($path = null, $host = null, $methods = null, $ip = null, array $attributes = array())
{
$this->path = $path;
$this->host = $host;
$this->methods = $methods;
$this->ip = $ip;
$this->attributes = $attributes;
$this->matchPath($path);
$this->matchHost($host);
$this->matchMethod($methods);
$this->matchIp($ip);
foreach ($attributes as $k => $v) {
$this->matchAttribute($k, $v);
}
}
/**
......@@ -89,11 +96,11 @@ class RequestMatcher implements RequestMatcherInterface
/**
* Adds a check for the HTTP method.
*
* @param string|array $method An HTTP method or an array of HTTP methods
* @param string|string[]|null $method An HTTP method or an array of HTTP methods
*/
public function matchMethod($method)
{
$this->methods = array_map('strtoupper', is_array($method) ? $method : array($method));
$this->methods = array_map('strtoupper', (array) $method);
}
/**
......@@ -114,7 +121,7 @@ class RequestMatcher implements RequestMatcherInterface
*/
public function matches(Request $request)
{
if (null !== $this->methods && !in_array($request->getMethod(), $this->methods)) {
if ($this->methods && !in_array($request->getMethod(), $this->methods)) {
return false;
}
......@@ -127,12 +134,12 @@ class RequestMatcher implements RequestMatcherInterface
if (null !== $this->path) {
$path = str_replace('#', '\\#', $this->path);
if (!preg_match('#'.$path.'#', $request->getPathInfo())) {
if (!preg_match('#'.$path.'#', rawurldecode($request->getPathInfo()))) {
return false;
}
}
if (null !== $this->host && !preg_match('#'.str_replace('#', '\\#', $this->host).'#', $request->getHost())) {
if (null !== $this->host && !preg_match('#'.str_replace('#', '\\#', $this->host).'#i', $request->getHost())) {
return false;
}
......@@ -198,11 +205,20 @@ class RequestMatcher implements RequestMatcherInterface
*/
protected function checkIp6($requestIp, $ip)
{
if (!defined('AF_INET6')) {
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".');
}
list($address, $netmask) = explode('/', $ip, 2);
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));
......
......@@ -23,11 +23,11 @@ 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
* @param Request $request The request to check for a match
*
* @return Boolean true if the request matches, false otherwise
*
* @api
*/
function matches(Request $request);
public function matches(Request $request);
}
......@@ -12,7 +12,7 @@
/**
* SessionHandlerInterface
*
* Provides forward compatability with PHP 5.4
* Provides forward compatibility with PHP 5.4
*
* Extensive documentation can be found at php.net, see links:
*
......@@ -36,7 +36,7 @@ interface SessionHandlerInterface
*
* @return boolean
*/
function open($savePath, $sessionName);
public function open($savePath, $sessionName);
/**
* Close session.
......@@ -45,18 +45,20 @@ interface SessionHandlerInterface
*
* @return boolean
*/
function close();
public function close();
/**
* Read session.
*
* @param string $sessionId
*
* @see http://php.net/sessionhandlerinterface.read
*
* @throws \RuntimeException On fatal error but not "record not found".
*
* @return string String as stored in persistent storage or empty string in all other cases.
*/
function read($sessionId);
public function read($sessionId);
/**
* Commit session to storage.
......@@ -68,7 +70,7 @@ interface SessionHandlerInterface
*
* @return boolean
*/
function write($sessionId, $data);
public function write($sessionId, $data);
/**
* Destroys this session.
......@@ -81,7 +83,7 @@ interface SessionHandlerInterface
*
* @return boolean
*/
function destroy($sessionId);
public function destroy($sessionId);
/**
* Garbage collection for storage.
......@@ -94,5 +96,5 @@ interface SessionHandlerInterface
*
* @return boolean
*/
function gc($lifetime);
public function gc($lifetime);
}
......@@ -61,7 +61,7 @@ class Response
*
* @var array
*/
static public $statusTexts = array(
public static $statusTexts = array(
100 => 'Continue',
101 => 'Switching Protocols',
102 => 'Processing', // RFC2518
......@@ -83,6 +83,7 @@ class Response
305 => 'Use Proxy',
306 => 'Reserved',
307 => 'Temporary Redirect',
308 => 'Permanent Redirect', // RFC-reschke-http-status-308-07
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
......@@ -101,26 +102,26 @@ class Response
415 => 'Unsupported Media Type',
416 => 'Requested Range Not Satisfiable',
417 => 'Expectation Failed',
418 => 'I\'m a teapot',
422 => 'Unprocessable Entity', // RFC4918
423 => 'Locked', // RFC4918
424 => 'Failed Dependency', // RFC4918
418 => 'I\'m a teapot', // RFC2324
422 => 'Unprocessable Entity', // RFC4918
423 => 'Locked', // RFC4918
424 => 'Failed Dependency', // RFC4918
425 => 'Reserved for WebDAV advanced collections expired proposal', // RFC2817
426 => 'Upgrade Required', // RFC2817
428 => 'Precondition Required', // RFC-nottingham-http-new-status-04
429 => 'Too Many Requests', // RFC-nottingham-http-new-status-04
431 => 'Request Header Fields Too Large', // RFC-nottingham-http-new-status-04
426 => 'Upgrade Required', // RFC2817
428 => 'Precondition Required', // RFC6585
429 => 'Too Many Requests', // RFC6585
431 => 'Request Header Fields Too Large', // RFC6585
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported',
506 => 'Variant Also Negotiates (Experimental)', // [RFC2295]
507 => 'Insufficient Storage', // RFC4918
508 => 'Loop Detected', // RFC5842
510 => 'Not Extended', // RFC2774
511 => 'Network Authentication Required', // RFC-nottingham-http-new-status-04
506 => 'Variant Also Negotiates (Experimental)', // RFC2295
507 => 'Insufficient Storage', // RFC4918
508 => 'Loop Detected', // RFC5842
510 => 'Not Extended', // RFC2774
511 => 'Network Authentication Required', // RFC6585
);
/**
......@@ -157,7 +158,7 @@ class Response
*
* @return Response
*/
static public function create($content = '', $status = 200, $headers = array())
public static function create($content = '', $status = 200, $headers = array())
{
return new static($content, $status, $headers);
}
......@@ -165,7 +166,7 @@ class Response
/**
* Returns the Response as an HTTP string.
*
* The string representation of the Resonse is the same as the
* The string representation of the Response is the same as the
* one that will be sent to the client only if the prepare() method
* has been called before.
*
......@@ -197,13 +198,15 @@ class Response
* the Request that is "associated" with this Response.
*
* @param Request $request A Request instance
*
* @return Response The current response.
*/
public function prepare(Request $request)
{
$headers = $this->headers;
if ($this->isInformational() || in_array($this->statusCode, array(204, 304))) {
$this->setContent('');
$this->setContent(null);
}
// Content-type based on the Request
......@@ -231,11 +234,24 @@ class Response
if ('HEAD' === $request->getMethod()) {
// cf. RFC2616 14.13
$length = $headers->get('Content-Length');
$this->setContent('');
$this->setContent(null);
if ($length) {
$headers->set('Content-Length', $length);
}
}
// Fix protocol
if ('HTTP/1.0' != $request->server->get('SERVER_PROTOCOL')) {
$this->setProtocolVersion('1.1');
}
// Check if we need to send extra expire info headers
if ('1.0' == $this->getProtocolVersion() && 'no-cache' == $this->headers->get('Cache-Control')) {
$this->headers->set('pragma', 'no-cache');
$this->headers->set('expires', -1);
}
return $this;
}
/**
......@@ -294,6 +310,18 @@ class Response
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
} elseif ('cli' !== PHP_SAPI) {
// ob_get_level() never returns 0 on some Windows configurations, so if
// the level is the same two times in a row, the loop should be stopped.
$previous = null;
$obStatus = ob_get_status(1);
while (($level = ob_get_level()) > 0 && $level !== $previous) {
$previous = $level;
if ($obStatus[$level - 1] && isset($obStatus[$level - 1]['del']) && $obStatus[$level - 1]['del']) {
ob_end_flush();
}
}
flush();
}
return $this;
......@@ -365,7 +393,10 @@ class Response
* Sets the response status code.
*
* @param integer $code HTTP status code
* @param string $text HTTP status text
* @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.
*
* @return Response
*
......@@ -375,12 +406,24 @@ class Response
*/
public function setStatusCode($code, $text = null)
{
$this->statusCode = (int) $code;
$this->statusCode = $code = (int) $code;
if ($this->isInvalid()) {
throw new \InvalidArgumentException(sprintf('The HTTP status code "%s" is not valid.', $code));
}
$this->statusText = false === $text ? '' : (null === $text ? self::$statusTexts[$this->statusCode] : $text);
if (null === $text) {
$this->statusText = isset(self::$statusTexts[$code]) ? self::$statusTexts[$code] : '';
return $this;
}
if (false === $text) {
$this->statusText = '';
return $this;
}
$this->statusText = $text;
return $this;
}
......@@ -528,7 +571,7 @@ class Response
*/
public function mustRevalidate()
{
return $this->headers->hasCacheControlDirective('must-revalidate') || $this->headers->has('must-proxy-revalidate');
return $this->headers->hasCacheControlDirective('must-revalidate') || $this->headers->has('proxy-revalidate');
}
/**
......@@ -542,7 +585,7 @@ class Response
*/
public function getDate()
{
return $this->headers->getDate('Date');
return $this->headers->getDate('Date', new \DateTime());
}
/**
......@@ -701,7 +744,7 @@ class Response
* When the responses TTL is <= 0, the response may not be served from cache without first
* revalidating with the origin.
*
* @return integer The TTL in seconds
* @return integer|null The TTL in seconds
*
* @api
*/
......@@ -960,6 +1003,10 @@ class Response
*/
public function isNotModified(Request $request)
{
if (!$request->isMethodSafe()) {
return false;
}
$lastModified = $request->headers->get('If-Modified-Since');
$notModified = false;
if ($etags = $request->getEtags()) {
......@@ -1095,7 +1142,7 @@ class Response
*/
public function isRedirect($location = null)
{
return in_array($this->statusCode, array(201, 301, 302, 303, 307)) && (null === $location ?: $location == $this->headers->get('Location'));
return in_array($this->statusCode, array(201, 301, 302, 303, 307, 308)) && (null === $location ?: $location == $this->headers->get('Location'));
}
/**
......
......@@ -231,7 +231,7 @@ class ResponseHeaderBag extends HeaderBag
throw new \InvalidArgumentException(sprintf('The disposition must be either "%s" or "%s".', self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE));
}
if (!$filenameFallback) {
if ('' == $filenameFallback) {
$filenameFallback = $filename;
}
......@@ -246,14 +246,14 @@ class ResponseHeaderBag extends HeaderBag
}
// path separators aren't allowed in either.
if (preg_match('#[/\\\\]#', $filename) || preg_match('#[/\\\\]#', $filenameFallback)) {
if (false !== strpos($filename, '/') || false !== strpos($filename, '\\') || false !== strpos($filenameFallback, '/') || false !== strpos($filenameFallback, '\\')) {
throw new \InvalidArgumentException('The filename and the fallback cannot contain the "/" and "\\" characters.');
}
$output = sprintf('%s; filename="%s"', $disposition, str_replace(array('\\', '"'), array('\\\\', '\\"'), $filenameFallback));
$output = sprintf('%s; filename="%s"', $disposition, str_replace('"', '\\"', $filenameFallback));
if ($filename != $filenameFallback) {
$output .= sprintf("; filename*=utf-8''%s", str_replace(array("'", '(', ')', '*'), array('%27', '%28', '%29', '%2A'), urlencode($filename)));
if ($filename !== $filenameFallback) {
$output .= sprintf("; filename*=utf-8''%s", rawurlencode($filename));
}
return $output;
......
......@@ -16,13 +16,14 @@ namespace Symfony\Component\HttpFoundation;
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Bulat Shakirzyanov <mallluhuct@gmail.com>
* @author Robert Kiss <kepten@gmail.com>
*/
class ServerBag extends ParameterBag
{
/**
* Gets the HTTP headers.
*
* @return string
* @return array
*/
public function getHeaders()
{
......@@ -33,14 +34,47 @@ class ServerBag extends ParameterBag
}
// CONTENT_* are not prefixed with HTTP_
elseif (in_array($key, array('CONTENT_LENGTH', 'CONTENT_MD5', 'CONTENT_TYPE'))) {
$headers[$key] = $this->parameters[$key];
$headers[$key] = $value;
}
}
// PHP_AUTH_USER/PHP_AUTH_PW
if (isset($this->parameters['PHP_AUTH_USER'])) {
$pass = isset($this->parameters['PHP_AUTH_PW']) ? $this->parameters['PHP_AUTH_PW'] : '';
$headers['AUTHORIZATION'] = 'Basic '.base64_encode($this->parameters['PHP_AUTH_USER'].':'.$pass);
$headers['PHP_AUTH_USER'] = $this->parameters['PHP_AUTH_USER'];
$headers['PHP_AUTH_PW'] = isset($this->parameters['PHP_AUTH_PW']) ? $this->parameters['PHP_AUTH_PW'] : '';
} else {
/*
* php-cgi under Apache does not pass HTTP Basic user/pass to PHP by default
* For this workaround to work, add these lines to your .htaccess file:
* RewriteCond %{HTTP:Authorization} ^(.+)$
* RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
*
* A sample .htaccess file:
* RewriteEngine On
* RewriteCond %{HTTP:Authorization} ^(.+)$
* RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
* RewriteCond %{REQUEST_FILENAME} !-f
* RewriteRule ^(.*)$ app.php [QSA,L]
*/
$authorizationHeader = null;
if (isset($this->parameters['HTTP_AUTHORIZATION'])) {
$authorizationHeader = $this->parameters['HTTP_AUTHORIZATION'];
} elseif (isset($this->parameters['REDIRECT_HTTP_AUTHORIZATION'])) {
$authorizationHeader = $this->parameters['REDIRECT_HTTP_AUTHORIZATION'];
}
// Decode AUTHORIZATION header into PHP_AUTH_USER and PHP_AUTH_PW when authorization header is basic
if ((null !== $authorizationHeader) && (0 === stripos($authorizationHeader, 'basic'))) {
$exploded = explode(':', base64_decode(substr($authorizationHeader, 6)));
if (count($exploded) == 2) {
list($headers['PHP_AUTH_USER'], $headers['PHP_AUTH_PW']) = $exploded;
}
}
}
// PHP_AUTH_USER/PHP_AUTH_PW
if (isset($headers['PHP_AUTH_USER'])) {
$headers['AUTHORIZATION'] = 'Basic '.base64_encode($headers['PHP_AUTH_USER'].':'.$headers['PHP_AUTH_PW']);
}
return $headers;
......
......@@ -14,7 +14,7 @@ namespace Symfony\Component\HttpFoundation\Session\Attribute;
/**
* This class relates to session attribute storage
*/
class AttributeBag implements AttributeBagInterface
class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Countable
{
private $name = 'attributes';
......@@ -134,4 +134,24 @@ class AttributeBag implements AttributeBagInterface
return $return;
}
/**
* Returns an iterator for attributes.
*
* @return \ArrayIterator An \ArrayIterator instance
*/
public function getIterator()
{
return new \ArrayIterator($this->attributes);
}
/**
* Returns the number of attributes.
*
* @return int The number of attributes
*/
public function count()
{
return count($this->attributes);
}
}
......@@ -27,7 +27,7 @@ interface AttributeBagInterface extends SessionBagInterface
*
* @return Boolean true if the attribute is defined, false otherwise
*/
function has($name);
public function has($name);
/**
* Returns an attribute.
......@@ -37,7 +37,7 @@ interface AttributeBagInterface extends SessionBagInterface
*
* @return mixed
*/
function get($name, $default = null);
public function get($name, $default = null);
/**
* Sets an attribute.
......@@ -45,21 +45,21 @@ interface AttributeBagInterface extends SessionBagInterface
* @param string $name
* @param mixed $value
*/
function set($name, $value);
public function set($name, $value);
/**
* Returns attributes.
*
* @return array Attributes
*/
function all();
public function all();
/**
* Sets attributes.
*
* @param array $attributes Attributes
*/
function replace(array $attributes);
public function replace(array $attributes);
/**
* Removes an attribute.
......@@ -68,5 +68,5 @@ interface AttributeBagInterface extends SessionBagInterface
*
* @return mixed The removed value
*/
function remove($name);
public function remove($name);
}
......@@ -75,7 +75,15 @@ class AutoExpireFlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function peek($type, $default = null)
public function add($type, $message)
{
$this->flashes['new'][$type][] = $message;
}
/**
* {@inheritdoc}
*/
public function peek($type, array $default = array())
{
return $this->has($type) ? $this->flashes['display'][$type] : $default;
}
......@@ -85,13 +93,13 @@ class AutoExpireFlashBag implements FlashBagInterface
*/
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'] : array();
}
/**
* {@inheritdoc}
*/
public function get($type, $default = null)
public function get($type, array $default = array())
{
$return = $default;
......@@ -129,9 +137,9 @@ class AutoExpireFlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function set($type, $message)
public function set($type, $messages)
{
$this->flashes['new'][$type] = $message;
$this->flashes['new'][$type] = (array) $messages;
}
/**
......@@ -139,7 +147,7 @@ class AutoExpireFlashBag implements FlashBagInterface
*/
public function has($type)
{
return array_key_exists($type, $this->flashes['display']);
return array_key_exists($type, $this->flashes['display']) && $this->flashes['display'][$type];
}
/**
......@@ -163,9 +171,6 @@ class AutoExpireFlashBag implements FlashBagInterface
*/
public function clear()
{
$return = $this->all();
$this->flashes = array('display' => array(), 'new' => array());
return $return;
return $this->all();
}
}
......@@ -16,7 +16,7 @@ namespace Symfony\Component\HttpFoundation\Session\Flash;
*
* @author Drak <drak@zikula.org>
*/
class FlashBag implements FlashBagInterface
class FlashBag implements FlashBagInterface, \IteratorAggregate, \Countable
{
private $name = 'flashes';
......@@ -68,7 +68,15 @@ class FlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function peek($type, $default = null)
public function add($type, $message)
{
$this->flashes[$type][] = $message;
}
/**
* {@inheritdoc}
*/
public function peek($type, array $default =array())
{
return $this->has($type) ? $this->flashes[$type] : $default;
}
......@@ -84,7 +92,7 @@ class FlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function get($type, $default = null)
public function get($type, array $default = array())
{
if (!$this->has($type)) {
return $default;
......@@ -111,9 +119,9 @@ class FlashBag implements FlashBagInterface
/**
* {@inheritdoc}
*/
public function set($type, $message)
public function set($type, $messages)
{
$this->flashes[$type] = $message;
$this->flashes[$type] = (array) $messages;
}
/**
......@@ -129,7 +137,7 @@ class FlashBag implements FlashBagInterface
*/
public function has($type)
{
return array_key_exists($type, $this->flashes);
return array_key_exists($type, $this->flashes) && $this->flashes[$type];
}
/**
......@@ -155,4 +163,24 @@ class FlashBag implements FlashBagInterface
{
return $this->all();
}
/**
* Returns an iterator for flashes.
*
* @return \ArrayIterator An \ArrayIterator instance
*/
public function getIterator()
{
return new \ArrayIterator($this->all());
}
/**
* Returns the number of flashes.
*
* @return int The number of flashes
*/
public function count()
{
return count($this->flashes);
}
}
......@@ -21,51 +21,59 @@ use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
interface FlashBagInterface extends SessionBagInterface
{
/**
* Registers a message for a given type.
* Adds a flash message for type.
*
* @param string $type
* @param string $message
*/
function set($type, $message);
public function add($type, $message);
/**
* Registers a message for a given type.
*
* @param string $type
* @param string|array $message
*/
public function set($type, $message);
/**
* Gets flash message for a given type.
* Gets flash messages for a given type.
*
* @param string $type Message category type.
* @param string $default Default value if $type doee not exist.
* @param array $default Default value if $type does not exist.
*
* @return string
* @return array
*/
function peek($type, $default = null);
public function peek($type, array $default = array());
/**
* Gets all flash messages.
*
* @return array
*/
function peekAll();
public function peekAll();
/**
* Gets and clears flash from the stack.
*
* @param string $type
* @param string $default Default value if $type doee not exist.
* @param array $default Default value if $type does not exist.
*
* @return string
* @return array
*/
function get($type, $default = null);
public function get($type, array $default = array());
/**
* Gets and clears flashes from the stack.
*
* @return array
*/
function all();
public function all();
/**
* Sets all flash messages.
*/
function setAll(array $messages);
public function setAll(array $messages);
/**
* Has flash messages for a given type?
......@@ -74,12 +82,12 @@ interface FlashBagInterface extends SessionBagInterface
*
* @return boolean
*/
function has($type);
public function has($type);
/**
* Returns a list of all defined types.
*
* @return array
*/
function keys();
public function keys();
}
......@@ -27,7 +27,7 @@ use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
*
* @api
*/
class Session implements SessionInterface
class Session implements SessionInterface, \IteratorAggregate, \Countable
{
/**
* Storage driver.
......@@ -57,13 +57,13 @@ class Session implements SessionInterface
{
$this->storage = $storage ?: new NativeSessionStorage();
$attributeBag = $attributes ?: new AttributeBag();
$this->attributeName = $attributeBag->getName();
$this->registerBag($attributeBag);
$attributes = $attributes ?: new AttributeBag();
$this->attributeName = $attributes->getName();
$this->registerBag($attributes);
$flashBag = $flashes ?: new FlashBag();
$this->flashName = $flashBag->getName();
$this->registerBag($flashBag);
$flashes = $flashes ?: new FlashBag();
$this->flashName = $flashes->getName();
$this->registerBag($flashes);
}
/**
......@@ -133,19 +133,47 @@ class Session implements SessionInterface
/**
* {@inheritdoc}
*/
public function invalidate()
public function isStarted()
{
return $this->storage->isStarted();
}
/**
* Returns an iterator for attributes.
*
* @return \ArrayIterator An \ArrayIterator instance
*/
public function getIterator()
{
return new \ArrayIterator($this->storage->getBag($this->attributeName)->all());
}
/**
* Returns the number of attributes.
*
* @return int The number of attributes
*/
public function count()
{
return count($this->storage->getBag($this->attributeName)->all());
}
/**
* {@inheritdoc}
*/
public function invalidate($lifetime = null)
{
$this->storage->clear();
return $this->storage->regenerate(true);
return $this->migrate(true, $lifetime);
}
/**
* {@inheritdoc}
*/
public function migrate($destroy = false)
public function migrate($destroy = false, $lifetime = null)
{
return $this->storage->regenerate($destroy);
return $this->storage->regenerate($destroy, $lifetime);
}
/**
......@@ -189,9 +217,15 @@ class Session implements SessionInterface
}
/**
* Registers a SessionBagInterface with the session.
*
* @param SessionBagInterface $bag
* {@inheritdoc}
*/
public function getMetadataBag()
{
return $this->storage->getMetadataBag();
}
/**
* {@inheritdoc}
*/
public function registerBag(SessionBagInterface $bag)
{
......@@ -199,11 +233,7 @@ class Session implements SessionInterface
}
/**
* Get's a bag instance.
*
* @param string $name
*
* @return SessionBagInterface
* {@inheritdoc}
*/
public function getBag($name)
{
......@@ -229,7 +259,20 @@ class Session implements SessionInterface
*/
public function getFlashes()
{
return $this->getBag('flashes')->all();
$all = $this->getBag($this->flashName)->all();
$return = array();
if ($all) {
foreach ($all as $name => $array) {
if (is_numeric(key($array))) {
$return[$name] = reset($array);
} else {
$return[$name] = $array;
}
}
}
return $return;
}
/**
......@@ -239,7 +282,9 @@ class Session implements SessionInterface
*/
public function setFlashes($values)
{
$this->getBag('flashes')->setAll($values);
foreach ($values as $name => $value) {
$this->getBag($this->flashName)->set($name, $value);
}
}
/**
......@@ -252,7 +297,9 @@ class Session implements SessionInterface
*/
public function getFlash($name, $default = null)
{
return $this->getBag('flashes')->get($name, $default);
$return = $this->getBag($this->flashName)->get($name);
return empty($return) ? $default : reset($return);
}
/**
......@@ -263,7 +310,7 @@ class Session implements SessionInterface
*/
public function setFlash($name, $value)
{
$this->getBag('flashes')->set($name, $value);
$this->getBag($this->flashName)->set($name, $value);
}
/**
......@@ -275,7 +322,7 @@ class Session implements SessionInterface
*/
public function hasFlash($name)
{
return $this->getBag('flashes')->has($name);
return $this->getBag($this->flashName)->has($name);
}
/**
......@@ -285,7 +332,7 @@ class Session implements SessionInterface
*/
public function removeFlash($name)
{
$this->getBag('flashes')->get($name);
$this->getBag($this->flashName)->get($name);
}
/**
......@@ -295,6 +342,6 @@ class Session implements SessionInterface
*/
public function clearFlashes()
{
return $this->getBag('flashes')->clear();
return $this->getBag($this->flashName)->clear();
}
}
......@@ -23,26 +23,26 @@ interface SessionBagInterface
*
* @return string
*/
function getName();
public function getName();
/**
* Initializes the Bag
*
* @param array $array
*/
function initialize(array &$array);
public function initialize(array &$array);
/**
* Gets the storage key for this bag.
*
* @return string
*/
function getStorageKey();
public function getStorageKey();
/**
* Clears out data from bag.
*
* @return mixed Whatever data was contained.
*/
function clear();
public function clear();
}
......@@ -11,6 +11,8 @@
namespace Symfony\Component\HttpFoundation\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag;
/**
* Interface for the session.
*
......@@ -27,7 +29,7 @@ interface SessionInterface
*
* @api
*/
function start();
public function start();
/**
* Returns the session ID.
......@@ -36,7 +38,7 @@ interface SessionInterface
*
* @api
*/
function getId();
public function getId();
/**
* Sets the session ID
......@@ -45,7 +47,7 @@ interface SessionInterface
*
* @api
*/
function setId($id);
public function setId($id);
/**
* Returns the session name.
......@@ -54,7 +56,7 @@ interface SessionInterface
*
* @api
*/
function getName();
public function getName();
/**
* Sets the session name.
......@@ -63,7 +65,7 @@ interface SessionInterface
*
* @api
*/
function setName($name);
public function setName($name);
/**
* Invalidates the current session.
......@@ -71,23 +73,32 @@ interface SessionInterface
* Clears all session attributes and flashes and regenerates the
* session and deletes the old session from persistence.
*
* @param integer $lifetime Sets the cookie lifetime for the session cookie. A null value
* will leave the system settings unchanged, 0 sets the cookie
* to expire with browser session. Time is in seconds, and is
* not a Unix timestamp.
*
* @return Boolean True if session invalidated, false if error.
*
* @api
*/
function invalidate();
public function invalidate($lifetime = null);
/**
* Migrates the current session to a new session id while maintaining all
* session attributes.
*
* @param Boolean $destroy Whether to delete the old session or leave it to garbage collection.
* @param Boolean $destroy Whether to delete the old session or leave it to garbage collection.
* @param integer $lifetime Sets the cookie lifetime for the session cookie. A null value
* will leave the system settings unchanged, 0 sets the cookie
* to expire with browser session. Time is in seconds, and is
* not a Unix timestamp.
*
* @return Boolean True if session migrated, false if error.
*
* @api
*/
function migrate($destroy = false);
public function migrate($destroy = false, $lifetime = null);
/**
* Force the session to be saved and closed.
......@@ -96,7 +107,7 @@ interface SessionInterface
* the session will be automatically saved at the end of
* code execution.
*/
function save();
public function save();
/**
* Checks if an attribute is defined.
......@@ -107,7 +118,7 @@ interface SessionInterface
*
* @api
*/
function has($name);
public function has($name);
/**
* Returns an attribute.
......@@ -119,7 +130,7 @@ interface SessionInterface
*
* @api
*/
function get($name, $default = null);
public function get($name, $default = null);
/**
* Sets an attribute.
......@@ -129,7 +140,7 @@ interface SessionInterface
*
* @api
*/
function set($name, $value);
public function set($name, $value);
/**
* Returns attributes.
......@@ -138,14 +149,14 @@ interface SessionInterface
*
* @api
*/
function all();
public function all();
/**
* Sets attributes.
*
* @param array $attributes Attributes
*/
function replace(array $attributes);
public function replace(array $attributes);
/**
* Removes an attribute.
......@@ -156,12 +167,42 @@ interface SessionInterface
*
* @api
*/
function remove($name);
public function remove($name);
/**
* Clears all attributes.
*
* @api
*/
function clear();
public function clear();
/**
* Checks if the session was started.
*
* @return Boolean
*/
public function isStarted();
/**
* Registers a SessionBagInterface with the session.
*
* @param SessionBagInterface $bag
*/
public function registerBag(SessionBagInterface $bag);
/**
* Gets a bag instance by name.
*
* @param string $name
*
* @return SessionBagInterface
*/
public function getBag($name);
/**
* Gets session meta.
*
* @return MetadataBag
*/
public function getMetadataBag();
}
......@@ -19,85 +19,55 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
class MemcacheSessionHandler implements \SessionHandlerInterface
{
/**
* Memcache driver.
*
* @var \Memcache
* @var \Memcache Memcache driver.
*/
private $memcache;
/**
* Configuration options.
*
* @var array
* @var integer Time to live in seconds
*/
private $memcacheOptions;
private $ttl;
/**
* Key prefix for shared environments.
*
* @var string
* @var string Key prefix for shared environments.
*/
private $prefix;
/**
* Constructor.
*
* @param \Memcache $memcache A \Memcache instance
* @param array $memcacheOptions An associative array of Memcache options
* @param array $options Session configuration options.
* List of available options:
* * prefix: The prefix to use for the memcache keys in order to avoid collision
* * expiretime: The time to live in seconds
*
* @param \Memcache $memcache A \Memcache instance
* @param array $options An associative array of Memcache options
*
* @throws \InvalidArgumentException When unsupported options are passed
*/
public function __construct(\Memcache $memcache, array $memcacheOptions = array(), array $options = array())
public function __construct(\Memcache $memcache, array $options = array())
{
$this->memcache = $memcache;
// defaults
if (!isset($memcacheOptions['serverpool'])) {
$memcacheOptions['serverpool'] = array(array(
'host' => '127.0.0.1',
'port' => 11211,
'timeout' => 1,
'persistent' => false,
'weight' => 1,
'retry_interval' => 15,
if ($diff = array_diff(array_keys($options), array('prefix', 'expiretime'))) {
throw new \InvalidArgumentException(sprintf(
'The following options are not supported "%s"', implode(', ', $diff)
));
}
$memcacheOptions['expiretime'] = isset($memcacheOptions['expiretime']) ? (int)$memcacheOptions['expiretime'] : 86400;
$this->prefix = isset($memcacheOptions['prefix']) ? $memcacheOptions['prefix'] : 'sf2s';
$this->memcacheOptions = $memcacheOptions;
}
protected function addServer(array $server)
{
if (!array_key_exists('host', $server)) {
throw new \InvalidArgumentException('host key must be set');
}
$server['port'] = isset($server['port']) ? (int)$server['port'] : 11211;
$server['timeout'] = isset($server['timeout']) ? (int)$server['timeout'] : 1;
$server['persistent'] = isset($server['persistent']) ? (bool)$server['persistent'] : false;
$server['weight'] = isset($server['weight']) ? (int)$server['weight'] : 1;
$server['retry_interval'] = isset($server['retry_interval']) ? (int)$server['retry_interval'] : 15;
$this->memcache->addserver($server['host'], $server['port'], $server['persistent'],$server['weight'],$server['timeout'],$server['retry_interval']);
$this->memcache = $memcache;
$this->ttl = isset($options['expiretime']) ? (int) $options['expiretime'] : 86400;
$this->prefix = isset($options['prefix']) ? $options['prefix'] : 'sf2s';
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function open($savePath, $sessionName)
{
foreach ($this->memcacheOptions['serverpool'] as $server) {
$this->addServer($server);
}
return true;
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function close()
{
......@@ -105,7 +75,7 @@ class MemcacheSessionHandler implements \SessionHandlerInterface
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function read($sessionId)
{
......@@ -113,15 +83,15 @@ class MemcacheSessionHandler implements \SessionHandlerInterface
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function write($sessionId, $data)
{
return $this->memcache->set($this->prefix.$sessionId, $data, 0, $this->memcacheOptions['expiretime']);
return $this->memcache->set($this->prefix.$sessionId, $data, 0, time() + $this->ttl);
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function destroy($sessionId)
{
......@@ -129,7 +99,7 @@ class MemcacheSessionHandler implements \SessionHandlerInterface
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function gc($lifetime)
{
......
......@@ -24,55 +24,56 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
class MemcachedSessionHandler implements \SessionHandlerInterface
{
/**
* Memcached driver.
*
* @var \Memcached
* @var \Memcached Memcached driver.
*/
private $memcached;
/**
* Configuration options.
*
* @var array
* @var integer Time to live in seconds
*/
private $ttl;
/**
* @var string Key prefix for shared environments.
*/
private $memcachedOptions;
private $prefix;
/**
* Constructor.
*
* @param \Memcached $memcached A \Memcached instance
* @param array $memcachedOptions An associative array of Memcached options
* @param array $options Session configuration options.
* 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
*
* @throws \InvalidArgumentException When unsupported options are passed
*/
public function __construct(\Memcached $memcached, array $memcachedOptions = array(), array $options = array())
public function __construct(\Memcached $memcached, array $options = array())
{
$this->memcached = $memcached;
// defaults
if (!isset($memcachedOptions['serverpool'])) {
$memcachedOptions['serverpool'][] = array(
'host' => '127.0.0.1',
'port' => 11211,
'weight' => 1);
if ($diff = array_diff(array_keys($options), array('prefix', 'expiretime'))) {
throw new \InvalidArgumentException(sprintf(
'The following options are not supported "%s"', implode(', ', $diff)
));
}
$memcachedOptions['expiretime'] = isset($memcachedOptions['expiretime']) ? (int)$memcachedOptions['expiretime'] : 86400;
$this->memcached->setOption(\Memcached::OPT_PREFIX_KEY, isset($memcachedOptions['prefix']) ? $memcachedOptions['prefix'] : 'sf2s');
$this->memcachedOptions = $memcachedOptions;
$this->ttl = isset($options['expiretime']) ? (int) $options['expiretime'] : 86400;
$this->prefix = isset($options['prefix']) ? $options['prefix'] : 'sf2s';
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function open($savePath, $sessionName)
{
return $this->memcached->addServers($this->memcachedOptions['serverpool']);
return true;
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function close()
{
......@@ -80,51 +81,35 @@ class MemcachedSessionHandler implements \SessionHandlerInterface
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function read($sessionId)
{
return $this->memcached->get($sessionId) ?: '';
return $this->memcached->get($this->prefix.$sessionId) ?: '';
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function write($sessionId, $data)
{
return $this->memcached->set($sessionId, $data, $this->memcachedOptions['expiretime']);
return $this->memcached->set($this->prefix.$sessionId, $data, time() + $this->ttl);
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function destroy($sessionId)
{
return $this->memcached->delete($sessionId);
return $this->memcached->delete($this->prefix.$sessionId);
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function gc($lifetime)
{
// not required here because memcached will auto expire the records anyhow.
return true;
}
/**
* Adds a server to the memcached handler.
*
* @param array $server
*/
protected function addServer(array $server)
{
if (array_key_exists('host', $server)) {
throw new \InvalidArgumentException('host key must be set');
}
$server['port'] = isset($server['port']) ? (int)$server['port'] : 11211;
$server['timeout'] = isset($server['timeout']) ? (int)$server['timeout'] : 1;
$server['presistent'] = isset($server['presistent']) ? (bool)$server['presistent'] : false;
$server['weight'] = isset($server['weight']) ? (bool)$server['weight'] : 1;
}
}
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
/**
* MongoDB session handler
*
* @author Markus Bachmann <markus.bachmann@bachi.biz>
*/
class MongoDbSessionHandler implements \SessionHandlerInterface
{
/**
* @var \Mongo
*/
private $mongo;
/**
* @var \MongoCollection
*/
private $collection;
/**
* @var array
*/
private $options;
/**
* Constructor.
*
* @param \Mongo|\MongoClient $mongo A MongoClient or Mongo instance
* @param array $options An associative array of field options
*
* @throws \InvalidArgumentException When MongoClient or Mongo instance not provided
* @throws \InvalidArgumentException When "database" or "collection" not provided
*/
public function __construct($mongo, array $options)
{
if (!($mongo instanceof \MongoClient || $mongo instanceof \Mongo)) {
throw new \InvalidArgumentException('MongoClient or Mongo instance required');
}
if (!isset($options['database']) || !isset($options['collection'])) {
throw new \InvalidArgumentException('You must provide the "database" and "collection" option for MongoDBSessionHandler');
}
$this->mongo = $mongo;
$this->options = array_merge(array(
'id_field' => 'sess_id',
'data_field' => 'sess_data',
'time_field' => 'sess_time',
), $options);
}
/**
* {@inheritDoc}
*/
public function open($savePath, $sessionName)
{
return true;
}
/**
* {@inheritDoc}
*/
public function close()
{
return true;
}
/**
* {@inheritDoc}
*/
public function destroy($sessionId)
{
$this->getCollection()->remove(
array($this->options['id_field'] => $sessionId),
array('justOne' => true)
);
return true;
}
/**
* {@inheritDoc}
*/
public function gc($lifetime)
{
$time = new \MongoTimestamp(time() - $lifetime);
$this->getCollection()->remove(array(
$this->options['time_field'] => array('$lt' => $time),
));
}
/**
* {@inheritDoc]
*/
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(
array($this->options['id_field'] => $sessionId),
array('$set' => $data),
array('upsert' => true)
);
return true;
}
/**
* {@inheritDoc}
*/
public function read($sessionId)
{
$dbData = $this->getCollection()->findOne(array(
$this->options['id_field'] => $sessionId,
));
return null === $dbData ? '' : $dbData[$this->options['data_field']]->bin;
}
/**
* Return a "MongoCollection" instance
*
* @return \MongoCollection
*/
private function getCollection()
{
if (null === $this->collection) {
$this->collection = $this->mongo->selectCollection($this->options['database'], $this->options['collection']);
}
return $this->collection;
}
}
......@@ -23,7 +23,13 @@ class NativeFileSessionHandler extends NativeSessionHandler
/**
* Constructor.
*
* @param string $savePath Path of directory to save session files. Default null will leave setting as defined by PHP.
* @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.
*
* @throws \InvalidArgumentException On invalid $savePath
*/
public function __construct($savePath = null)
{
......@@ -31,11 +37,22 @@ class NativeFileSessionHandler extends NativeSessionHandler
$savePath = ini_get('session.save_path');
}
if ($savePath && !is_dir($savePath)) {
mkdir($savePath, 0777, true);
$baseDir = $savePath;
if ($count = substr_count($savePath, ';')) {
if ($count > 2) {
throw new \InvalidArgumentException(sprintf('Invalid argument $savePath \'%s\'', $savePath));
}
// characters after last ';' are the path
$baseDir = ltrim(strrchr($savePath, ';'), ';');
}
if ($baseDir && !is_dir($baseDir)) {
mkdir($baseDir, 0777, true);
}
ini_set('session.save_handler', 'files');
ini_set('session.save_path', $savePath);
ini_set('session.save_handler', 'files');
}
}
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
/**
* NativeMemcacheSessionHandler.
*
* Driver for the memcache session save hadlers provided by the memcache PHP extension.
*
* @see http://php.net/memcache
*
* @author Drak <drak@zikula.org>
*/
class NativeMemcacheSessionHandler extends NativeSessionHandler
{
/**
* Constructor.
*
* @param string $savePath Path of memcache server.
* @param array $options Session configuration options.
*/
public function __construct($savePath = 'tcp://127.0.0.1:11211?persistent=0', array $options = array())
{
if (!extension_loaded('memcache')) {
throw new \RuntimeException('PHP does not have "memcache" session module registered');
}
if (null === $savePath) {
$savePath = ini_get('session.save_path');
}
ini_set('session.save_handler', 'memcache');
ini_set('session.save_path', $savePath);
$this->setOptions($options);
}
/**
* Set any memcached ini values.
*
* @see http://php.net/memcache.ini
*/
protected function setOptions(array $options)
{
foreach ($options as $key => $value) {
if (in_array($key, array(
'memcache.allow_failover', 'memcache.max_failover_attempts',
'memcache.chunk_size', 'memcache.default_port', 'memcache.hash_strategy',
'memcache.hash_function', 'memcache.protocol', 'memcache.redundancy',
'memcache.session_redundancy', 'memcache.compress_threshold',
'memcache.lock_timeout'))) {
ini_set($key, $value);
}
}
}
}
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
/**
* NativeMemcachedSessionHandler.
*
* Driver for the memcached session save hadlers provided by the memcached PHP extension.
*
* @see http://php.net/memcached.sessions
*
* @author Drak <drak@zikula.org>
*/
class NativeMemcachedSessionHandler extends NativeSessionHandler
{
/**
* Constructor.
*
* @param string $savePath Comma separated list of servers: e.g. memcache1.example.com:11211,memcache2.example.com:11211
* @param array $options Session configuration options.
*/
public function __construct($savePath = '127.0.0.1:11211', array $options = array())
{
if (!extension_loaded('memcached')) {
throw new \RuntimeException('PHP does not have "memcached" session module registered');
}
if (null === $savePath) {
$savePath = ini_get('session.save_path');
}
ini_set('session.save_handler', 'memcached');
ini_set('session.save_path', $savePath);
$this->setOptions($options);
}
/**
* Set any memcached ini values.
*
* @see https://github.com/php-memcached-dev/php-memcached/blob/master/memcached.ini
*/
protected function setOptions(array $options)
{
foreach ($options as $key => $value) {
if (in_array($key, array(
'memcached.sess_locking', 'memcached.sess_lock_wait',
'memcached.sess_prefix', 'memcached.compression_type',
'memcached.compression_factor', 'memcached.compression_threshold',
'memcached.serializer'))) {
ini_set($key, $value);
}
}
}
}
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
/**
* NativeSqliteSessionHandler.
*
* Driver for the sqlite session save hadlers provided by the SQLite PHP extension.
*
* @author Drak <drak@zikula.org>
*/
class NativeSqliteSessionHandler extends NativeSessionHandler
{
/**
* Constructor.
*
* @param string $savePath Path to SQLite database file itself.
* @param array $options Session configuration options.
*/
public function __construct($savePath, array $options = array())
{
if (!extension_loaded('sqlite')) {
throw new \RuntimeException('PHP does not have "sqlite" session module registered');
}
if (null === $savePath) {
$savePath = ini_get('session.save_path');
}
ini_set('session.save_handler', 'sqlite');
ini_set('session.save_path', $savePath);
$this->setOptions($options);
}
/**
* Set any sqlite ini values.
*
* @see http://php.net/sqlite.configuration
*/
protected function setOptions(array $options)
{
foreach ($options as $key => $value) {
if (in_array($key, array('sqlite.assoc_case'))) {
ini_set($key, $value);
}
}
}
}
......@@ -14,7 +14,7 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
/**
* NullSessionHandler.
*
* Can be used in unit testing or in a sitation where persisted sessions are not desired.
* Can be used in unit testing or in a situations where persisted sessions are not desired.
*
* @author Drak <drak@zikula.org>
*
......
......@@ -20,30 +20,30 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
class PdoSessionHandler implements \SessionHandlerInterface
{
/**
* PDO instance.
*
* @var \PDO
* @var \PDO PDO instance.
*/
private $pdo;
/**
* Database options.
*
*
* @var array
* @var array Database options.
*/
private $dbOptions;
/**
* Constructor.
*
* List of available options:
* * db_table: The name of the table [required]
* * db_id_col: The column where to store the session id [default: sess_id]
* * db_data_col: The column where to store the session data [default: sess_data]
* * db_time_col: The column where to store the timestamp [default: sess_time]
*
* @param \PDO $pdo A \PDO instance
* @param array $dbOptions An associative array of DB options
* @param array $options Session configuration options
*
* @throws \InvalidArgumentException When "db_table" option is not provided
*/
public function __construct(\PDO $pdo, array $dbOptions = array(), array $options = array())
public function __construct(\PDO $pdo, array $dbOptions = array())
{
if (!array_key_exists('db_table', $dbOptions)) {
throw new \InvalidArgumentException('You must provide the "db_table" option for a PdoSessionStorage.');
......@@ -58,7 +58,7 @@ class PdoSessionHandler implements \SessionHandlerInterface
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function open($path, $name)
{
......@@ -66,7 +66,7 @@ class PdoSessionHandler implements \SessionHandlerInterface
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function close()
{
......@@ -74,12 +74,12 @@ class PdoSessionHandler implements \SessionHandlerInterface
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function destroy($id)
{
// get table/column
$dbTable = $this->dbOptions['db_table'];
$dbTable = $this->dbOptions['db_table'];
$dbIdCol = $this->dbOptions['db_id_col'];
// delete the record associated with this id
......@@ -97,20 +97,20 @@ class PdoSessionHandler implements \SessionHandlerInterface
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function gc($lifetime)
{
// get table/column
$dbTable = $this->dbOptions['db_table'];
$dbTable = $this->dbOptions['db_table'];
$dbTimeCol = $this->dbOptions['db_time_col'];
// delete the session records that have expired
$sql = "DELETE FROM $dbTable WHERE $dbTimeCol < (:time - $lifetime)";
$sql = "DELETE FROM $dbTable WHERE $dbTimeCol < :time";
try {
$stmt = $this->pdo->prepare($sql);
$stmt->bindValue(':time', time(), \PDO::PARAM_INT);
$stmt->bindValue(':time', time() - $lifetime, \PDO::PARAM_INT);
$stmt->execute();
} catch (\PDOException $e) {
throw new \RuntimeException(sprintf('PDOException was thrown when trying to manipulate session data: %s', $e->getMessage()), 0, $e);
......@@ -120,12 +120,12 @@ class PdoSessionHandler implements \SessionHandlerInterface
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function read($id)
{
// get table/columns
$dbTable = $this->dbOptions['db_table'];
$dbTable = $this->dbOptions['db_table'];
$dbDataCol = $this->dbOptions['db_data_col'];
$dbIdCol = $this->dbOptions['db_id_col'];
......@@ -154,7 +154,7 @@ class PdoSessionHandler implements \SessionHandlerInterface
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function write($id, $data)
{
......@@ -164,27 +164,47 @@ class PdoSessionHandler implements \SessionHandlerInterface
$dbIdCol = $this->dbOptions['db_id_col'];
$dbTimeCol = $this->dbOptions['db_time_col'];
$sql = ('mysql' === $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME))
? "INSERT INTO $dbTable ($dbIdCol, $dbDataCol, $dbTimeCol) VALUES (:id, :data, :time) "
."ON DUPLICATE KEY UPDATE $dbDataCol = VALUES($dbDataCol), $dbTimeCol = CASE WHEN $dbTimeCol = :time THEN (VALUES($dbTimeCol) + 1) ELSE VALUES($dbTimeCol) END"
: "UPDATE $dbTable SET $dbDataCol = :data, $dbTimeCol = :time WHERE $dbIdCol = :id";
//session data can contain non binary safe characters so we need to encode it
$encoded = base64_encode($data);
try {
//session data can contain non binary safe characters so we need to encode it
$encoded = base64_encode($data);
$stmt = $this->pdo->prepare($sql);
$stmt->bindParam(':id', $id, \PDO::PARAM_STR);
$stmt->bindParam(':data', $encoded, \PDO::PARAM_STR);
$stmt->bindValue(':time', time(), \PDO::PARAM_INT);
$stmt->execute();
if (!$stmt->rowCount()) {
// No session exists in the database to update. This happens when we have called
// session_regenerate_id()
$this->createNewSession($id, $data);
$driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
if ('mysql' === $driver) {
// MySQL would report $stmt->rowCount() = 0 on UPDATE when the data is left unchanged
// it could result in calling createNewSession() whereas the session already exists in
// the DB which would fail as the id is unique
$stmt = $this->pdo->prepare(
"INSERT INTO $dbTable ($dbIdCol, $dbDataCol, $dbTimeCol) VALUES (:id, :data, :time) " .
"ON DUPLICATE KEY UPDATE $dbDataCol = VALUES($dbDataCol), $dbTimeCol = VALUES($dbTimeCol)"
);
$stmt->bindParam(':id', $id, \PDO::PARAM_STR);
$stmt->bindParam(':data', $encoded, \PDO::PARAM_STR);
$stmt->bindValue(':time', time(), \PDO::PARAM_INT);
$stmt->execute();
} elseif ('oci' === $driver) {
$stmt = $this->pdo->prepare("MERGE INTO $dbTable USING DUAL ON($dbIdCol = :id) ".
"WHEN NOT MATCHED THEN INSERT ($dbIdCol, $dbDataCol, $dbTimeCol) VALUES (:id, :data, sysdate) " .
"WHEN MATCHED THEN UPDATE SET $dbDataCol = :data WHERE $dbIdCol = :id");
$stmt->bindParam(':id', $id, \PDO::PARAM_STR);
$stmt->bindParam(':data', $encoded, \PDO::PARAM_STR);
$stmt->execute();
} else {
$stmt = $this->pdo->prepare("UPDATE $dbTable SET $dbDataCol = :data, $dbTimeCol = :time WHERE $dbIdCol = :id");
$stmt->bindParam(':id', $id, \PDO::PARAM_STR);
$stmt->bindParam(':data', $encoded, \PDO::PARAM_STR);
$stmt->bindValue(':time', time(), \PDO::PARAM_INT);
$stmt->execute();
if (!$stmt->rowCount()) {
// No session exists in the database to update. This happens when we have called
// session_regenerate_id()
$this->createNewSession($id, $data);
}
}
} catch (\PDOException $e) {
throw new \RuntimeException(sprintf('PDOException was thrown when trying to write the session data: %s', $e->getMessage()), 0, $e);
throw new \RuntimeException(sprintf('PDOException was thrown when trying to write the session data: %s', $e->getMessage()), 0, $e);
}
return true;
......@@ -201,7 +221,7 @@ class PdoSessionHandler implements \SessionHandlerInterface
private function createNewSession($id, $data = '')
{
// get table/column
$dbTable = $this->dbOptions['db_table'];
$dbTable = $this->dbOptions['db_table'];
$dbDataCol = $this->dbOptions['db_data_col'];
$dbIdCol = $this->dbOptions['db_id_col'];
$dbTimeCol = $this->dbOptions['db_time_col'];
......
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session\Storage;
use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
/**
* Metadata container.
*
* Adds metadata to the session.
*
* @author Drak <drak@zikula.org>
*/
class MetadataBag implements SessionBagInterface
{
const CREATED = 'c';
const UPDATED = 'u';
const LIFETIME = 'l';
/**
* @var string
*/
private $name = '__metadata';
/**
* @var string
*/
private $storageKey;
/**
* @var array
*/
protected $meta = array();
/**
* Unix timestamp.
*
* @var integer
*/
private $lastUsed;
/**
* Constructor.
*
* @param string $storageKey The key used to store bag in the session.
*/
public function __construct($storageKey = '_sf2_meta')
{
$this->storageKey = $storageKey;
$this->meta = array(self::CREATED => 0, self::UPDATED => 0, self::LIFETIME => 0);
}
/**
* {@inheritdoc}
*/
public function initialize(array &$array)
{
$this->meta = &$array;
if (isset($array[self::CREATED])) {
$this->lastUsed = $this->meta[self::UPDATED];
$this->meta[self::UPDATED] = time();
} else {
$this->stampCreated();
}
}
/**
* Gets the lifetime that the session cookie was set with.
*
* @return integer
*/
public function getLifetime()
{
return $this->meta[self::LIFETIME];
}
/**
* Stamps a new session's metadata.
*
* @param integer $lifetime Sets the cookie lifetime for the session cookie. A null value
* will leave the system settings unchanged, 0 sets the cookie
* to expire with browser session. Time is in seconds, and is
* not a Unix timestamp.
*/
public function stampNew($lifetime = null)
{
$this->stampCreated($lifetime);
}
/**
* {@inheritdoc}
*/
public function getStorageKey()
{
return $this->storageKey;
}
/**
* Gets the created timestamp metadata.
*
* @return integer Unix timestamp
*/
public function getCreated()
{
return $this->meta[self::CREATED];
}
/**
* Gets the last used metadata.
*
* @return integer Unix timestamp
*/
public function getLastUsed()
{
return $this->lastUsed;
}
/**
* {@inheritdoc}
*/
public function clear()
{
// nothing to do
}
/**
* {@inheritdoc}
*/
public function getName()
{
return $this->name;
}
/**
* Sets name.
*
* @param string $name
*/
public function setName($name)
{
$this->name = $name;
}
private function stampCreated($lifetime = null)
{
$timeStamp = time();
$this->meta[self::CREATED] = $this->meta[self::UPDATED] = $this->lastUsed = $timeStamp;
$this->meta[self::LIFETIME] = (null === $lifetime) ? ini_get('session.cookie_lifetime') : $lifetime;
}
}
......@@ -12,6 +12,7 @@
namespace Symfony\Component\HttpFoundation\Session\Storage;
use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag;
/**
* MockArraySessionStorage mocks the session for unit tests.
......@@ -52,14 +53,26 @@ class MockArraySessionStorage implements SessionStorageInterface
*/
protected $data = array();
/**
* @var MetadataBag
*/
protected $metadataBag;
/**
* @var array
*/
protected $bags;
/**
* Constructor.
*
* @param string $name Session name
* @param string $name Session name
* @param MetadataBag $metaBag MetadataBag instance.
*/
public function __construct($name = 'MOCKSESSID')
public function __construct($name = 'MOCKSESSID', MetadataBag $metaBag = null)
{
$this->name = $name;
$this->setMetadataBag($metaBag);
}
/**
......@@ -90,16 +103,16 @@ class MockArraySessionStorage implements SessionStorageInterface
return true;
}
/**
* {@inheritdoc}
*/
public function regenerate($destroy = false)
public function regenerate($destroy = false, $lifetime = null)
{
if (!$this->started) {
$this->start();
}
$this->metadataBag->stampNew($lifetime);
$this->id = $this->generateId();
return true;
......@@ -146,6 +159,9 @@ class MockArraySessionStorage implements SessionStorageInterface
*/
public function save()
{
if (!$this->started || $this->closed) {
throw new \RuntimeException("Trying to save a session that was not started yet or was already closed");
}
// nothing to do since we don't persist the session data
$this->closed = false;
}
......@@ -191,6 +207,38 @@ class MockArraySessionStorage implements SessionStorageInterface
return $this->bags[$name];
}
/**
* {@inheritdoc}
*/
public function isStarted()
{
return $this->started;
}
/**
* Sets the MetadataBag.
*
* @param MetadataBag $bag
*/
public function setMetadataBag(MetadataBag $bag = null)
{
if (null === $bag) {
$bag = new MetadataBag();
}
$this->metadataBag = $bag;
}
/**
* Gets the MetadataBag.
*
* @return MetadataBag
*/
public function getMetadataBag()
{
return $this->metadataBag;
}
/**
* Generates a session ID.
*
......@@ -206,7 +254,9 @@ class MockArraySessionStorage implements SessionStorageInterface
protected function loadSession()
{
foreach ($this->bags as $bag) {
$bags = array_merge($this->bags, array($this->metadataBag));
foreach ($bags as $bag) {
$key = $bag->getStorageKey();
$this->data[$key] = isset($this->data[$key]) ? $this->data[$key] : array();
$bag->initialize($this->data[$key]);
......
......@@ -29,13 +29,19 @@ class MockFileSessionStorage extends MockArraySessionStorage
*/
private $savePath;
/**
* @var array
*/
private $sessionData;
/**
* Constructor.
*
* @param string $savePath Path of directory to save session files.
* @param string $name Session name.
* @param string $savePath Path of directory to save session files.
* @param string $name Session name.
* @param MetadataBag $metaBag MetadataBag instance.
*/
public function __construct($savePath = null, $name = 'MOCKSESSID')
public function __construct($savePath = null, $name = 'MOCKSESSID', MetadataBag $metaBag = null)
{
if (null === $savePath) {
$savePath = sys_get_temp_dir();
......@@ -47,7 +53,7 @@ class MockFileSessionStorage extends MockArraySessionStorage
$this->savePath = $savePath;
parent::__construct($name);
parent::__construct($name, $metaBag);
}
/**
......@@ -73,15 +79,17 @@ class MockFileSessionStorage extends MockArraySessionStorage
/**
* {@inheritdoc}
*/
public function regenerate($destroy = false)
public function regenerate($destroy = false, $lifetime = null)
{
if (!$this->started) {
$this->start();
}
if ($destroy) {
$this->destroy();
}
$this->id = $this->generateId();
return true;
return parent::regenerate($destroy, $lifetime);
}
/**
......@@ -89,7 +97,16 @@ class MockFileSessionStorage extends MockArraySessionStorage
*/
public function save()
{
if (!$this->started) {
throw new \RuntimeException("Trying to save a session that was not started yet or was already closed");
}
file_put_contents($this->getFilePath(), serialize($this->data));
// this is needed for Silex, where the session object is re-used across requests
// in functional tests. In Symfony, the container is rebooted, so we don't have
// this issue
$this->started = false;
}
/**
......
......@@ -12,6 +12,7 @@
namespace Symfony\Component\HttpFoundation\Session\Storage;
use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag;
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\NativeProxy;
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy;
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy;
......@@ -45,17 +46,24 @@ class NativeSessionStorage implements SessionStorageInterface
*/
protected $saveHandler;
/**
* @var MetadataBag
*/
protected $metadataBag;
/**
* Constructor.
*
* Depending on how you want the storage driver to behave you probably
* want top override this constructor entirely.
* want to override this constructor entirely.
*
* List of options for $options array with their defaults.
* @see http://php.net/session.configuration for options
* but we omit 'session.' from the beginning of the keys for convenience.
*
* auto_start, "0"
* ("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, "nocache" (use "0" to prevent headers from being sent entirely).
* cookie_domain, ""
* cookie_httponly, ""
......@@ -83,13 +91,12 @@ class NativeSessionStorage implements SessionStorageInterface
* upload_progress.min-freq, "1"
* url_rewriter.tags, "a=href,area=href,frame=src,form=,fieldset="
*
* @param array $options Session configuration options.
* @param object $handler SessionHandlerInterface.
* @param array $options Session configuration options.
* @param object $handler SessionHandlerInterface.
* @param MetadataBag $metaBag MetadataBag.
*/
public function __construct(array $options = array(), $handler = null)
public function __construct(array $options = array(), $handler = null, MetadataBag $metaBag = null)
{
// sensible defaults
ini_set('session.auto_start', 0); // by default we prefer to explicitly start the session using the class.
ini_set('session.cache_limiter', ''); // disable by default because it's managed by HeaderBag (if used)
ini_set('session.use_cookies', 1);
......@@ -99,6 +106,7 @@ class NativeSessionStorage implements SessionStorageInterface
register_shutdown_function('session_write_close');
}
$this->setMetadataBag($metaBag);
$this->setOptions($options);
$this->setSaveHandler($handler);
}
......@@ -165,7 +173,7 @@ class NativeSessionStorage implements SessionStorageInterface
*/
public function setId($id)
{
return $this->saveHandler->setId($id);
$this->saveHandler->setId($id);
}
/**
......@@ -187,8 +195,16 @@ class NativeSessionStorage implements SessionStorageInterface
/**
* {@inheritdoc}
*/
public function regenerate($destroy = false)
public function regenerate($destroy = false, $lifetime = null)
{
if (null !== $lifetime) {
ini_set('session.cookie_lifetime', $lifetime);
}
if ($destroy) {
$this->metadataBag->stampNew();
}
return session_regenerate_id($destroy);
}
......@@ -240,15 +256,47 @@ class NativeSessionStorage implements SessionStorageInterface
throw new \InvalidArgumentException(sprintf('The SessionBagInterface %s is not registered.', $name));
}
if (ini_get('session.auto_start') && !$this->started) {
$this->start();
} elseif ($this->saveHandler->isActive() && !$this->started) {
if ($this->saveHandler->isActive() && !$this->started) {
$this->loadSession();
} elseif (!$this->started) {
$this->start();
}
return $this->bags[$name];
}
/**
* Sets the MetadataBag.
*
* @param MetadataBag $metaBag
*/
public function setMetadataBag(MetadataBag $metaBag = null)
{
if (null === $metaBag) {
$metaBag = new MetadataBag();
}
$this->metadataBag = $metaBag;
}
/**
* Gets the MetadataBag.
*
* @return MetadataBag
*/
public function getMetadataBag()
{
return $this->metadataBag;
}
/**
* {@inheritdoc}
*/
public function isStarted()
{
return $this->started;
}
/**
* Sets session.* ini variables.
*
......@@ -261,17 +309,20 @@ class NativeSessionStorage implements SessionStorageInterface
*/
public function setOptions(array $options)
{
$validOptions = array_flip(array(
'cache_limiter', 'cookie_domain', 'cookie_httponly',
'cookie_lifetime', 'cookie_path', 'cookie_secure',
'entropy_file', 'entropy_length', 'gc_divisor',
'gc_maxlifetime', 'gc_probability', 'hash_bits_per_character',
'hash_function', 'name', 'referer_check',
'serialize_handler', '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',
));
foreach ($options as $key => $value) {
if (in_array($key, array(
'auto_start', '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',
'serialize_handler', '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'))) {
if (isset($validOptions[$key])) {
ini_set('session.'.$key, $value);
}
}
......@@ -298,7 +349,7 @@ class NativeSessionStorage implements SessionStorageInterface
if (!$saveHandler instanceof AbstractProxy && $saveHandler instanceof \SessionHandlerInterface) {
$saveHandler = new SessionHandlerProxy($saveHandler);
} elseif (!$saveHandler instanceof AbstractProxy) {
$saveHandler = new NativeProxy($saveHandler);
$saveHandler = new NativeProxy();
}
$this->saveHandler = $saveHandler;
......@@ -335,7 +386,9 @@ class NativeSessionStorage implements SessionStorageInterface
$session = &$_SESSION;
}
foreach ($this->bags as $bag) {
$bags = array_merge($this->bags, array($this->metadataBag));
foreach ($bags as $bag) {
$key = $bag->getStorageKey();
$session[$key] = isset($session[$key]) ? $session[$key] : array();
$bag->initialize($session[$key]);
......
......@@ -58,7 +58,7 @@ abstract class AbstractProxy
/**
* Returns true if this handler wraps an internal PHP session save handler using \SessionHandler.
*
* @return bool
* @return Boolean
*/
public function isWrapper()
{
......@@ -68,7 +68,7 @@ abstract class AbstractProxy
/**
* Has a session started?
*
* @return bool
* @return Boolean
*/
public function isActive()
{
......@@ -78,7 +78,7 @@ abstract class AbstractProxy
/**
* Sets the active flag.
*
* @param bool $flag
* @param Boolean $flag
*/
public function setActive($flag)
{
......
......@@ -32,7 +32,7 @@ class NativeProxy extends AbstractProxy
/**
* Returns true if this handler wraps an internal PHP session save handler using \SessionHandler.
*
* @return bool False.
* @return Boolean False.
*/
public function isWrapper()
{
......
......@@ -42,7 +42,7 @@ class SessionHandlerProxy extends AbstractProxy implements \SessionHandlerInterf
*/
public function open($savePath, $sessionName)
{
$return = (bool)$this->handler->open($savePath, $sessionName);
$return = (bool) $this->handler->open($savePath, $sessionName);
if (true === $return) {
$this->active = true;
......
......@@ -12,6 +12,7 @@
namespace Symfony\Component\HttpFoundation\Session\Storage;
use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag;
/**
* StorageInterface.
......@@ -32,7 +33,14 @@ interface SessionStorageInterface
*
* @api
*/
function start();
public function start();
/**
* Checks if the session is started.
*
* @return boolean True if started, false otherwise.
*/
public function isStarted();
/**
* Returns the session ID
......@@ -41,7 +49,7 @@ interface SessionStorageInterface
*
* @api
*/
function getId();
public function getId();
/**
* Sets the session ID
......@@ -50,7 +58,7 @@ interface SessionStorageInterface
*
* @api
*/
function setId($id);
public function setId($id);
/**
* Returns the session name
......@@ -59,7 +67,7 @@ interface SessionStorageInterface
*
* @api
*/
function getName();
public function getName();
/**
* Sets the session name
......@@ -68,7 +76,7 @@ interface SessionStorageInterface
*
* @api
*/
function setName($name);
public function setName($name);
/**
* Regenerates id that represents this storage.
......@@ -81,7 +89,11 @@ interface SessionStorageInterface
* Note regenerate+destroy should not clear the session data in memory
* only delete the session data from persistent storage.
*
* @param Boolean $destroy Destroy session when regenerating?
* @param Boolean $destroy Destroy session when regenerating?
* @param integer $lifetime Sets the cookie lifetime for the session cookie. A null value
* will leave the system settings unchanged, 0 sets the cookie
* to expire with browser session. Time is in seconds, and is
* not a Unix timestamp.
*
* @return Boolean True if session regenerated, false if error
*
......@@ -89,7 +101,7 @@ interface SessionStorageInterface
*
* @api
*/
function regenerate($destroy = false);
public function regenerate($destroy = false, $lifetime = null);
/**
* Force the session to be saved and closed.
......@@ -98,13 +110,16 @@ interface SessionStorageInterface
* used for a storage object design for unit or functional testing where
* a real PHP session would interfere with testing, in which case it
* 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.
*/
function save();
public function save();
/**
* Clear all session data in memory.
*/
function clear();
public function clear();
/**
* Gets a SessionBagInterface by name.
......@@ -115,12 +130,17 @@ interface SessionStorageInterface
*
* @throws \InvalidArgumentException If the bag does not exist
*/
function getBag($name);
public function getBag($name);
/**
* Registers a SessionBagInterface for use.
*
* @param SessionBagInterface $bag
*/
function registerBag(SessionBagInterface $bag);
public function registerBag(SessionBagInterface $bag);
/**
* @return MetadataBag
*/
public function getMetadataBag();
}
......@@ -72,21 +72,17 @@ class StreamedResponse extends Response
}
/**
* @{inheritdoc}
* {@inheritdoc}
*/
public function prepare(Request $request)
{
if ('1.0' != $request->server->get('SERVER_PROTOCOL')) {
$this->setProtocolVersion('1.1');
}
$this->headers->set('Cache-Control', 'no-cache');
parent::prepare($request);
return parent::prepare($request);
}
/**
* @{inheritdoc}
* {@inheritdoc}
*
* This method only sends the content once.
*/
......@@ -106,7 +102,7 @@ class StreamedResponse extends Response
}
/**
* @{inheritdoc}
* {@inheritdoc}
*
* @throws \LogicException when the content is not null
*/
......@@ -118,7 +114,7 @@ class StreamedResponse extends Response
}
/**
* @{inheritdoc}
* {@inheritdoc}
*
* @return false
*/
......
......@@ -16,7 +16,7 @@
}
],
"require": {
"php": ">=5.3.2"
"php": ">=5.3.3"
},
"autoload": {
"psr-0": {
......@@ -25,9 +25,5 @@
}
},
"target-dir": "Symfony/Component/HttpFoundation",
"extra": {
"branch-alias": {
"dev-master": "2.1-dev"
}
}
"minimum-stability": "dev"
}
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