188 changed files with 26556 additions and 0 deletions
@ -0,0 +1,3 @@ |
|||
vendor/ |
|||
composer.lock |
|||
phpunit.xml |
|||
@ -0,0 +1,173 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation; |
|||
|
|||
/** |
|||
* Represents an Accept-* header. |
|||
* |
|||
* An accept header is compound with a list of items, |
|||
* sorted by descending quality. |
|||
* |
|||
* @author Jean-François Simon <contact@jfsimon.fr> |
|||
*/ |
|||
class AcceptHeader |
|||
{ |
|||
/** |
|||
* @var AcceptHeaderItem[] |
|||
*/ |
|||
private $items = []; |
|||
|
|||
/** |
|||
* @var bool |
|||
*/ |
|||
private $sorted = true; |
|||
|
|||
/** |
|||
* @param AcceptHeaderItem[] $items |
|||
*/ |
|||
public function __construct(array $items) |
|||
{ |
|||
foreach ($items as $item) { |
|||
$this->add($item); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Builds an AcceptHeader instance from a string. |
|||
* |
|||
* @param string $headerValue |
|||
* |
|||
* @return self |
|||
*/ |
|||
public static function fromString($headerValue) |
|||
{ |
|||
$index = 0; |
|||
|
|||
$parts = HeaderUtils::split((string) $headerValue, ',;='); |
|||
|
|||
return new self(array_map(function ($subParts) use (&$index) { |
|||
$part = array_shift($subParts); |
|||
$attributes = HeaderUtils::combine($subParts); |
|||
|
|||
$item = new AcceptHeaderItem($part[0], $attributes); |
|||
$item->setIndex($index++); |
|||
|
|||
return $item; |
|||
}, $parts)); |
|||
} |
|||
|
|||
/** |
|||
* Returns header value's string representation. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function __toString() |
|||
{ |
|||
return implode(',', $this->items); |
|||
} |
|||
|
|||
/** |
|||
* Tests if header has given value. |
|||
* |
|||
* @param string $value |
|||
* |
|||
* @return bool |
|||
*/ |
|||
public function has($value) |
|||
{ |
|||
return isset($this->items[$value]); |
|||
} |
|||
|
|||
/** |
|||
* Returns given value's item, if exists. |
|||
* |
|||
* @param string $value |
|||
* |
|||
* @return AcceptHeaderItem|null |
|||
*/ |
|||
public function get($value) |
|||
{ |
|||
return $this->items[$value] ?? $this->items[explode('/', $value)[0].'/*'] ?? $this->items['*/*'] ?? $this->items['*'] ?? null; |
|||
} |
|||
|
|||
/** |
|||
* Adds an item. |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function add(AcceptHeaderItem $item) |
|||
{ |
|||
$this->items[$item->getValue()] = $item; |
|||
$this->sorted = false; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Returns all items. |
|||
* |
|||
* @return AcceptHeaderItem[] |
|||
*/ |
|||
public function all() |
|||
{ |
|||
$this->sort(); |
|||
|
|||
return $this->items; |
|||
} |
|||
|
|||
/** |
|||
* Filters items on their value using given regex. |
|||
* |
|||
* @param string $pattern |
|||
* |
|||
* @return self |
|||
*/ |
|||
public function filter($pattern) |
|||
{ |
|||
return new self(array_filter($this->items, function (AcceptHeaderItem $item) use ($pattern) { |
|||
return preg_match($pattern, $item->getValue()); |
|||
})); |
|||
} |
|||
|
|||
/** |
|||
* Returns first item. |
|||
* |
|||
* @return AcceptHeaderItem|null |
|||
*/ |
|||
public function first() |
|||
{ |
|||
$this->sort(); |
|||
|
|||
return !empty($this->items) ? reset($this->items) : null; |
|||
} |
|||
|
|||
/** |
|||
* Sorts items by descending quality. |
|||
*/ |
|||
private function sort() |
|||
{ |
|||
if (!$this->sorted) { |
|||
uasort($this->items, function (AcceptHeaderItem $a, AcceptHeaderItem $b) { |
|||
$qA = $a->getQuality(); |
|||
$qB = $b->getQuality(); |
|||
|
|||
if ($qA === $qB) { |
|||
return $a->getIndex() > $b->getIndex() ? 1 : -1; |
|||
} |
|||
|
|||
return $qA > $qB ? -1 : 1; |
|||
}); |
|||
|
|||
$this->sorted = true; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,191 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation; |
|||
|
|||
/** |
|||
* Represents an Accept-* header item. |
|||
* |
|||
* @author Jean-François Simon <contact@jfsimon.fr> |
|||
*/ |
|||
class AcceptHeaderItem |
|||
{ |
|||
private $value; |
|||
private $quality = 1.0; |
|||
private $index = 0; |
|||
private $attributes = []; |
|||
|
|||
public function __construct(string $value, array $attributes = []) |
|||
{ |
|||
$this->value = $value; |
|||
foreach ($attributes as $name => $value) { |
|||
$this->setAttribute($name, $value); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Builds an AcceptHeaderInstance instance from a string. |
|||
* |
|||
* @param string $itemValue |
|||
* |
|||
* @return self |
|||
*/ |
|||
public static function fromString($itemValue) |
|||
{ |
|||
$parts = HeaderUtils::split($itemValue, ';='); |
|||
|
|||
$part = array_shift($parts); |
|||
$attributes = HeaderUtils::combine($parts); |
|||
|
|||
return new self($part[0], $attributes); |
|||
} |
|||
|
|||
/** |
|||
* Returns header value's string representation. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function __toString() |
|||
{ |
|||
$string = $this->value.($this->quality < 1 ? ';q='.$this->quality : ''); |
|||
if (\count($this->attributes) > 0) { |
|||
$string .= '; '.HeaderUtils::toString($this->attributes, ';'); |
|||
} |
|||
|
|||
return $string; |
|||
} |
|||
|
|||
/** |
|||
* Set the item value. |
|||
* |
|||
* @param string $value |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function setValue($value) |
|||
{ |
|||
$this->value = $value; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Returns the item value. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getValue() |
|||
{ |
|||
return $this->value; |
|||
} |
|||
|
|||
/** |
|||
* Set the item quality. |
|||
* |
|||
* @param float $quality |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function setQuality($quality) |
|||
{ |
|||
$this->quality = $quality; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Returns the item quality. |
|||
* |
|||
* @return float |
|||
*/ |
|||
public function getQuality() |
|||
{ |
|||
return $this->quality; |
|||
} |
|||
|
|||
/** |
|||
* Set the item index. |
|||
* |
|||
* @param int $index |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function setIndex($index) |
|||
{ |
|||
$this->index = $index; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Returns the item index. |
|||
* |
|||
* @return int |
|||
*/ |
|||
public function getIndex() |
|||
{ |
|||
return $this->index; |
|||
} |
|||
|
|||
/** |
|||
* Tests if an attribute exists. |
|||
* |
|||
* @param string $name |
|||
* |
|||
* @return bool |
|||
*/ |
|||
public function hasAttribute($name) |
|||
{ |
|||
return isset($this->attributes[$name]); |
|||
} |
|||
|
|||
/** |
|||
* Returns an attribute by its name. |
|||
* |
|||
* @param string $name |
|||
* @param mixed $default |
|||
* |
|||
* @return mixed |
|||
*/ |
|||
public function getAttribute($name, $default = null) |
|||
{ |
|||
return isset($this->attributes[$name]) ? $this->attributes[$name] : $default; |
|||
} |
|||
|
|||
/** |
|||
* Returns all attributes. |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function getAttributes() |
|||
{ |
|||
return $this->attributes; |
|||
} |
|||
|
|||
/** |
|||
* Set an attribute. |
|||
* |
|||
* @param string $name |
|||
* @param string $value |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function setAttribute($name, $value) |
|||
{ |
|||
if ('q' === $name) { |
|||
$this->quality = (float) $value; |
|||
} else { |
|||
$this->attributes[$name] = (string) $value; |
|||
} |
|||
|
|||
return $this; |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation; |
|||
|
|||
/** |
|||
* Request represents an HTTP request from an Apache server. |
|||
* |
|||
* @author Fabien Potencier <fabien@symfony.com> |
|||
*/ |
|||
class ApacheRequest extends Request |
|||
{ |
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function prepareRequestUri() |
|||
{ |
|||
return $this->server->get('REQUEST_URI'); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function prepareBaseUrl() |
|||
{ |
|||
$baseUrl = $this->server->get('SCRIPT_NAME'); |
|||
|
|||
if (false === strpos($this->server->get('REQUEST_URI'), $baseUrl)) { |
|||
// assume mod_rewrite |
|||
return rtrim(\dirname($baseUrl), '/\\'); |
|||
} |
|||
|
|||
return $baseUrl; |
|||
} |
|||
} |
|||
@ -0,0 +1,359 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation; |
|||
|
|||
use Symfony\Component\HttpFoundation\File\Exception\FileException; |
|||
use Symfony\Component\HttpFoundation\File\File; |
|||
|
|||
/** |
|||
* BinaryFileResponse represents an HTTP response delivering a file. |
|||
* |
|||
* @author Niklas Fiekas <niklas.fiekas@tu-clausthal.de> |
|||
* @author stealth35 <stealth35-php@live.fr> |
|||
* @author Igor Wiedler <igor@wiedler.ch> |
|||
* @author Jordan Alliot <jordan.alliot@gmail.com> |
|||
* @author Sergey Linnik <linniksa@gmail.com> |
|||
*/ |
|||
class BinaryFileResponse extends Response |
|||
{ |
|||
protected static $trustXSendfileTypeHeader = false; |
|||
|
|||
/** |
|||
* @var File |
|||
*/ |
|||
protected $file; |
|||
protected $offset = 0; |
|||
protected $maxlen = -1; |
|||
protected $deleteFileAfterSend = false; |
|||
|
|||
/** |
|||
* @param \SplFileInfo|string $file The file to stream |
|||
* @param int $status The response status code |
|||
* @param array $headers An array of response headers |
|||
* @param bool $public Files are public by default |
|||
* @param string|null $contentDisposition The type of Content-Disposition to set automatically with the filename |
|||
* @param bool $autoEtag Whether the ETag header should be automatically set |
|||
* @param bool $autoLastModified Whether the Last-Modified header should be automatically set |
|||
*/ |
|||
public function __construct($file, int $status = 200, array $headers = [], bool $public = true, string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true) |
|||
{ |
|||
parent::__construct(null, $status, $headers); |
|||
|
|||
$this->setFile($file, $contentDisposition, $autoEtag, $autoLastModified); |
|||
|
|||
if ($public) { |
|||
$this->setPublic(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* @param \SplFileInfo|string $file The file to stream |
|||
* @param int $status The response status code |
|||
* @param array $headers An array of response headers |
|||
* @param bool $public Files are public by default |
|||
* @param string|null $contentDisposition The type of Content-Disposition to set automatically with the filename |
|||
* @param bool $autoEtag Whether the ETag header should be automatically set |
|||
* @param bool $autoLastModified Whether the Last-Modified header should be automatically set |
|||
* |
|||
* @return static |
|||
*/ |
|||
public static function create($file = null, $status = 200, $headers = [], $public = true, $contentDisposition = null, $autoEtag = false, $autoLastModified = true) |
|||
{ |
|||
return new static($file, $status, $headers, $public, $contentDisposition, $autoEtag, $autoLastModified); |
|||
} |
|||
|
|||
/** |
|||
* Sets the file to stream. |
|||
* |
|||
* @param \SplFileInfo|string $file The file to stream |
|||
* @param string $contentDisposition |
|||
* @param bool $autoEtag |
|||
* @param bool $autoLastModified |
|||
* |
|||
* @return $this |
|||
* |
|||
* @throws FileException |
|||
*/ |
|||
public function setFile($file, $contentDisposition = null, $autoEtag = false, $autoLastModified = true) |
|||
{ |
|||
if (!$file instanceof File) { |
|||
if ($file instanceof \SplFileInfo) { |
|||
$file = new File($file->getPathname()); |
|||
} else { |
|||
$file = new File((string) $file); |
|||
} |
|||
} |
|||
|
|||
if (!$file->isReadable()) { |
|||
throw new FileException('File must be readable.'); |
|||
} |
|||
|
|||
$this->file = $file; |
|||
|
|||
if ($autoEtag) { |
|||
$this->setAutoEtag(); |
|||
} |
|||
|
|||
if ($autoLastModified) { |
|||
$this->setAutoLastModified(); |
|||
} |
|||
|
|||
if ($contentDisposition) { |
|||
$this->setContentDisposition($contentDisposition); |
|||
} |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Gets the file. |
|||
* |
|||
* @return File The file to stream |
|||
*/ |
|||
public function getFile() |
|||
{ |
|||
return $this->file; |
|||
} |
|||
|
|||
/** |
|||
* Automatically sets the Last-Modified header according the file modification date. |
|||
*/ |
|||
public function setAutoLastModified() |
|||
{ |
|||
$this->setLastModified(\DateTime::createFromFormat('U', $this->file->getMTime())); |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Automatically sets the ETag header according to the checksum of the file. |
|||
*/ |
|||
public function setAutoEtag() |
|||
{ |
|||
$this->setEtag(base64_encode(hash_file('sha256', $this->file->getPathname(), true))); |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Sets the Content-Disposition header with the given filename. |
|||
* |
|||
* @param string $disposition ResponseHeaderBag::DISPOSITION_INLINE or ResponseHeaderBag::DISPOSITION_ATTACHMENT |
|||
* @param string $filename Optionally use this UTF-8 encoded filename instead of the real name of the file |
|||
* @param string $filenameFallback A fallback filename, containing only ASCII characters. Defaults to an automatically encoded filename |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function setContentDisposition($disposition, $filename = '', $filenameFallback = '') |
|||
{ |
|||
if ('' === $filename) { |
|||
$filename = $this->file->getFilename(); |
|||
} |
|||
|
|||
if ('' === $filenameFallback && (!preg_match('/^[\x20-\x7e]*$/', $filename) || false !== strpos($filename, '%'))) { |
|||
$encoding = mb_detect_encoding($filename, null, true) ?: '8bit'; |
|||
|
|||
for ($i = 0, $filenameLength = mb_strlen($filename, $encoding); $i < $filenameLength; ++$i) { |
|||
$char = mb_substr($filename, $i, 1, $encoding); |
|||
|
|||
if ('%' === $char || \ord($char) < 32 || \ord($char) > 126) { |
|||
$filenameFallback .= '_'; |
|||
} else { |
|||
$filenameFallback .= $char; |
|||
} |
|||
} |
|||
} |
|||
|
|||
$dispositionHeader = $this->headers->makeDisposition($disposition, $filename, $filenameFallback); |
|||
$this->headers->set('Content-Disposition', $dispositionHeader); |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function prepare(Request $request) |
|||
{ |
|||
if (!$this->headers->has('Content-Type')) { |
|||
$this->headers->set('Content-Type', $this->file->getMimeType() ?: 'application/octet-stream'); |
|||
} |
|||
|
|||
if ('HTTP/1.0' !== $request->server->get('SERVER_PROTOCOL')) { |
|||
$this->setProtocolVersion('1.1'); |
|||
} |
|||
|
|||
$this->ensureIEOverSSLCompatibility($request); |
|||
|
|||
$this->offset = 0; |
|||
$this->maxlen = -1; |
|||
|
|||
if (false === $fileSize = $this->file->getSize()) { |
|||
return $this; |
|||
} |
|||
$this->headers->set('Content-Length', $fileSize); |
|||
|
|||
if (!$this->headers->has('Accept-Ranges')) { |
|||
// Only accept ranges on safe HTTP methods |
|||
$this->headers->set('Accept-Ranges', $request->isMethodSafe(false) ? 'bytes' : 'none'); |
|||
} |
|||
|
|||
if (self::$trustXSendfileTypeHeader && $request->headers->has('X-Sendfile-Type')) { |
|||
// Use X-Sendfile, do not send any content. |
|||
$type = $request->headers->get('X-Sendfile-Type'); |
|||
$path = $this->file->getRealPath(); |
|||
// Fall back to scheme://path for stream wrapped locations. |
|||
if (false === $path) { |
|||
$path = $this->file->getPathname(); |
|||
} |
|||
if ('x-accel-redirect' === strtolower($type)) { |
|||
// Do X-Accel-Mapping substitutions. |
|||
// @link http://wiki.nginx.org/X-accel#X-Accel-Redirect |
|||
$parts = HeaderUtils::split($request->headers->get('X-Accel-Mapping', ''), ',='); |
|||
foreach ($parts as $part) { |
|||
list($pathPrefix, $location) = $part; |
|||
if (substr($path, 0, \strlen($pathPrefix)) === $pathPrefix) { |
|||
$path = $location.substr($path, \strlen($pathPrefix)); |
|||
// Only set X-Accel-Redirect header if a valid URI can be produced |
|||
// as nginx does not serve arbitrary file paths. |
|||
$this->headers->set($type, $path); |
|||
$this->maxlen = 0; |
|||
break; |
|||
} |
|||
} |
|||
} else { |
|||
$this->headers->set($type, $path); |
|||
$this->maxlen = 0; |
|||
} |
|||
} elseif ($request->headers->has('Range')) { |
|||
// Process the range headers. |
|||
if (!$request->headers->has('If-Range') || $this->hasValidIfRangeHeader($request->headers->get('If-Range'))) { |
|||
$range = $request->headers->get('Range'); |
|||
|
|||
list($start, $end) = explode('-', substr($range, 6), 2) + [0]; |
|||
|
|||
$end = ('' === $end) ? $fileSize - 1 : (int) $end; |
|||
|
|||
if ('' === $start) { |
|||
$start = $fileSize - $end; |
|||
$end = $fileSize - 1; |
|||
} else { |
|||
$start = (int) $start; |
|||
} |
|||
|
|||
if ($start <= $end) { |
|||
if ($start < 0 || $end > $fileSize - 1) { |
|||
$this->setStatusCode(416); |
|||
$this->headers->set('Content-Range', sprintf('bytes */%s', $fileSize)); |
|||
} elseif (0 !== $start || $end !== $fileSize - 1) { |
|||
$this->maxlen = $end < $fileSize ? $end - $start + 1 : -1; |
|||
$this->offset = $start; |
|||
|
|||
$this->setStatusCode(206); |
|||
$this->headers->set('Content-Range', sprintf('bytes %s-%s/%s', $start, $end, $fileSize)); |
|||
$this->headers->set('Content-Length', $end - $start + 1); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
private function hasValidIfRangeHeader($header) |
|||
{ |
|||
if ($this->getEtag() === $header) { |
|||
return true; |
|||
} |
|||
|
|||
if (null === $lastModified = $this->getLastModified()) { |
|||
return false; |
|||
} |
|||
|
|||
return $lastModified->format('D, d M Y H:i:s').' GMT' === $header; |
|||
} |
|||
|
|||
/** |
|||
* Sends the file. |
|||
* |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function sendContent() |
|||
{ |
|||
if (!$this->isSuccessful()) { |
|||
return parent::sendContent(); |
|||
} |
|||
|
|||
if (0 === $this->maxlen) { |
|||
return $this; |
|||
} |
|||
|
|||
$out = fopen('php://output', 'wb'); |
|||
$file = fopen($this->file->getPathname(), 'rb'); |
|||
|
|||
stream_copy_to_stream($file, $out, $this->maxlen, $this->offset); |
|||
|
|||
fclose($out); |
|||
fclose($file); |
|||
|
|||
if ($this->deleteFileAfterSend && file_exists($this->file->getPathname())) { |
|||
unlink($this->file->getPathname()); |
|||
} |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
* |
|||
* @throws \LogicException when the content is not null |
|||
*/ |
|||
public function setContent($content) |
|||
{ |
|||
if (null !== $content) { |
|||
throw new \LogicException('The content cannot be set on a BinaryFileResponse instance.'); |
|||
} |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getContent() |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* Trust X-Sendfile-Type header. |
|||
*/ |
|||
public static function trustXSendfileTypeHeader() |
|||
{ |
|||
self::$trustXSendfileTypeHeader = true; |
|||
} |
|||
|
|||
/** |
|||
* If this is set to true, the file will be unlinked after the request is send |
|||
* Note: If the X-Sendfile header is used, the deleteFileAfterSend setting will not be used. |
|||
* |
|||
* @param bool $shouldDelete |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function deleteFileAfterSend($shouldDelete = true) |
|||
{ |
|||
$this->deleteFileAfterSend = $shouldDelete; |
|||
|
|||
return $this; |
|||
} |
|||
} |
|||
@ -0,0 +1,222 @@ |
|||
CHANGELOG |
|||
========= |
|||
|
|||
4.3.0 |
|||
----- |
|||
|
|||
* added PHPUnit constraints: `RequestAttributeValueSame`, `ResponseCookieValueSame`, `ResponseHasCookie`, |
|||
`ResponseHasHeader`, `ResponseHeaderSame`, `ResponseIsRedirected`, `ResponseIsSuccessful`, and `ResponseStatusCodeSame` |
|||
* deprecated `MimeTypeGuesserInterface` and `ExtensionGuesserInterface` in favor of `Symfony\Component\Mime\MimeTypesInterface`. |
|||
* deprecated `MimeType` and `MimeTypeExtensionGuesser` in favor of `Symfony\Component\Mime\MimeTypes`. |
|||
* deprecated `FileBinaryMimeTypeGuesser` in favor of `Symfony\Component\Mime\FileBinaryMimeTypeGuesser`. |
|||
* deprecated `FileinfoMimeTypeGuesser` in favor of `Symfony\Component\Mime\FileinfoMimeTypeGuesser`. |
|||
* added `UrlHelper` that allows to get an absolute URL and a relative path for a given path |
|||
|
|||
4.2.0 |
|||
----- |
|||
|
|||
* the default value of the "$secure" and "$samesite" arguments of Cookie's constructor |
|||
will respectively change from "false" to "null" and from "null" to "lax" in Symfony |
|||
5.0, you should define their values explicitly or use "Cookie::create()" instead. |
|||
* added `matchPort()` in RequestMatcher |
|||
|
|||
4.1.3 |
|||
----- |
|||
|
|||
* [BC BREAK] Support for the IIS-only `X_ORIGINAL_URL` and `X_REWRITE_URL` |
|||
HTTP headers has been dropped for security reasons. |
|||
|
|||
4.1.0 |
|||
----- |
|||
|
|||
* Query string normalization uses `parse_str()` instead of custom parsing logic. |
|||
* Passing the file size to the constructor of the `UploadedFile` class is deprecated. |
|||
* The `getClientSize()` method of the `UploadedFile` class is deprecated. Use `getSize()` instead. |
|||
* added `RedisSessionHandler` to use Redis as a session storage |
|||
* The `get()` method of the `AcceptHeader` class now takes into account the |
|||
`*` and `*/*` default values (if they are present in the Accept HTTP header) |
|||
when looking for items. |
|||
* deprecated `Request::getSession()` when no session has been set. Use `Request::hasSession()` instead. |
|||
* added `CannotWriteFileException`, `ExtensionFileException`, `FormSizeFileException`, |
|||
`IniSizeFileException`, `NoFileException`, `NoTmpDirFileException`, `PartialFileException` to |
|||
handle failed `UploadedFile`. |
|||
* added `MigratingSessionHandler` for migrating between two session handlers without losing sessions |
|||
* added `HeaderUtils`. |
|||
|
|||
4.0.0 |
|||
----- |
|||
|
|||
* the `Request::setTrustedHeaderName()` and `Request::getTrustedHeaderName()` |
|||
methods have been removed |
|||
* the `Request::HEADER_CLIENT_IP` constant has been removed, use |
|||
`Request::HEADER_X_FORWARDED_FOR` instead |
|||
* the `Request::HEADER_CLIENT_HOST` constant has been removed, use |
|||
`Request::HEADER_X_FORWARDED_HOST` instead |
|||
* the `Request::HEADER_CLIENT_PROTO` constant has been removed, use |
|||
`Request::HEADER_X_FORWARDED_PROTO` instead |
|||
* the `Request::HEADER_CLIENT_PORT` constant has been removed, use |
|||
`Request::HEADER_X_FORWARDED_PORT` instead |
|||
* checking for cacheable HTTP methods using the `Request::isMethodSafe()` |
|||
method (by not passing `false` as its argument) is not supported anymore and |
|||
throws a `\BadMethodCallException` |
|||
* the `WriteCheckSessionHandler`, `NativeSessionHandler` and `NativeProxy` classes have been removed |
|||
* setting session save handlers that do not implement `\SessionHandlerInterface` in |
|||
`NativeSessionStorage::setSaveHandler()` is not supported anymore and throws a |
|||
`\TypeError` |
|||
|
|||
3.4.0 |
|||
----- |
|||
|
|||
* implemented PHP 7.0's `SessionUpdateTimestampHandlerInterface` with a new |
|||
`AbstractSessionHandler` base class and a new `StrictSessionHandler` wrapper |
|||
* deprecated the `WriteCheckSessionHandler`, `NativeSessionHandler` and `NativeProxy` classes |
|||
* deprecated setting session save handlers that do not implement `\SessionHandlerInterface` in `NativeSessionStorage::setSaveHandler()` |
|||
* deprecated using `MongoDbSessionHandler` with the legacy mongo extension; use it with the mongodb/mongodb package and ext-mongodb instead |
|||
* deprecated `MemcacheSessionHandler`; use `MemcachedSessionHandler` instead |
|||
|
|||
3.3.0 |
|||
----- |
|||
|
|||
* the `Request::setTrustedProxies()` method takes a new `$trustedHeaderSet` argument, |
|||
see https://symfony.com/doc/current/deployment/proxies.html for more info, |
|||
* deprecated the `Request::setTrustedHeaderName()` and `Request::getTrustedHeaderName()` methods, |
|||
* added `File\Stream`, to be passed to `BinaryFileResponse` when the size of the served file is unknown, |
|||
disabling `Range` and `Content-Length` handling, switching to chunked encoding instead |
|||
* added the `Cookie::fromString()` method that allows to create a cookie from a |
|||
raw header string |
|||
|
|||
3.1.0 |
|||
----- |
|||
|
|||
* Added support for creating `JsonResponse` with a string of JSON data |
|||
|
|||
3.0.0 |
|||
----- |
|||
|
|||
* The precedence of parameters returned from `Request::get()` changed from "GET, PATH, BODY" to "PATH, GET, BODY" |
|||
|
|||
2.8.0 |
|||
----- |
|||
|
|||
* Finding deep items in `ParameterBag::get()` is deprecated since version 2.8 and |
|||
will be removed in 3.0. |
|||
|
|||
2.6.0 |
|||
----- |
|||
|
|||
* PdoSessionHandler changes |
|||
- implemented different session locking strategies to prevent loss of data by concurrent access to the same session |
|||
- [BC BREAK] save session data in a binary column without base64_encode |
|||
- [BC BREAK] added lifetime column to the session table which allows to have different lifetimes for each session |
|||
- implemented lazy connections that are only opened when a session is used by either passing a dsn string |
|||
explicitly or falling back to session.save_path ini setting |
|||
- added a createTable method that initializes a correctly defined table depending on the database vendor |
|||
|
|||
2.5.0 |
|||
----- |
|||
|
|||
* added `JsonResponse::setEncodingOptions()` & `JsonResponse::getEncodingOptions()` for easier manipulation |
|||
of the options used while encoding data to JSON format. |
|||
|
|||
2.4.0 |
|||
----- |
|||
|
|||
* added RequestStack |
|||
* added Request::getEncodings() |
|||
* added accessors methods to session handlers |
|||
|
|||
2.3.0 |
|||
----- |
|||
|
|||
* added support for ranges of IPs in trusted proxies |
|||
* `UploadedFile::isValid` now returns false if the file was not uploaded via HTTP (in a non-test mode) |
|||
* Improved error-handling of `\Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler` |
|||
to ensure the supplied PDO handler throws Exceptions on error (as the class expects). Added related test cases |
|||
to verify that Exceptions are properly thrown when the PDO queries fail. |
|||
|
|||
2.2.0 |
|||
----- |
|||
|
|||
* fixed the Request::create() precedence (URI information always take precedence now) |
|||
* added Request::getTrustedProxies() |
|||
* deprecated Request::isProxyTrusted() |
|||
* [BC BREAK] JsonResponse does not turn a top level empty array to an object anymore, use an ArrayObject to enforce objects |
|||
* added a IpUtils class to check if an IP belongs to a CIDR |
|||
* added Request::getRealMethod() to get the "real" HTTP method (getMethod() returns the "intended" HTTP method) |
|||
* disabled _method request parameter support by default (call Request::enableHttpMethodParameterOverride() to |
|||
enable it, and Request::getHttpMethodParameterOverride() to check if it is supported) |
|||
* Request::splitHttpAcceptHeader() method is deprecated and will be removed in 2.3 |
|||
* Deprecated Flashbag::count() and \Countable interface, will be removed in 2.3 |
|||
|
|||
2.1.0 |
|||
----- |
|||
|
|||
* 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 |
|||
behavior of messages auto expiring after one page page load. Messages must |
|||
be retrieved by `get()` or `all()`. |
|||
* Added `Symfony\Component\HttpFoundation\Attribute\AttributeBag` to replicate |
|||
attributes storage behavior from 2.0.x (default). |
|||
* Added `Symfony\Component\HttpFoundation\Attribute\NamespacedAttributeBag` for |
|||
namespace session attributes. |
|||
* Flash API can stores messages in an array so there may be multiple messages |
|||
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. |
|||
@ -0,0 +1,298 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation; |
|||
|
|||
/** |
|||
* Represents a cookie. |
|||
* |
|||
* @author Johannes M. Schmitt <schmittjoh@gmail.com> |
|||
*/ |
|||
class Cookie |
|||
{ |
|||
protected $name; |
|||
protected $value; |
|||
protected $domain; |
|||
protected $expire; |
|||
protected $path; |
|||
protected $secure; |
|||
protected $httpOnly; |
|||
private $raw; |
|||
private $sameSite; |
|||
private $secureDefault = false; |
|||
|
|||
const SAMESITE_NONE = 'none'; |
|||
const SAMESITE_LAX = 'lax'; |
|||
const SAMESITE_STRICT = 'strict'; |
|||
|
|||
/** |
|||
* Creates cookie from raw header string. |
|||
* |
|||
* @param string $cookie |
|||
* @param bool $decode |
|||
* |
|||
* @return static |
|||
*/ |
|||
public static function fromString($cookie, $decode = false) |
|||
{ |
|||
$data = [ |
|||
'expires' => 0, |
|||
'path' => '/', |
|||
'domain' => null, |
|||
'secure' => false, |
|||
'httponly' => false, |
|||
'raw' => !$decode, |
|||
'samesite' => null, |
|||
]; |
|||
|
|||
$parts = HeaderUtils::split($cookie, ';='); |
|||
$part = array_shift($parts); |
|||
|
|||
$name = $decode ? urldecode($part[0]) : $part[0]; |
|||
$value = isset($part[1]) ? ($decode ? urldecode($part[1]) : $part[1]) : null; |
|||
|
|||
$data = HeaderUtils::combine($parts) + $data; |
|||
|
|||
if (isset($data['max-age'])) { |
|||
$data['expires'] = time() + (int) $data['max-age']; |
|||
} |
|||
|
|||
return new static($name, $value, $data['expires'], $data['path'], $data['domain'], $data['secure'], $data['httponly'], $data['raw'], $data['samesite']); |
|||
} |
|||
|
|||
public static function create(string $name, string $value = null, $expire = 0, ?string $path = '/', string $domain = null, bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = self::SAMESITE_LAX): self |
|||
{ |
|||
return new self($name, $value, $expire, $path, $domain, $secure, $httpOnly, $raw, $sameSite); |
|||
} |
|||
|
|||
/** |
|||
* @param string $name The name of the cookie |
|||
* @param string|null $value The value of the cookie |
|||
* @param int|string|\DateTimeInterface $expire The time the cookie expires |
|||
* @param string $path The path on the server in which the cookie will be available on |
|||
* @param string|null $domain The domain that the cookie is available to |
|||
* @param bool|null $secure Whether the client should send back the cookie only over HTTPS or null to auto-enable this when the request is already using HTTPS |
|||
* @param bool $httpOnly Whether the cookie will be made accessible only through the HTTP protocol |
|||
* @param bool $raw Whether the cookie value should be sent with no url encoding |
|||
* @param string|null $sameSite Whether the cookie will be available for cross-site requests |
|||
* |
|||
* @throws \InvalidArgumentException |
|||
*/ |
|||
public function __construct(string $name, string $value = null, $expire = 0, ?string $path = '/', string $domain = null, ?bool $secure = false, bool $httpOnly = true, bool $raw = false, string $sameSite = null) |
|||
{ |
|||
if (9 > \func_num_args()) { |
|||
@trigger_error(sprintf('The default value of the "$secure" and "$samesite" arguments of "%s"\'s constructor will respectively change from "false" to "null" and from "null" to "lax" in Symfony 5.0, you should define their values explicitly or use "Cookie::create()" instead.', __METHOD__), E_USER_DEPRECATED); |
|||
} |
|||
|
|||
// from PHP source code |
|||
if (preg_match("/[=,; \t\r\n\013\014]/", $name)) { |
|||
throw new \InvalidArgumentException(sprintf('The cookie name "%s" contains invalid characters.', $name)); |
|||
} |
|||
|
|||
if (empty($name)) { |
|||
throw new \InvalidArgumentException('The cookie name cannot be empty.'); |
|||
} |
|||
|
|||
// convert expiration time to a Unix timestamp |
|||
if ($expire instanceof \DateTimeInterface) { |
|||
$expire = $expire->format('U'); |
|||
} elseif (!is_numeric($expire)) { |
|||
$expire = strtotime($expire); |
|||
|
|||
if (false === $expire) { |
|||
throw new \InvalidArgumentException('The cookie expiration time is not valid.'); |
|||
} |
|||
} |
|||
|
|||
$this->name = $name; |
|||
$this->value = $value; |
|||
$this->domain = $domain; |
|||
$this->expire = 0 < $expire ? (int) $expire : 0; |
|||
$this->path = empty($path) ? '/' : $path; |
|||
$this->secure = $secure; |
|||
$this->httpOnly = $httpOnly; |
|||
$this->raw = $raw; |
|||
|
|||
if ('' === $sameSite) { |
|||
$sameSite = null; |
|||
} elseif (null !== $sameSite) { |
|||
$sameSite = strtolower($sameSite); |
|||
} |
|||
|
|||
if (!\in_array($sameSite, [self::SAMESITE_LAX, self::SAMESITE_STRICT, self::SAMESITE_NONE, null], true)) { |
|||
throw new \InvalidArgumentException('The "sameSite" parameter value is not valid.'); |
|||
} |
|||
|
|||
$this->sameSite = $sameSite; |
|||
} |
|||
|
|||
/** |
|||
* Returns the cookie as a string. |
|||
* |
|||
* @return string The cookie |
|||
*/ |
|||
public function __toString() |
|||
{ |
|||
$str = ($this->isRaw() ? $this->getName() : urlencode($this->getName())).'='; |
|||
|
|||
if ('' === (string) $this->getValue()) { |
|||
$str .= 'deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; Max-Age=0'; |
|||
} else { |
|||
$str .= $this->isRaw() ? $this->getValue() : rawurlencode($this->getValue()); |
|||
|
|||
if (0 !== $this->getExpiresTime()) { |
|||
$str .= '; expires='.gmdate('D, d-M-Y H:i:s T', $this->getExpiresTime()).'; Max-Age='.$this->getMaxAge(); |
|||
} |
|||
} |
|||
|
|||
if ($this->getPath()) { |
|||
$str .= '; path='.$this->getPath(); |
|||
} |
|||
|
|||
if ($this->getDomain()) { |
|||
$str .= '; domain='.$this->getDomain(); |
|||
} |
|||
|
|||
if (true === $this->isSecure()) { |
|||
$str .= '; secure'; |
|||
} |
|||
|
|||
if (true === $this->isHttpOnly()) { |
|||
$str .= '; httponly'; |
|||
} |
|||
|
|||
if (null !== $this->getSameSite()) { |
|||
$str .= '; samesite='.$this->getSameSite(); |
|||
} |
|||
|
|||
return $str; |
|||
} |
|||
|
|||
/** |
|||
* Gets the name of the cookie. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getName() |
|||
{ |
|||
return $this->name; |
|||
} |
|||
|
|||
/** |
|||
* Gets the value of the cookie. |
|||
* |
|||
* @return string|null |
|||
*/ |
|||
public function getValue() |
|||
{ |
|||
return $this->value; |
|||
} |
|||
|
|||
/** |
|||
* Gets the domain that the cookie is available to. |
|||
* |
|||
* @return string|null |
|||
*/ |
|||
public function getDomain() |
|||
{ |
|||
return $this->domain; |
|||
} |
|||
|
|||
/** |
|||
* Gets the time the cookie expires. |
|||
* |
|||
* @return int |
|||
*/ |
|||
public function getExpiresTime() |
|||
{ |
|||
return $this->expire; |
|||
} |
|||
|
|||
/** |
|||
* Gets the max-age attribute. |
|||
* |
|||
* @return int |
|||
*/ |
|||
public function getMaxAge() |
|||
{ |
|||
$maxAge = $this->expire - time(); |
|||
|
|||
return 0 >= $maxAge ? 0 : $maxAge; |
|||
} |
|||
|
|||
/** |
|||
* Gets the path on the server in which the cookie will be available on. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getPath() |
|||
{ |
|||
return $this->path; |
|||
} |
|||
|
|||
/** |
|||
* Checks whether the cookie should only be transmitted over a secure HTTPS connection from the client. |
|||
* |
|||
* @return bool |
|||
*/ |
|||
public function isSecure() |
|||
{ |
|||
return $this->secure ?? $this->secureDefault; |
|||
} |
|||
|
|||
/** |
|||
* Checks whether the cookie will be made accessible only through the HTTP protocol. |
|||
* |
|||
* @return bool |
|||
*/ |
|||
public function isHttpOnly() |
|||
{ |
|||
return $this->httpOnly; |
|||
} |
|||
|
|||
/** |
|||
* Whether this cookie is about to be cleared. |
|||
* |
|||
* @return bool |
|||
*/ |
|||
public function isCleared() |
|||
{ |
|||
return 0 !== $this->expire && $this->expire < time(); |
|||
} |
|||
|
|||
/** |
|||
* Checks if the cookie value should be sent with no url encoding. |
|||
* |
|||
* @return bool |
|||
*/ |
|||
public function isRaw() |
|||
{ |
|||
return $this->raw; |
|||
} |
|||
|
|||
/** |
|||
* Gets the SameSite attribute. |
|||
* |
|||
* @return string|null |
|||
*/ |
|||
public function getSameSite() |
|||
{ |
|||
return $this->sameSite; |
|||
} |
|||
|
|||
/** |
|||
* @param bool $default The default value of the "secure" flag when it is set to null |
|||
*/ |
|||
public function setSecureDefault(bool $default): void |
|||
{ |
|||
$this->secureDefault = $default; |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\Exception; |
|||
|
|||
/** |
|||
* The HTTP request contains headers with conflicting information. |
|||
* |
|||
* @author Magnus Nordlander <magnus@fervo.se> |
|||
*/ |
|||
class ConflictingHeadersException extends \UnexpectedValueException implements RequestExceptionInterface |
|||
{ |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\Exception; |
|||
|
|||
/** |
|||
* Interface for Request exceptions. |
|||
* |
|||
* Exceptions implementing this interface should trigger an HTTP 400 response in the application code. |
|||
*/ |
|||
interface RequestExceptionInterface |
|||
{ |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\Exception; |
|||
|
|||
/** |
|||
* Raised when a user has performed an operation that should be considered |
|||
* suspicious from a security perspective. |
|||
*/ |
|||
class SuspiciousOperationException extends \UnexpectedValueException implements RequestExceptionInterface |
|||
{ |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation; |
|||
|
|||
use Symfony\Component\ExpressionLanguage\ExpressionLanguage; |
|||
|
|||
/** |
|||
* ExpressionRequestMatcher uses an expression to match a Request. |
|||
* |
|||
* @author Fabien Potencier <fabien@symfony.com> |
|||
*/ |
|||
class ExpressionRequestMatcher extends RequestMatcher |
|||
{ |
|||
private $language; |
|||
private $expression; |
|||
|
|||
public function setExpression(ExpressionLanguage $language, $expression) |
|||
{ |
|||
$this->language = $language; |
|||
$this->expression = $expression; |
|||
} |
|||
|
|||
public function matches(Request $request) |
|||
{ |
|||
if (!$this->language) { |
|||
throw new \LogicException('Unable to match the request as the expression language is not available.'); |
|||
} |
|||
|
|||
return $this->language->evaluate($this->expression, [ |
|||
'request' => $request, |
|||
'method' => $request->getMethod(), |
|||
'path' => rawurldecode($request->getPathInfo()), |
|||
'host' => $request->getHost(), |
|||
'ip' => $request->getClientIp(), |
|||
'attributes' => $request->attributes->all(), |
|||
]) && parent::matches($request); |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\File\Exception; |
|||
|
|||
/** |
|||
* Thrown when the access on a file was denied. |
|||
* |
|||
* @author Bernhard Schussek <bschussek@gmail.com> |
|||
*/ |
|||
class AccessDeniedException extends FileException |
|||
{ |
|||
/** |
|||
* @param string $path The path to the accessed file |
|||
*/ |
|||
public function __construct(string $path) |
|||
{ |
|||
parent::__construct(sprintf('The file %s could not be accessed', $path)); |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\File\Exception; |
|||
|
|||
/** |
|||
* Thrown when an UPLOAD_ERR_CANT_WRITE error occurred with UploadedFile. |
|||
* |
|||
* @author Florent Mata <florentmata@gmail.com> |
|||
*/ |
|||
class CannotWriteFileException extends FileException |
|||
{ |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\File\Exception; |
|||
|
|||
/** |
|||
* Thrown when an UPLOAD_ERR_EXTENSION error occurred with UploadedFile. |
|||
* |
|||
* @author Florent Mata <florentmata@gmail.com> |
|||
*/ |
|||
class ExtensionFileException extends FileException |
|||
{ |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\File\Exception; |
|||
|
|||
/** |
|||
* Thrown when an error occurred in the component File. |
|||
* |
|||
* @author Bernhard Schussek <bschussek@gmail.com> |
|||
*/ |
|||
class FileException extends \RuntimeException |
|||
{ |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\File\Exception; |
|||
|
|||
/** |
|||
* Thrown when a file was not found. |
|||
* |
|||
* @author Bernhard Schussek <bschussek@gmail.com> |
|||
*/ |
|||
class FileNotFoundException extends FileException |
|||
{ |
|||
/** |
|||
* @param string $path The path to the file that was not found |
|||
*/ |
|||
public function __construct(string $path) |
|||
{ |
|||
parent::__construct(sprintf('The file "%s" does not exist', $path)); |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\File\Exception; |
|||
|
|||
/** |
|||
* Thrown when an UPLOAD_ERR_FORM_SIZE error occurred with UploadedFile. |
|||
* |
|||
* @author Florent Mata <florentmata@gmail.com> |
|||
*/ |
|||
class FormSizeFileException extends FileException |
|||
{ |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\File\Exception; |
|||
|
|||
/** |
|||
* Thrown when an UPLOAD_ERR_INI_SIZE error occurred with UploadedFile. |
|||
* |
|||
* @author Florent Mata <florentmata@gmail.com> |
|||
*/ |
|||
class IniSizeFileException extends FileException |
|||
{ |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\File\Exception; |
|||
|
|||
/** |
|||
* Thrown when an UPLOAD_ERR_NO_FILE error occurred with UploadedFile. |
|||
* |
|||
* @author Florent Mata <florentmata@gmail.com> |
|||
*/ |
|||
class NoFileException extends FileException |
|||
{ |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\File\Exception; |
|||
|
|||
/** |
|||
* Thrown when an UPLOAD_ERR_NO_TMP_DIR error occurred with UploadedFile. |
|||
* |
|||
* @author Florent Mata <florentmata@gmail.com> |
|||
*/ |
|||
class NoTmpDirFileException extends FileException |
|||
{ |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\File\Exception; |
|||
|
|||
/** |
|||
* Thrown when an UPLOAD_ERR_PARTIAL error occurred with UploadedFile. |
|||
* |
|||
* @author Florent Mata <florentmata@gmail.com> |
|||
*/ |
|||
class PartialFileException extends FileException |
|||
{ |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\File\Exception; |
|||
|
|||
class UnexpectedTypeException extends FileException |
|||
{ |
|||
public function __construct($value, string $expectedType) |
|||
{ |
|||
parent::__construct(sprintf('Expected argument of type %s, %s given', $expectedType, \is_object($value) ? \get_class($value) : \gettype($value))); |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\File\Exception; |
|||
|
|||
/** |
|||
* Thrown when an error occurred during file upload. |
|||
* |
|||
* @author Bernhard Schussek <bschussek@gmail.com> |
|||
*/ |
|||
class UploadException extends FileException |
|||
{ |
|||
} |
|||
@ -0,0 +1,132 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\File; |
|||
|
|||
use Symfony\Component\HttpFoundation\File\Exception\FileException; |
|||
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; |
|||
use Symfony\Component\Mime\MimeTypes; |
|||
|
|||
/** |
|||
* A file in the file system. |
|||
* |
|||
* @author Bernhard Schussek <bschussek@gmail.com> |
|||
*/ |
|||
class File extends \SplFileInfo |
|||
{ |
|||
/** |
|||
* Constructs a new file from the given path. |
|||
* |
|||
* @param string $path The path to the file |
|||
* @param bool $checkPath Whether to check the path or not |
|||
* |
|||
* @throws FileNotFoundException If the given path is not a file |
|||
*/ |
|||
public function __construct(string $path, bool $checkPath = true) |
|||
{ |
|||
if ($checkPath && !is_file($path)) { |
|||
throw new FileNotFoundException($path); |
|||
} |
|||
|
|||
parent::__construct($path); |
|||
} |
|||
|
|||
/** |
|||
* Returns the extension based on the mime type. |
|||
* |
|||
* If the mime type is unknown, returns null. |
|||
* |
|||
* This method uses the mime type as guessed by getMimeType() |
|||
* to guess the file extension. |
|||
* |
|||
* @return string|null The guessed extension or null if it cannot be guessed |
|||
* |
|||
* @see MimeTypes |
|||
* @see getMimeType() |
|||
*/ |
|||
public function guessExtension() |
|||
{ |
|||
return MimeTypes::getDefault()->getExtensions($this->getMimeType())[0] ?? null; |
|||
} |
|||
|
|||
/** |
|||
* Returns the mime type of the file. |
|||
* |
|||
* The mime type is guessed using a MimeTypeGuesserInterface instance, |
|||
* which uses finfo_file() then the "file" system binary, |
|||
* depending on which of those are available. |
|||
* |
|||
* @return string|null The guessed mime type (e.g. "application/pdf") |
|||
* |
|||
* @see MimeTypes |
|||
*/ |
|||
public function getMimeType() |
|||
{ |
|||
return MimeTypes::getDefault()->guessMimeType($this->getPathname()); |
|||
} |
|||
|
|||
/** |
|||
* Moves the file to a new location. |
|||
* |
|||
* @param string $directory The destination folder |
|||
* @param string $name The new file name |
|||
* |
|||
* @return self A File object representing the new file |
|||
* |
|||
* @throws FileException if the target file could not be created |
|||
*/ |
|||
public function move($directory, $name = null) |
|||
{ |
|||
$target = $this->getTargetFile($directory, $name); |
|||
|
|||
set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; }); |
|||
$renamed = rename($this->getPathname(), $target); |
|||
restore_error_handler(); |
|||
if (!$renamed) { |
|||
throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $this->getPathname(), $target, strip_tags($error))); |
|||
} |
|||
|
|||
@chmod($target, 0666 & ~umask()); |
|||
|
|||
return $target; |
|||
} |
|||
|
|||
protected function getTargetFile($directory, $name = null) |
|||
{ |
|||
if (!is_dir($directory)) { |
|||
if (false === @mkdir($directory, 0777, true) && !is_dir($directory)) { |
|||
throw new FileException(sprintf('Unable to create the "%s" directory', $directory)); |
|||
} |
|||
} elseif (!is_writable($directory)) { |
|||
throw new FileException(sprintf('Unable to write in the "%s" directory', $directory)); |
|||
} |
|||
|
|||
$target = rtrim($directory, '/\\').\DIRECTORY_SEPARATOR.(null === $name ? $this->getBasename() : $this->getName($name)); |
|||
|
|||
return new self($target, false); |
|||
} |
|||
|
|||
/** |
|||
* 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 $originalName; |
|||
} |
|||
} |
|||
@ -0,0 +1,102 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\File\MimeType; |
|||
|
|||
use Symfony\Component\Mime\MimeTypes; |
|||
|
|||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" instead.', ExtensionGuesser::class, MimeTypes::class), E_USER_DEPRECATED); |
|||
|
|||
/** |
|||
* A singleton mime type to file extension guesser. |
|||
* |
|||
* A default guesser is provided. |
|||
* You can register custom guessers by calling the register() |
|||
* method on the singleton instance: |
|||
* |
|||
* $guesser = ExtensionGuesser::getInstance(); |
|||
* $guesser->register(new MyCustomExtensionGuesser()); |
|||
* |
|||
* The last registered guesser is preferred over previously registered ones. |
|||
* |
|||
* @deprecated since Symfony 4.3, use {@link MimeTypes} instead |
|||
*/ |
|||
class ExtensionGuesser implements ExtensionGuesserInterface |
|||
{ |
|||
/** |
|||
* The singleton instance. |
|||
* |
|||
* @var ExtensionGuesser |
|||
*/ |
|||
private static $instance = null; |
|||
|
|||
/** |
|||
* All registered ExtensionGuesserInterface instances. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $guessers = []; |
|||
|
|||
/** |
|||
* Returns the singleton instance. |
|||
* |
|||
* @return self |
|||
*/ |
|||
public static function getInstance() |
|||
{ |
|||
if (null === self::$instance) { |
|||
self::$instance = new self(); |
|||
} |
|||
|
|||
return self::$instance; |
|||
} |
|||
|
|||
/** |
|||
* Registers all natively provided extension guessers. |
|||
*/ |
|||
private function __construct() |
|||
{ |
|||
$this->register(new MimeTypeExtensionGuesser()); |
|||
} |
|||
|
|||
/** |
|||
* Registers a new extension guesser. |
|||
* |
|||
* When guessing, this guesser is preferred over previously registered ones. |
|||
*/ |
|||
public function register(ExtensionGuesserInterface $guesser) |
|||
{ |
|||
array_unshift($this->guessers, $guesser); |
|||
} |
|||
|
|||
/** |
|||
* Tries to guess the extension. |
|||
* |
|||
* The mime type is passed to each registered mime type guesser in reverse order |
|||
* of their registration (last registered is queried first). Once a guesser |
|||
* returns a value that is not NULL, this method terminates and returns the |
|||
* value. |
|||
* |
|||
* @param string $mimeType The mime type |
|||
* |
|||
* @return string The guessed extension or NULL, if none could be guessed |
|||
*/ |
|||
public function guess($mimeType) |
|||
{ |
|||
foreach ($this->guessers as $guesser) { |
|||
if (null !== $extension = $guesser->guess($mimeType)) { |
|||
return $extension; |
|||
} |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\File\MimeType; |
|||
|
|||
use Symfony\Component\Mime\MimeTypesInterface; |
|||
|
|||
/** |
|||
* Guesses the file extension corresponding to a given mime type. |
|||
* |
|||
* @deprecated since Symfony 4.3, use {@link MimeTypesInterface} instead |
|||
*/ |
|||
interface ExtensionGuesserInterface |
|||
{ |
|||
/** |
|||
* Makes a best guess for a file extension, given a mime type. |
|||
* |
|||
* @param string $mimeType The mime type |
|||
* |
|||
* @return string The guessed extension or NULL, if none could be guessed |
|||
*/ |
|||
public function guess($mimeType); |
|||
} |
|||
@ -0,0 +1,104 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\File\MimeType; |
|||
|
|||
use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException; |
|||
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; |
|||
use Symfony\Component\Mime\FileBinaryMimeTypeGuesser as NewFileBinaryMimeTypeGuesser; |
|||
|
|||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" instead.', FileBinaryMimeTypeGuesser::class, NewFileBinaryMimeTypeGuesser::class), E_USER_DEPRECATED); |
|||
|
|||
/** |
|||
* Guesses the mime type with the binary "file" (only available on *nix). |
|||
* |
|||
* @author Bernhard Schussek <bschussek@gmail.com> |
|||
* |
|||
* @deprecated since Symfony 4.3, use {@link NewFileBinaryMimeTypeGuesser} instead |
|||
*/ |
|||
class FileBinaryMimeTypeGuesser implements MimeTypeGuesserInterface |
|||
{ |
|||
private $cmd; |
|||
|
|||
/** |
|||
* The $cmd pattern must contain a "%s" string that will be replaced |
|||
* with the file name to guess. |
|||
* |
|||
* The command output must start with the mime type of the file. |
|||
* |
|||
* @param string $cmd The command to run to get the mime type of a file |
|||
*/ |
|||
public function __construct(string $cmd = 'file -b --mime %s 2>/dev/null') |
|||
{ |
|||
$this->cmd = $cmd; |
|||
} |
|||
|
|||
/** |
|||
* Returns whether this guesser is supported on the current OS. |
|||
* |
|||
* @return bool |
|||
*/ |
|||
public static function isSupported() |
|||
{ |
|||
static $supported = null; |
|||
|
|||
if (null !== $supported) { |
|||
return $supported; |
|||
} |
|||
|
|||
if ('\\' === \DIRECTORY_SEPARATOR || !\function_exists('passthru') || !\function_exists('escapeshellarg')) { |
|||
return $supported = false; |
|||
} |
|||
|
|||
ob_start(); |
|||
passthru('command -v file', $exitStatus); |
|||
$binPath = trim(ob_get_clean()); |
|||
|
|||
return $supported = 0 === $exitStatus && '' !== $binPath; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function guess($path) |
|||
{ |
|||
if (!is_file($path)) { |
|||
throw new FileNotFoundException($path); |
|||
} |
|||
|
|||
if (!is_readable($path)) { |
|||
throw new AccessDeniedException($path); |
|||
} |
|||
|
|||
if (!self::isSupported()) { |
|||
return null; |
|||
} |
|||
|
|||
ob_start(); |
|||
|
|||
// need to use --mime instead of -i. see #6641 |
|||
passthru(sprintf($this->cmd, escapeshellarg($path)), $return); |
|||
if ($return > 0) { |
|||
ob_end_clean(); |
|||
|
|||
return null; |
|||
} |
|||
|
|||
$type = trim(ob_get_clean()); |
|||
|
|||
if (!preg_match('#^([a-z0-9\-]+/[a-z0-9\-\.]+)#i', $type, $match)) { |
|||
// it's not a type, but an error message |
|||
return null; |
|||
} |
|||
|
|||
return $match[1]; |
|||
} |
|||
} |
|||
@ -0,0 +1,74 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\File\MimeType; |
|||
|
|||
use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException; |
|||
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; |
|||
use Symfony\Component\Mime\FileinfoMimeTypeGuesser as NewFileinfoMimeTypeGuesser; |
|||
|
|||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" instead.', FileinfoMimeTypeGuesser::class, NewFileinfoMimeTypeGuesser::class), E_USER_DEPRECATED); |
|||
|
|||
/** |
|||
* Guesses the mime type using the PECL extension FileInfo. |
|||
* |
|||
* @author Bernhard Schussek <bschussek@gmail.com> |
|||
* |
|||
* @deprecated since Symfony 4.3, use {@link NewFileinfoMimeTypeGuesser} instead |
|||
*/ |
|||
class FileinfoMimeTypeGuesser implements MimeTypeGuesserInterface |
|||
{ |
|||
private $magicFile; |
|||
|
|||
/** |
|||
* @param string $magicFile A magic file to use with the finfo instance |
|||
* |
|||
* @see https://php.net/finfo-open |
|||
*/ |
|||
public function __construct(string $magicFile = null) |
|||
{ |
|||
$this->magicFile = $magicFile; |
|||
} |
|||
|
|||
/** |
|||
* Returns whether this guesser is supported on the current OS/PHP setup. |
|||
* |
|||
* @return bool |
|||
*/ |
|||
public static function isSupported() |
|||
{ |
|||
return \function_exists('finfo_open'); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function guess($path) |
|||
{ |
|||
if (!is_file($path)) { |
|||
throw new FileNotFoundException($path); |
|||
} |
|||
|
|||
if (!is_readable($path)) { |
|||
throw new AccessDeniedException($path); |
|||
} |
|||
|
|||
if (!self::isSupported()) { |
|||
return null; |
|||
} |
|||
|
|||
if (!$finfo = new \finfo(FILEINFO_MIME_TYPE, $this->magicFile)) { |
|||
return null; |
|||
} |
|||
|
|||
return $finfo->file($path); |
|||
} |
|||
} |
|||
@ -0,0 +1,826 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\File\MimeType; |
|||
|
|||
use Symfony\Component\Mime\MimeTypes; |
|||
|
|||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" instead.', MimeTypeExtensionGuesser::class, MimeTypes::class), E_USER_DEPRECATED); |
|||
|
|||
/** |
|||
* Provides a best-guess mapping of mime type to file extension. |
|||
* |
|||
* @deprecated since Symfony 4.3, use {@link MimeTypes} instead |
|||
*/ |
|||
class MimeTypeExtensionGuesser implements ExtensionGuesserInterface |
|||
{ |
|||
/** |
|||
* A map of mime types and their default extensions. |
|||
* |
|||
* This list has been placed under the public domain by the Apache HTTPD project. |
|||
* This list has been updated from upstream on 2019-01-14. |
|||
* |
|||
* @see https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types |
|||
*/ |
|||
protected $defaultExtensions = [ |
|||
'application/andrew-inset' => 'ez', |
|||
'application/applixware' => 'aw', |
|||
'application/atom+xml' => 'atom', |
|||
'application/atomcat+xml' => 'atomcat', |
|||
'application/atomsvc+xml' => 'atomsvc', |
|||
'application/ccxml+xml' => 'ccxml', |
|||
'application/cdmi-capability' => 'cdmia', |
|||
'application/cdmi-container' => 'cdmic', |
|||
'application/cdmi-domain' => 'cdmid', |
|||
'application/cdmi-object' => 'cdmio', |
|||
'application/cdmi-queue' => 'cdmiq', |
|||
'application/cu-seeme' => 'cu', |
|||
'application/davmount+xml' => 'davmount', |
|||
'application/docbook+xml' => 'dbk', |
|||
'application/dssc+der' => 'dssc', |
|||
'application/dssc+xml' => 'xdssc', |
|||
'application/ecmascript' => 'ecma', |
|||
'application/emma+xml' => 'emma', |
|||
'application/epub+zip' => 'epub', |
|||
'application/exi' => 'exi', |
|||
'application/font-tdpfr' => 'pfr', |
|||
'application/gml+xml' => 'gml', |
|||
'application/gpx+xml' => 'gpx', |
|||
'application/gxf' => 'gxf', |
|||
'application/hyperstudio' => 'stk', |
|||
'application/inkml+xml' => 'ink', |
|||
'application/ipfix' => 'ipfix', |
|||
'application/java-archive' => 'jar', |
|||
'application/java-serialized-object' => 'ser', |
|||
'application/java-vm' => 'class', |
|||
'application/javascript' => 'js', |
|||
'application/json' => 'json', |
|||
'application/jsonml+json' => 'jsonml', |
|||
'application/lost+xml' => 'lostxml', |
|||
'application/mac-binhex40' => 'hqx', |
|||
'application/mac-compactpro' => 'cpt', |
|||
'application/mads+xml' => 'mads', |
|||
'application/marc' => 'mrc', |
|||
'application/marcxml+xml' => 'mrcx', |
|||
'application/mathematica' => 'ma', |
|||
'application/mathml+xml' => 'mathml', |
|||
'application/mbox' => 'mbox', |
|||
'application/mediaservercontrol+xml' => 'mscml', |
|||
'application/metalink+xml' => 'metalink', |
|||
'application/metalink4+xml' => 'meta4', |
|||
'application/mets+xml' => 'mets', |
|||
'application/mods+xml' => 'mods', |
|||
'application/mp21' => 'm21', |
|||
'application/mp4' => 'mp4s', |
|||
'application/msword' => 'doc', |
|||
'application/mxf' => 'mxf', |
|||
'application/octet-stream' => 'bin', |
|||
'application/oda' => 'oda', |
|||
'application/oebps-package+xml' => 'opf', |
|||
'application/ogg' => 'ogx', |
|||
'application/omdoc+xml' => 'omdoc', |
|||
'application/onenote' => 'onetoc', |
|||
'application/oxps' => 'oxps', |
|||
'application/patch-ops-error+xml' => 'xer', |
|||
'application/pdf' => 'pdf', |
|||
'application/pgp-encrypted' => 'pgp', |
|||
'application/pgp-signature' => 'asc', |
|||
'application/pics-rules' => 'prf', |
|||
'application/pkcs10' => 'p10', |
|||
'application/pkcs7-mime' => 'p7m', |
|||
'application/pkcs7-signature' => 'p7s', |
|||
'application/pkcs8' => 'p8', |
|||
'application/pkix-attr-cert' => 'ac', |
|||
'application/pkix-cert' => 'cer', |
|||
'application/pkix-crl' => 'crl', |
|||
'application/pkix-pkipath' => 'pkipath', |
|||
'application/pkixcmp' => 'pki', |
|||
'application/pls+xml' => 'pls', |
|||
'application/postscript' => 'ai', |
|||
'application/prs.cww' => 'cww', |
|||
'application/pskc+xml' => 'pskcxml', |
|||
'application/rdf+xml' => 'rdf', |
|||
'application/reginfo+xml' => 'rif', |
|||
'application/relax-ng-compact-syntax' => 'rnc', |
|||
'application/resource-lists+xml' => 'rl', |
|||
'application/resource-lists-diff+xml' => 'rld', |
|||
'application/rls-services+xml' => 'rs', |
|||
'application/rpki-ghostbusters' => 'gbr', |
|||
'application/rpki-manifest' => 'mft', |
|||
'application/rpki-roa' => 'roa', |
|||
'application/rsd+xml' => 'rsd', |
|||
'application/rss+xml' => 'rss', |
|||
'application/rtf' => 'rtf', |
|||
'application/sbml+xml' => 'sbml', |
|||
'application/scvp-cv-request' => 'scq', |
|||
'application/scvp-cv-response' => 'scs', |
|||
'application/scvp-vp-request' => 'spq', |
|||
'application/scvp-vp-response' => 'spp', |
|||
'application/sdp' => 'sdp', |
|||
'application/set-payment-initiation' => 'setpay', |
|||
'application/set-registration-initiation' => 'setreg', |
|||
'application/shf+xml' => 'shf', |
|||
'application/smil+xml' => 'smi', |
|||
'application/sparql-query' => 'rq', |
|||
'application/sparql-results+xml' => 'srx', |
|||
'application/srgs' => 'gram', |
|||
'application/srgs+xml' => 'grxml', |
|||
'application/sru+xml' => 'sru', |
|||
'application/ssdl+xml' => 'ssdl', |
|||
'application/ssml+xml' => 'ssml', |
|||
'application/tei+xml' => 'tei', |
|||
'application/thraud+xml' => 'tfi', |
|||
'application/timestamped-data' => 'tsd', |
|||
'application/vnd.3gpp.pic-bw-large' => 'plb', |
|||
'application/vnd.3gpp.pic-bw-small' => 'psb', |
|||
'application/vnd.3gpp.pic-bw-var' => 'pvb', |
|||
'application/vnd.3gpp2.tcap' => 'tcap', |
|||
'application/vnd.3m.post-it-notes' => 'pwn', |
|||
'application/vnd.accpac.simply.aso' => 'aso', |
|||
'application/vnd.accpac.simply.imp' => 'imp', |
|||
'application/vnd.acucobol' => 'acu', |
|||
'application/vnd.acucorp' => 'atc', |
|||
'application/vnd.adobe.air-application-installer-package+zip' => 'air', |
|||
'application/vnd.adobe.formscentral.fcdt' => 'fcdt', |
|||
'application/vnd.adobe.fxp' => 'fxp', |
|||
'application/vnd.adobe.xdp+xml' => 'xdp', |
|||
'application/vnd.adobe.xfdf' => 'xfdf', |
|||
'application/vnd.ahead.space' => 'ahead', |
|||
'application/vnd.airzip.filesecure.azf' => 'azf', |
|||
'application/vnd.airzip.filesecure.azs' => 'azs', |
|||
'application/vnd.amazon.ebook' => 'azw', |
|||
'application/vnd.americandynamics.acc' => 'acc', |
|||
'application/vnd.amiga.ami' => 'ami', |
|||
'application/vnd.android.package-archive' => 'apk', |
|||
'application/vnd.anser-web-certificate-issue-initiation' => 'cii', |
|||
'application/vnd.anser-web-funds-transfer-initiation' => 'fti', |
|||
'application/vnd.antix.game-component' => 'atx', |
|||
'application/vnd.apple.installer+xml' => 'mpkg', |
|||
'application/vnd.apple.mpegurl' => 'm3u8', |
|||
'application/vnd.aristanetworks.swi' => 'swi', |
|||
'application/vnd.astraea-software.iota' => 'iota', |
|||
'application/vnd.audiograph' => 'aep', |
|||
'application/vnd.blueice.multipass' => 'mpm', |
|||
'application/vnd.bmi' => 'bmi', |
|||
'application/vnd.businessobjects' => 'rep', |
|||
'application/vnd.chemdraw+xml' => 'cdxml', |
|||
'application/vnd.chipnuts.karaoke-mmd' => 'mmd', |
|||
'application/vnd.cinderella' => 'cdy', |
|||
'application/vnd.claymore' => 'cla', |
|||
'application/vnd.cloanto.rp9' => 'rp9', |
|||
'application/vnd.clonk.c4group' => 'c4g', |
|||
'application/vnd.cluetrust.cartomobile-config' => 'c11amc', |
|||
'application/vnd.cluetrust.cartomobile-config-pkg' => 'c11amz', |
|||
'application/vnd.commonspace' => 'csp', |
|||
'application/vnd.contact.cmsg' => 'cdbcmsg', |
|||
'application/vnd.cosmocaller' => 'cmc', |
|||
'application/vnd.crick.clicker' => 'clkx', |
|||
'application/vnd.crick.clicker.keyboard' => 'clkk', |
|||
'application/vnd.crick.clicker.palette' => 'clkp', |
|||
'application/vnd.crick.clicker.template' => 'clkt', |
|||
'application/vnd.crick.clicker.wordbank' => 'clkw', |
|||
'application/vnd.criticaltools.wbs+xml' => 'wbs', |
|||
'application/vnd.ctc-posml' => 'pml', |
|||
'application/vnd.cups-ppd' => 'ppd', |
|||
'application/vnd.curl.car' => 'car', |
|||
'application/vnd.curl.pcurl' => 'pcurl', |
|||
'application/vnd.dart' => 'dart', |
|||
'application/vnd.data-vision.rdz' => 'rdz', |
|||
'application/vnd.dece.data' => 'uvf', |
|||
'application/vnd.dece.ttml+xml' => 'uvt', |
|||
'application/vnd.dece.unspecified' => 'uvx', |
|||
'application/vnd.dece.zip' => 'uvz', |
|||
'application/vnd.denovo.fcselayout-link' => 'fe_launch', |
|||
'application/vnd.dna' => 'dna', |
|||
'application/vnd.dolby.mlp' => 'mlp', |
|||
'application/vnd.dpgraph' => 'dpg', |
|||
'application/vnd.dreamfactory' => 'dfac', |
|||
'application/vnd.ds-keypoint' => 'kpxx', |
|||
'application/vnd.dvb.ait' => 'ait', |
|||
'application/vnd.dvb.service' => 'svc', |
|||
'application/vnd.dynageo' => 'geo', |
|||
'application/vnd.ecowin.chart' => 'mag', |
|||
'application/vnd.enliven' => 'nml', |
|||
'application/vnd.epson.esf' => 'esf', |
|||
'application/vnd.epson.msf' => 'msf', |
|||
'application/vnd.epson.quickanime' => 'qam', |
|||
'application/vnd.epson.salt' => 'slt', |
|||
'application/vnd.epson.ssf' => 'ssf', |
|||
'application/vnd.eszigno3+xml' => 'es3', |
|||
'application/vnd.ezpix-album' => 'ez2', |
|||
'application/vnd.ezpix-package' => 'ez3', |
|||
'application/vnd.fdf' => 'fdf', |
|||
'application/vnd.fdsn.mseed' => 'mseed', |
|||
'application/vnd.fdsn.seed' => 'seed', |
|||
'application/vnd.flographit' => 'gph', |
|||
'application/vnd.fluxtime.clip' => 'ftc', |
|||
'application/vnd.framemaker' => 'fm', |
|||
'application/vnd.frogans.fnc' => 'fnc', |
|||
'application/vnd.frogans.ltf' => 'ltf', |
|||
'application/vnd.fsc.weblaunch' => 'fsc', |
|||
'application/vnd.fujitsu.oasys' => 'oas', |
|||
'application/vnd.fujitsu.oasys2' => 'oa2', |
|||
'application/vnd.fujitsu.oasys3' => 'oa3', |
|||
'application/vnd.fujitsu.oasysgp' => 'fg5', |
|||
'application/vnd.fujitsu.oasysprs' => 'bh2', |
|||
'application/vnd.fujixerox.ddd' => 'ddd', |
|||
'application/vnd.fujixerox.docuworks' => 'xdw', |
|||
'application/vnd.fujixerox.docuworks.binder' => 'xbd', |
|||
'application/vnd.fuzzysheet' => 'fzs', |
|||
'application/vnd.genomatix.tuxedo' => 'txd', |
|||
'application/vnd.geogebra.file' => 'ggb', |
|||
'application/vnd.geogebra.tool' => 'ggt', |
|||
'application/vnd.geometry-explorer' => 'gex', |
|||
'application/vnd.geonext' => 'gxt', |
|||
'application/vnd.geoplan' => 'g2w', |
|||
'application/vnd.geospace' => 'g3w', |
|||
'application/vnd.gmx' => 'gmx', |
|||
'application/vnd.google-earth.kml+xml' => 'kml', |
|||
'application/vnd.google-earth.kmz' => 'kmz', |
|||
'application/vnd.grafeq' => 'gqf', |
|||
'application/vnd.groove-account' => 'gac', |
|||
'application/vnd.groove-help' => 'ghf', |
|||
'application/vnd.groove-identity-message' => 'gim', |
|||
'application/vnd.groove-injector' => 'grv', |
|||
'application/vnd.groove-tool-message' => 'gtm', |
|||
'application/vnd.groove-tool-template' => 'tpl', |
|||
'application/vnd.groove-vcard' => 'vcg', |
|||
'application/vnd.hal+xml' => 'hal', |
|||
'application/vnd.handheld-entertainment+xml' => 'zmm', |
|||
'application/vnd.hbci' => 'hbci', |
|||
'application/vnd.hhe.lesson-player' => 'les', |
|||
'application/vnd.hp-hpgl' => 'hpgl', |
|||
'application/vnd.hp-hpid' => 'hpid', |
|||
'application/vnd.hp-hps' => 'hps', |
|||
'application/vnd.hp-jlyt' => 'jlt', |
|||
'application/vnd.hp-pcl' => 'pcl', |
|||
'application/vnd.hp-pclxl' => 'pclxl', |
|||
'application/vnd.hydrostatix.sof-data' => 'sfd-hdstx', |
|||
'application/vnd.ibm.minipay' => 'mpy', |
|||
'application/vnd.ibm.modcap' => 'afp', |
|||
'application/vnd.ibm.rights-management' => 'irm', |
|||
'application/vnd.ibm.secure-container' => 'sc', |
|||
'application/vnd.iccprofile' => 'icc', |
|||
'application/vnd.igloader' => 'igl', |
|||
'application/vnd.immervision-ivp' => 'ivp', |
|||
'application/vnd.immervision-ivu' => 'ivu', |
|||
'application/vnd.insors.igm' => 'igm', |
|||
'application/vnd.intercon.formnet' => 'xpw', |
|||
'application/vnd.intergeo' => 'i2g', |
|||
'application/vnd.intu.qbo' => 'qbo', |
|||
'application/vnd.intu.qfx' => 'qfx', |
|||
'application/vnd.ipunplugged.rcprofile' => 'rcprofile', |
|||
'application/vnd.irepository.package+xml' => 'irp', |
|||
'application/vnd.is-xpr' => 'xpr', |
|||
'application/vnd.isac.fcs' => 'fcs', |
|||
'application/vnd.jam' => 'jam', |
|||
'application/vnd.jcp.javame.midlet-rms' => 'rms', |
|||
'application/vnd.jisp' => 'jisp', |
|||
'application/vnd.joost.joda-archive' => 'joda', |
|||
'application/vnd.kahootz' => 'ktz', |
|||
'application/vnd.kde.karbon' => 'karbon', |
|||
'application/vnd.kde.kchart' => 'chrt', |
|||
'application/vnd.kde.kformula' => 'kfo', |
|||
'application/vnd.kde.kivio' => 'flw', |
|||
'application/vnd.kde.kontour' => 'kon', |
|||
'application/vnd.kde.kpresenter' => 'kpr', |
|||
'application/vnd.kde.kspread' => 'ksp', |
|||
'application/vnd.kde.kword' => 'kwd', |
|||
'application/vnd.kenameaapp' => 'htke', |
|||
'application/vnd.kidspiration' => 'kia', |
|||
'application/vnd.kinar' => 'kne', |
|||
'application/vnd.koan' => 'skp', |
|||
'application/vnd.kodak-descriptor' => 'sse', |
|||
'application/vnd.las.las+xml' => 'lasxml', |
|||
'application/vnd.llamagraphics.life-balance.desktop' => 'lbd', |
|||
'application/vnd.llamagraphics.life-balance.exchange+xml' => 'lbe', |
|||
'application/vnd.lotus-1-2-3' => '123', |
|||
'application/vnd.lotus-approach' => 'apr', |
|||
'application/vnd.lotus-freelance' => 'pre', |
|||
'application/vnd.lotus-notes' => 'nsf', |
|||
'application/vnd.lotus-organizer' => 'org', |
|||
'application/vnd.lotus-screencam' => 'scm', |
|||
'application/vnd.lotus-wordpro' => 'lwp', |
|||
'application/vnd.macports.portpkg' => 'portpkg', |
|||
'application/vnd.mcd' => 'mcd', |
|||
'application/vnd.medcalcdata' => 'mc1', |
|||
'application/vnd.mediastation.cdkey' => 'cdkey', |
|||
'application/vnd.mfer' => 'mwf', |
|||
'application/vnd.mfmp' => 'mfm', |
|||
'application/vnd.micrografx.flo' => 'flo', |
|||
'application/vnd.micrografx.igx' => 'igx', |
|||
'application/vnd.mif' => 'mif', |
|||
'application/vnd.mobius.daf' => 'daf', |
|||
'application/vnd.mobius.dis' => 'dis', |
|||
'application/vnd.mobius.mbk' => 'mbk', |
|||
'application/vnd.mobius.mqy' => 'mqy', |
|||
'application/vnd.mobius.msl' => 'msl', |
|||
'application/vnd.mobius.plc' => 'plc', |
|||
'application/vnd.mobius.txf' => 'txf', |
|||
'application/vnd.mophun.application' => 'mpn', |
|||
'application/vnd.mophun.certificate' => 'mpc', |
|||
'application/vnd.mozilla.xul+xml' => 'xul', |
|||
'application/vnd.ms-artgalry' => 'cil', |
|||
'application/vnd.ms-cab-compressed' => 'cab', |
|||
'application/vnd.ms-excel' => 'xls', |
|||
'application/vnd.ms-excel.addin.macroenabled.12' => 'xlam', |
|||
'application/vnd.ms-excel.sheet.binary.macroenabled.12' => 'xlsb', |
|||
'application/vnd.ms-excel.sheet.macroenabled.12' => 'xlsm', |
|||
'application/vnd.ms-excel.template.macroenabled.12' => 'xltm', |
|||
'application/vnd.ms-fontobject' => 'eot', |
|||
'application/vnd.ms-htmlhelp' => 'chm', |
|||
'application/vnd.ms-ims' => 'ims', |
|||
'application/vnd.ms-lrm' => 'lrm', |
|||
'application/vnd.ms-officetheme' => 'thmx', |
|||
'application/vnd.ms-pki.seccat' => 'cat', |
|||
'application/vnd.ms-pki.stl' => 'stl', |
|||
'application/vnd.ms-powerpoint' => 'ppt', |
|||
'application/vnd.ms-powerpoint.addin.macroenabled.12' => 'ppam', |
|||
'application/vnd.ms-powerpoint.presentation.macroenabled.12' => 'pptm', |
|||
'application/vnd.ms-powerpoint.slide.macroenabled.12' => 'sldm', |
|||
'application/vnd.ms-powerpoint.slideshow.macroenabled.12' => 'ppsm', |
|||
'application/vnd.ms-powerpoint.template.macroenabled.12' => 'potm', |
|||
'application/vnd.ms-project' => 'mpp', |
|||
'application/vnd.ms-word.document.macroenabled.12' => 'docm', |
|||
'application/vnd.ms-word.template.macroenabled.12' => 'dotm', |
|||
'application/vnd.ms-works' => 'wps', |
|||
'application/vnd.ms-wpl' => 'wpl', |
|||
'application/vnd.ms-xpsdocument' => 'xps', |
|||
'application/vnd.mseq' => 'mseq', |
|||
'application/vnd.musician' => 'mus', |
|||
'application/vnd.muvee.style' => 'msty', |
|||
'application/vnd.mynfc' => 'taglet', |
|||
'application/vnd.neurolanguage.nlu' => 'nlu', |
|||
'application/vnd.nitf' => 'ntf', |
|||
'application/vnd.noblenet-directory' => 'nnd', |
|||
'application/vnd.noblenet-sealer' => 'nns', |
|||
'application/vnd.noblenet-web' => 'nnw', |
|||
'application/vnd.nokia.n-gage.data' => 'ngdat', |
|||
'application/vnd.nokia.n-gage.symbian.install' => 'n-gage', |
|||
'application/vnd.nokia.radio-preset' => 'rpst', |
|||
'application/vnd.nokia.radio-presets' => 'rpss', |
|||
'application/vnd.novadigm.edm' => 'edm', |
|||
'application/vnd.novadigm.edx' => 'edx', |
|||
'application/vnd.novadigm.ext' => 'ext', |
|||
'application/vnd.oasis.opendocument.chart' => 'odc', |
|||
'application/vnd.oasis.opendocument.chart-template' => 'otc', |
|||
'application/vnd.oasis.opendocument.database' => 'odb', |
|||
'application/vnd.oasis.opendocument.formula' => 'odf', |
|||
'application/vnd.oasis.opendocument.formula-template' => 'odft', |
|||
'application/vnd.oasis.opendocument.graphics' => 'odg', |
|||
'application/vnd.oasis.opendocument.graphics-template' => 'otg', |
|||
'application/vnd.oasis.opendocument.image' => 'odi', |
|||
'application/vnd.oasis.opendocument.image-template' => 'oti', |
|||
'application/vnd.oasis.opendocument.presentation' => 'odp', |
|||
'application/vnd.oasis.opendocument.presentation-template' => 'otp', |
|||
'application/vnd.oasis.opendocument.spreadsheet' => 'ods', |
|||
'application/vnd.oasis.opendocument.spreadsheet-template' => 'ots', |
|||
'application/vnd.oasis.opendocument.text' => 'odt', |
|||
'application/vnd.oasis.opendocument.text-master' => 'odm', |
|||
'application/vnd.oasis.opendocument.text-template' => 'ott', |
|||
'application/vnd.oasis.opendocument.text-web' => 'oth', |
|||
'application/vnd.olpc-sugar' => 'xo', |
|||
'application/vnd.oma.dd2+xml' => 'dd2', |
|||
'application/vnd.openofficeorg.extension' => 'oxt', |
|||
'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx', |
|||
'application/vnd.openxmlformats-officedocument.presentationml.slide' => 'sldx', |
|||
'application/vnd.openxmlformats-officedocument.presentationml.slideshow' => 'ppsx', |
|||
'application/vnd.openxmlformats-officedocument.presentationml.template' => 'potx', |
|||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx', |
|||
'application/vnd.openxmlformats-officedocument.spreadsheetml.template' => 'xltx', |
|||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx', |
|||
'application/vnd.openxmlformats-officedocument.wordprocessingml.template' => 'dotx', |
|||
'application/vnd.osgeo.mapguide.package' => 'mgp', |
|||
'application/vnd.osgi.dp' => 'dp', |
|||
'application/vnd.osgi.subsystem' => 'esa', |
|||
'application/vnd.palm' => 'pdb', |
|||
'application/vnd.pawaafile' => 'paw', |
|||
'application/vnd.pg.format' => 'str', |
|||
'application/vnd.pg.osasli' => 'ei6', |
|||
'application/vnd.picsel' => 'efif', |
|||
'application/vnd.pmi.widget' => 'wg', |
|||
'application/vnd.pocketlearn' => 'plf', |
|||
'application/vnd.powerbuilder6' => 'pbd', |
|||
'application/vnd.previewsystems.box' => 'box', |
|||
'application/vnd.proteus.magazine' => 'mgz', |
|||
'application/vnd.publishare-delta-tree' => 'qps', |
|||
'application/vnd.pvi.ptid1' => 'ptid', |
|||
'application/vnd.quark.quarkxpress' => 'qxd', |
|||
'application/vnd.realvnc.bed' => 'bed', |
|||
'application/vnd.recordare.musicxml' => 'mxl', |
|||
'application/vnd.recordare.musicxml+xml' => 'musicxml', |
|||
'application/vnd.rig.cryptonote' => 'cryptonote', |
|||
'application/vnd.rim.cod' => 'cod', |
|||
'application/vnd.rn-realmedia' => 'rm', |
|||
'application/vnd.rn-realmedia-vbr' => 'rmvb', |
|||
'application/vnd.route66.link66+xml' => 'link66', |
|||
'application/vnd.sailingtracker.track' => 'st', |
|||
'application/vnd.seemail' => 'see', |
|||
'application/vnd.sema' => 'sema', |
|||
'application/vnd.semd' => 'semd', |
|||
'application/vnd.semf' => 'semf', |
|||
'application/vnd.shana.informed.formdata' => 'ifm', |
|||
'application/vnd.shana.informed.formtemplate' => 'itp', |
|||
'application/vnd.shana.informed.interchange' => 'iif', |
|||
'application/vnd.shana.informed.package' => 'ipk', |
|||
'application/vnd.simtech-mindmapper' => 'twd', |
|||
'application/vnd.smaf' => 'mmf', |
|||
'application/vnd.smart.teacher' => 'teacher', |
|||
'application/vnd.solent.sdkm+xml' => 'sdkm', |
|||
'application/vnd.spotfire.dxp' => 'dxp', |
|||
'application/vnd.spotfire.sfs' => 'sfs', |
|||
'application/vnd.stardivision.calc' => 'sdc', |
|||
'application/vnd.stardivision.draw' => 'sda', |
|||
'application/vnd.stardivision.impress' => 'sdd', |
|||
'application/vnd.stardivision.math' => 'smf', |
|||
'application/vnd.stardivision.writer' => 'sdw', |
|||
'application/vnd.stardivision.writer-global' => 'sgl', |
|||
'application/vnd.stepmania.package' => 'smzip', |
|||
'application/vnd.stepmania.stepchart' => 'sm', |
|||
'application/vnd.sun.xml.calc' => 'sxc', |
|||
'application/vnd.sun.xml.calc.template' => 'stc', |
|||
'application/vnd.sun.xml.draw' => 'sxd', |
|||
'application/vnd.sun.xml.draw.template' => 'std', |
|||
'application/vnd.sun.xml.impress' => 'sxi', |
|||
'application/vnd.sun.xml.impress.template' => 'sti', |
|||
'application/vnd.sun.xml.math' => 'sxm', |
|||
'application/vnd.sun.xml.writer' => 'sxw', |
|||
'application/vnd.sun.xml.writer.global' => 'sxg', |
|||
'application/vnd.sun.xml.writer.template' => 'stw', |
|||
'application/vnd.sus-calendar' => 'sus', |
|||
'application/vnd.svd' => 'svd', |
|||
'application/vnd.symbian.install' => 'sis', |
|||
'application/vnd.syncml+xml' => 'xsm', |
|||
'application/vnd.syncml.dm+wbxml' => 'bdm', |
|||
'application/vnd.syncml.dm+xml' => 'xdm', |
|||
'application/vnd.tao.intent-module-archive' => 'tao', |
|||
'application/vnd.tcpdump.pcap' => 'pcap', |
|||
'application/vnd.tmobile-livetv' => 'tmo', |
|||
'application/vnd.trid.tpt' => 'tpt', |
|||
'application/vnd.triscape.mxs' => 'mxs', |
|||
'application/vnd.trueapp' => 'tra', |
|||
'application/vnd.ufdl' => 'ufd', |
|||
'application/vnd.uiq.theme' => 'utz', |
|||
'application/vnd.umajin' => 'umj', |
|||
'application/vnd.unity' => 'unityweb', |
|||
'application/vnd.uoml+xml' => 'uoml', |
|||
'application/vnd.vcx' => 'vcx', |
|||
'application/vnd.visio' => 'vsd', |
|||
'application/vnd.visionary' => 'vis', |
|||
'application/vnd.vsf' => 'vsf', |
|||
'application/vnd.wap.wbxml' => 'wbxml', |
|||
'application/vnd.wap.wmlc' => 'wmlc', |
|||
'application/vnd.wap.wmlscriptc' => 'wmlsc', |
|||
'application/vnd.webturbo' => 'wtb', |
|||
'application/vnd.wolfram.player' => 'nbp', |
|||
'application/vnd.wordperfect' => 'wpd', |
|||
'application/vnd.wqd' => 'wqd', |
|||
'application/vnd.wt.stf' => 'stf', |
|||
'application/vnd.xara' => 'xar', |
|||
'application/vnd.xfdl' => 'xfdl', |
|||
'application/vnd.yamaha.hv-dic' => 'hvd', |
|||
'application/vnd.yamaha.hv-script' => 'hvs', |
|||
'application/vnd.yamaha.hv-voice' => 'hvp', |
|||
'application/vnd.yamaha.openscoreformat' => 'osf', |
|||
'application/vnd.yamaha.openscoreformat.osfpvg+xml' => 'osfpvg', |
|||
'application/vnd.yamaha.smaf-audio' => 'saf', |
|||
'application/vnd.yamaha.smaf-phrase' => 'spf', |
|||
'application/vnd.yellowriver-custom-menu' => 'cmp', |
|||
'application/vnd.zul' => 'zir', |
|||
'application/vnd.zzazz.deck+xml' => 'zaz', |
|||
'application/voicexml+xml' => 'vxml', |
|||
'application/widget' => 'wgt', |
|||
'application/winhlp' => 'hlp', |
|||
'application/wsdl+xml' => 'wsdl', |
|||
'application/wspolicy+xml' => 'wspolicy', |
|||
'application/x-7z-compressed' => '7z', |
|||
'application/x-abiword' => 'abw', |
|||
'application/x-ace-compressed' => 'ace', |
|||
'application/x-apple-diskimage' => 'dmg', |
|||
'application/x-authorware-bin' => 'aab', |
|||
'application/x-authorware-map' => 'aam', |
|||
'application/x-authorware-seg' => 'aas', |
|||
'application/x-bcpio' => 'bcpio', |
|||
'application/x-bittorrent' => 'torrent', |
|||
'application/x-blorb' => 'blb', |
|||
'application/x-bzip' => 'bz', |
|||
'application/x-bzip2' => 'bz2', |
|||
'application/x-cbr' => 'cbr', |
|||
'application/x-cdlink' => 'vcd', |
|||
'application/x-cfs-compressed' => 'cfs', |
|||
'application/x-chat' => 'chat', |
|||
'application/x-chess-pgn' => 'pgn', |
|||
'application/x-conference' => 'nsc', |
|||
'application/x-cpio' => 'cpio', |
|||
'application/x-csh' => 'csh', |
|||
'application/x-debian-package' => 'deb', |
|||
'application/x-dgc-compressed' => 'dgc', |
|||
'application/x-director' => 'dir', |
|||
'application/x-doom' => 'wad', |
|||
'application/x-dtbncx+xml' => 'ncx', |
|||
'application/x-dtbook+xml' => 'dtb', |
|||
'application/x-dtbresource+xml' => 'res', |
|||
'application/x-dvi' => 'dvi', |
|||
'application/x-envoy' => 'evy', |
|||
'application/x-eva' => 'eva', |
|||
'application/x-font-bdf' => 'bdf', |
|||
'application/x-font-ghostscript' => 'gsf', |
|||
'application/x-font-linux-psf' => 'psf', |
|||
'application/x-font-otf' => 'otf', |
|||
'application/x-font-pcf' => 'pcf', |
|||
'application/x-font-snf' => 'snf', |
|||
'application/x-font-ttf' => 'ttf', |
|||
'application/x-font-type1' => 'pfa', |
|||
'application/x-font-woff' => 'woff', |
|||
'application/x-freearc' => 'arc', |
|||
'application/x-futuresplash' => 'spl', |
|||
'application/x-gca-compressed' => 'gca', |
|||
'application/x-glulx' => 'ulx', |
|||
'application/x-gnumeric' => 'gnumeric', |
|||
'application/x-gramps-xml' => 'gramps', |
|||
'application/x-gtar' => 'gtar', |
|||
'application/x-hdf' => 'hdf', |
|||
'application/x-install-instructions' => 'install', |
|||
'application/x-iso9660-image' => 'iso', |
|||
'application/x-java-jnlp-file' => 'jnlp', |
|||
'application/x-latex' => 'latex', |
|||
'application/x-lzh-compressed' => 'lzh', |
|||
'application/x-mie' => 'mie', |
|||
'application/x-mobipocket-ebook' => 'prc', |
|||
'application/x-ms-application' => 'application', |
|||
'application/x-ms-shortcut' => 'lnk', |
|||
'application/x-ms-wmd' => 'wmd', |
|||
'application/x-ms-wmz' => 'wmz', |
|||
'application/x-ms-xbap' => 'xbap', |
|||
'application/x-msaccess' => 'mdb', |
|||
'application/x-msbinder' => 'obd', |
|||
'application/x-mscardfile' => 'crd', |
|||
'application/x-msclip' => 'clp', |
|||
'application/x-msdownload' => 'exe', |
|||
'application/x-msmediaview' => 'mvb', |
|||
'application/x-msmetafile' => 'wmf', |
|||
'application/x-msmoney' => 'mny', |
|||
'application/x-mspublisher' => 'pub', |
|||
'application/x-msschedule' => 'scd', |
|||
'application/x-msterminal' => 'trm', |
|||
'application/x-mswrite' => 'wri', |
|||
'application/x-netcdf' => 'nc', |
|||
'application/x-nzb' => 'nzb', |
|||
'application/x-pkcs12' => 'p12', |
|||
'application/x-pkcs7-certificates' => 'p7b', |
|||
'application/x-pkcs7-certreqresp' => 'p7r', |
|||
'application/x-rar-compressed' => 'rar', |
|||
'application/x-rar' => 'rar', |
|||
'application/x-research-info-systems' => 'ris', |
|||
'application/x-sh' => 'sh', |
|||
'application/x-shar' => 'shar', |
|||
'application/x-shockwave-flash' => 'swf', |
|||
'application/x-silverlight-app' => 'xap', |
|||
'application/x-sql' => 'sql', |
|||
'application/x-stuffit' => 'sit', |
|||
'application/x-stuffitx' => 'sitx', |
|||
'application/x-subrip' => 'srt', |
|||
'application/x-sv4cpio' => 'sv4cpio', |
|||
'application/x-sv4crc' => 'sv4crc', |
|||
'application/x-t3vm-image' => 't3', |
|||
'application/x-tads' => 'gam', |
|||
'application/x-tar' => 'tar', |
|||
'application/x-tcl' => 'tcl', |
|||
'application/x-tex' => 'tex', |
|||
'application/x-tex-tfm' => 'tfm', |
|||
'application/x-texinfo' => 'texinfo', |
|||
'application/x-tgif' => 'obj', |
|||
'application/x-ustar' => 'ustar', |
|||
'application/x-wais-source' => 'src', |
|||
'application/x-x509-ca-cert' => 'der', |
|||
'application/x-xfig' => 'fig', |
|||
'application/x-xliff+xml' => 'xlf', |
|||
'application/x-xpinstall' => 'xpi', |
|||
'application/x-xz' => 'xz', |
|||
'application/x-zip-compressed' => 'zip', |
|||
'application/x-zmachine' => 'z1', |
|||
'application/xaml+xml' => 'xaml', |
|||
'application/xcap-diff+xml' => 'xdf', |
|||
'application/xenc+xml' => 'xenc', |
|||
'application/xhtml+xml' => 'xhtml', |
|||
'application/xml' => 'xml', |
|||
'application/xml-dtd' => 'dtd', |
|||
'application/xop+xml' => 'xop', |
|||
'application/xproc+xml' => 'xpl', |
|||
'application/xslt+xml' => 'xslt', |
|||
'application/xspf+xml' => 'xspf', |
|||
'application/xv+xml' => 'mxml', |
|||
'application/yang' => 'yang', |
|||
'application/yin+xml' => 'yin', |
|||
'application/zip' => 'zip', |
|||
'audio/adpcm' => 'adp', |
|||
'audio/basic' => 'au', |
|||
'audio/midi' => 'mid', |
|||
'audio/mp4' => 'm4a', |
|||
'audio/mpeg' => 'mpga', |
|||
'audio/ogg' => 'oga', |
|||
'audio/s3m' => 's3m', |
|||
'audio/silk' => 'sil', |
|||
'audio/vnd.dece.audio' => 'uva', |
|||
'audio/vnd.digital-winds' => 'eol', |
|||
'audio/vnd.dra' => 'dra', |
|||
'audio/vnd.dts' => 'dts', |
|||
'audio/vnd.dts.hd' => 'dtshd', |
|||
'audio/vnd.lucent.voice' => 'lvp', |
|||
'audio/vnd.ms-playready.media.pya' => 'pya', |
|||
'audio/vnd.nuera.ecelp4800' => 'ecelp4800', |
|||
'audio/vnd.nuera.ecelp7470' => 'ecelp7470', |
|||
'audio/vnd.nuera.ecelp9600' => 'ecelp9600', |
|||
'audio/vnd.rip' => 'rip', |
|||
'audio/webm' => 'weba', |
|||
'audio/x-aac' => 'aac', |
|||
'audio/x-aiff' => 'aif', |
|||
'audio/x-caf' => 'caf', |
|||
'audio/x-flac' => 'flac', |
|||
'audio/x-hx-aac-adts' => 'aac', |
|||
'audio/x-matroska' => 'mka', |
|||
'audio/x-mpegurl' => 'm3u', |
|||
'audio/x-ms-wax' => 'wax', |
|||
'audio/x-ms-wma' => 'wma', |
|||
'audio/x-pn-realaudio' => 'ram', |
|||
'audio/x-pn-realaudio-plugin' => 'rmp', |
|||
'audio/x-wav' => 'wav', |
|||
'audio/xm' => 'xm', |
|||
'chemical/x-cdx' => 'cdx', |
|||
'chemical/x-cif' => 'cif', |
|||
'chemical/x-cmdf' => 'cmdf', |
|||
'chemical/x-cml' => 'cml', |
|||
'chemical/x-csml' => 'csml', |
|||
'chemical/x-xyz' => 'xyz', |
|||
'font/collection' => 'ttc', |
|||
'font/otf' => 'otf', |
|||
'font/ttf' => 'ttf', |
|||
'font/woff' => 'woff', |
|||
'font/woff2' => 'woff2', |
|||
'image/bmp' => 'bmp', |
|||
'image/x-ms-bmp' => 'bmp', |
|||
'image/cgm' => 'cgm', |
|||
'image/g3fax' => 'g3', |
|||
'image/gif' => 'gif', |
|||
'image/ief' => 'ief', |
|||
'image/jpeg' => 'jpeg', |
|||
'image/pjpeg' => 'jpeg', |
|||
'image/ktx' => 'ktx', |
|||
'image/png' => 'png', |
|||
'image/prs.btif' => 'btif', |
|||
'image/sgi' => 'sgi', |
|||
'image/svg+xml' => 'svg', |
|||
'image/tiff' => 'tiff', |
|||
'image/vnd.adobe.photoshop' => 'psd', |
|||
'image/vnd.dece.graphic' => 'uvi', |
|||
'image/vnd.djvu' => 'djvu', |
|||
'image/vnd.dvb.subtitle' => 'sub', |
|||
'image/vnd.dwg' => 'dwg', |
|||
'image/vnd.dxf' => 'dxf', |
|||
'image/vnd.fastbidsheet' => 'fbs', |
|||
'image/vnd.fpx' => 'fpx', |
|||
'image/vnd.fst' => 'fst', |
|||
'image/vnd.fujixerox.edmics-mmr' => 'mmr', |
|||
'image/vnd.fujixerox.edmics-rlc' => 'rlc', |
|||
'image/vnd.ms-modi' => 'mdi', |
|||
'image/vnd.ms-photo' => 'wdp', |
|||
'image/vnd.net-fpx' => 'npx', |
|||
'image/vnd.wap.wbmp' => 'wbmp', |
|||
'image/vnd.xiff' => 'xif', |
|||
'image/webp' => 'webp', |
|||
'image/x-3ds' => '3ds', |
|||
'image/x-cmu-raster' => 'ras', |
|||
'image/x-cmx' => 'cmx', |
|||
'image/x-freehand' => 'fh', |
|||
'image/x-icon' => 'ico', |
|||
'image/x-mrsid-image' => 'sid', |
|||
'image/x-pcx' => 'pcx', |
|||
'image/x-pict' => 'pic', |
|||
'image/x-portable-anymap' => 'pnm', |
|||
'image/x-portable-bitmap' => 'pbm', |
|||
'image/x-portable-graymap' => 'pgm', |
|||
'image/x-portable-pixmap' => 'ppm', |
|||
'image/x-rgb' => 'rgb', |
|||
'image/x-tga' => 'tga', |
|||
'image/x-xbitmap' => 'xbm', |
|||
'image/x-xpixmap' => 'xpm', |
|||
'image/x-xwindowdump' => 'xwd', |
|||
'message/rfc822' => 'eml', |
|||
'model/iges' => 'igs', |
|||
'model/mesh' => 'msh', |
|||
'model/vnd.collada+xml' => 'dae', |
|||
'model/vnd.dwf' => 'dwf', |
|||
'model/vnd.gdl' => 'gdl', |
|||
'model/vnd.gtw' => 'gtw', |
|||
'model/vnd.mts' => 'mts', |
|||
'model/vnd.vtu' => 'vtu', |
|||
'model/vrml' => 'wrl', |
|||
'model/x3d+binary' => 'x3db', |
|||
'model/x3d+vrml' => 'x3dv', |
|||
'model/x3d+xml' => 'x3d', |
|||
'text/cache-manifest' => 'appcache', |
|||
'text/calendar' => 'ics', |
|||
'text/css' => 'css', |
|||
'text/csv' => 'csv', |
|||
'text/html' => 'html', |
|||
'text/n3' => 'n3', |
|||
'text/plain' => 'txt', |
|||
'text/prs.lines.tag' => 'dsc', |
|||
'text/richtext' => 'rtx', |
|||
'text/rtf' => 'rtf', |
|||
'text/sgml' => 'sgml', |
|||
'text/tab-separated-values' => 'tsv', |
|||
'text/troff' => 't', |
|||
'text/turtle' => 'ttl', |
|||
'text/uri-list' => 'uri', |
|||
'text/vcard' => 'vcard', |
|||
'text/vnd.curl' => 'curl', |
|||
'text/vnd.curl.dcurl' => 'dcurl', |
|||
'text/vnd.curl.mcurl' => 'mcurl', |
|||
'text/vnd.curl.scurl' => 'scurl', |
|||
'text/vnd.dvb.subtitle' => 'sub', |
|||
'text/vnd.fly' => 'fly', |
|||
'text/vnd.fmi.flexstor' => 'flx', |
|||
'text/vnd.graphviz' => 'gv', |
|||
'text/vnd.in3d.3dml' => '3dml', |
|||
'text/vnd.in3d.spot' => 'spot', |
|||
'text/vnd.sun.j2me.app-descriptor' => 'jad', |
|||
'text/vnd.wap.wml' => 'wml', |
|||
'text/vnd.wap.wmlscript' => 'wmls', |
|||
'text/vtt' => 'vtt', |
|||
'text/x-asm' => 's', |
|||
'text/x-c' => 'c', |
|||
'text/x-fortran' => 'f', |
|||
'text/x-java-source' => 'java', |
|||
'text/x-nfo' => 'nfo', |
|||
'text/x-opml' => 'opml', |
|||
'text/x-pascal' => 'p', |
|||
'text/x-setext' => 'etx', |
|||
'text/x-sfv' => 'sfv', |
|||
'text/x-uuencode' => 'uu', |
|||
'text/x-vcalendar' => 'vcs', |
|||
'text/x-vcard' => 'vcf', |
|||
'video/3gpp' => '3gp', |
|||
'video/3gpp2' => '3g2', |
|||
'video/h261' => 'h261', |
|||
'video/h263' => 'h263', |
|||
'video/h264' => 'h264', |
|||
'video/jpeg' => 'jpgv', |
|||
'video/jpm' => 'jpm', |
|||
'video/mj2' => 'mj2', |
|||
'video/mp4' => 'mp4', |
|||
'video/mpeg' => 'mpeg', |
|||
'video/ogg' => 'ogv', |
|||
'video/quicktime' => 'qt', |
|||
'video/vnd.dece.hd' => 'uvh', |
|||
'video/vnd.dece.mobile' => 'uvm', |
|||
'video/vnd.dece.pd' => 'uvp', |
|||
'video/vnd.dece.sd' => 'uvs', |
|||
'video/vnd.dece.video' => 'uvv', |
|||
'video/vnd.dvb.file' => 'dvb', |
|||
'video/vnd.fvt' => 'fvt', |
|||
'video/vnd.mpegurl' => 'mxu', |
|||
'video/vnd.ms-playready.media.pyv' => 'pyv', |
|||
'video/vnd.uvvu.mp4' => 'uvu', |
|||
'video/vnd.vivo' => 'viv', |
|||
'video/webm' => 'webm', |
|||
'video/x-f4v' => 'f4v', |
|||
'video/x-fli' => 'fli', |
|||
'video/x-flv' => 'flv', |
|||
'video/x-m4v' => 'm4v', |
|||
'video/x-matroska' => 'mkv', |
|||
'video/x-mng' => 'mng', |
|||
'video/x-ms-asf' => 'asf', |
|||
'video/x-ms-vob' => 'vob', |
|||
'video/x-ms-wm' => 'wm', |
|||
'video/x-ms-wmv' => 'wmv', |
|||
'video/x-ms-wmx' => 'wmx', |
|||
'video/x-ms-wvx' => 'wvx', |
|||
'video/x-msvideo' => 'avi', |
|||
'video/x-sgi-movie' => 'movie', |
|||
'video/x-smv' => 'smv', |
|||
'x-conference/x-cooltalk' => 'ice', |
|||
]; |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function guess($mimeType) |
|||
{ |
|||
if (isset($this->defaultExtensions[$mimeType])) { |
|||
return $this->defaultExtensions[$mimeType]; |
|||
} |
|||
|
|||
$lcMimeType = strtolower($mimeType); |
|||
|
|||
return isset($this->defaultExtensions[$lcMimeType]) ? $this->defaultExtensions[$lcMimeType] : null; |
|||
} |
|||
} |
|||
@ -0,0 +1,138 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\File\MimeType; |
|||
|
|||
use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException; |
|||
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; |
|||
use Symfony\Component\Mime\MimeTypes; |
|||
|
|||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" instead.', MimeTypeGuesser::class, MimeTypes::class), E_USER_DEPRECATED); |
|||
|
|||
/** |
|||
* A singleton mime type guesser. |
|||
* |
|||
* By default, all mime type guessers provided by the framework are installed |
|||
* (if available on the current OS/PHP setup). |
|||
* |
|||
* You can register custom guessers by calling the register() method on the |
|||
* singleton instance. Custom guessers are always called before any default ones. |
|||
* |
|||
* $guesser = MimeTypeGuesser::getInstance(); |
|||
* $guesser->register(new MyCustomMimeTypeGuesser()); |
|||
* |
|||
* If you want to change the order of the default guessers, just re-register your |
|||
* preferred one as a custom one. The last registered guesser is preferred over |
|||
* previously registered ones. |
|||
* |
|||
* Re-registering a built-in guesser also allows you to configure it: |
|||
* |
|||
* $guesser = MimeTypeGuesser::getInstance(); |
|||
* $guesser->register(new FileinfoMimeTypeGuesser('/path/to/magic/file')); |
|||
* |
|||
* @author Bernhard Schussek <bschussek@gmail.com> |
|||
*/ |
|||
class MimeTypeGuesser implements MimeTypeGuesserInterface |
|||
{ |
|||
/** |
|||
* The singleton instance. |
|||
* |
|||
* @var MimeTypeGuesser |
|||
*/ |
|||
private static $instance = null; |
|||
|
|||
/** |
|||
* All registered MimeTypeGuesserInterface instances. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $guessers = []; |
|||
|
|||
/** |
|||
* Returns the singleton instance. |
|||
* |
|||
* @return self |
|||
*/ |
|||
public static function getInstance() |
|||
{ |
|||
if (null === self::$instance) { |
|||
self::$instance = new self(); |
|||
} |
|||
|
|||
return self::$instance; |
|||
} |
|||
|
|||
/** |
|||
* Resets the singleton instance. |
|||
*/ |
|||
public static function reset() |
|||
{ |
|||
self::$instance = null; |
|||
} |
|||
|
|||
/** |
|||
* Registers all natively provided mime type guessers. |
|||
*/ |
|||
private function __construct() |
|||
{ |
|||
$this->register(new FileBinaryMimeTypeGuesser()); |
|||
$this->register(new FileinfoMimeTypeGuesser()); |
|||
} |
|||
|
|||
/** |
|||
* Registers a new mime type guesser. |
|||
* |
|||
* When guessing, this guesser is preferred over previously registered ones. |
|||
*/ |
|||
public function register(MimeTypeGuesserInterface $guesser) |
|||
{ |
|||
array_unshift($this->guessers, $guesser); |
|||
} |
|||
|
|||
/** |
|||
* Tries to guess the mime type of the given file. |
|||
* |
|||
* The file is passed to each registered mime type guesser in reverse order |
|||
* of their registration (last registered is queried first). Once a guesser |
|||
* returns a value that is not NULL, this method terminates and returns the |
|||
* value. |
|||
* |
|||
* @param string $path The path to the file |
|||
* |
|||
* @return string The mime type or NULL, if none could be guessed |
|||
* |
|||
* @throws \LogicException |
|||
* @throws FileNotFoundException |
|||
* @throws AccessDeniedException |
|||
*/ |
|||
public function guess($path) |
|||
{ |
|||
if (!is_file($path)) { |
|||
throw new FileNotFoundException($path); |
|||
} |
|||
|
|||
if (!is_readable($path)) { |
|||
throw new AccessDeniedException($path); |
|||
} |
|||
|
|||
foreach ($this->guessers as $guesser) { |
|||
if (null !== $mimeType = $guesser->guess($path)) { |
|||
return $mimeType; |
|||
} |
|||
} |
|||
|
|||
if (2 === \count($this->guessers) && !FileBinaryMimeTypeGuesser::isSupported() && !FileinfoMimeTypeGuesser::isSupported()) { |
|||
throw new \LogicException('Unable to guess the mime type as no guessers are available (Did you enable the php_fileinfo extension?)'); |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\File\MimeType; |
|||
|
|||
use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException; |
|||
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; |
|||
use Symfony\Component\Mime\MimeTypesInterface; |
|||
|
|||
/** |
|||
* Guesses the mime type of a file. |
|||
* |
|||
* @author Bernhard Schussek <bschussek@gmail.com> |
|||
* |
|||
* @deprecated since Symfony 4.3, use {@link MimeTypesInterface} instead |
|||
*/ |
|||
interface MimeTypeGuesserInterface |
|||
{ |
|||
/** |
|||
* Guesses the mime type of the file with the given path. |
|||
* |
|||
* @param string $path The path to the file |
|||
* |
|||
* @return string|null The mime type or NULL, if none could be guessed |
|||
* |
|||
* @throws FileNotFoundException If the file does not exist |
|||
* @throws AccessDeniedException If the file could not be read |
|||
*/ |
|||
public function guess($path); |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\File; |
|||
|
|||
/** |
|||
* A PHP stream of unknown size. |
|||
* |
|||
* @author Nicolas Grekas <p@tchwork.com> |
|||
*/ |
|||
class Stream extends File |
|||
{ |
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getSize() |
|||
{ |
|||
return false; |
|||
} |
|||
} |
|||
@ -0,0 +1,310 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\File; |
|||
|
|||
use Symfony\Component\HttpFoundation\File\Exception\CannotWriteFileException; |
|||
use Symfony\Component\HttpFoundation\File\Exception\ExtensionFileException; |
|||
use Symfony\Component\HttpFoundation\File\Exception\FileException; |
|||
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; |
|||
use Symfony\Component\HttpFoundation\File\Exception\FormSizeFileException; |
|||
use Symfony\Component\HttpFoundation\File\Exception\IniSizeFileException; |
|||
use Symfony\Component\HttpFoundation\File\Exception\NoFileException; |
|||
use Symfony\Component\HttpFoundation\File\Exception\NoTmpDirFileException; |
|||
use Symfony\Component\HttpFoundation\File\Exception\PartialFileException; |
|||
use Symfony\Component\Mime\MimeTypes; |
|||
|
|||
/** |
|||
* A file uploaded through a form. |
|||
* |
|||
* @author Bernhard Schussek <bschussek@gmail.com> |
|||
* @author Florian Eckerstorfer <florian@eckerstorfer.org> |
|||
* @author Fabien Potencier <fabien@symfony.com> |
|||
*/ |
|||
class UploadedFile extends File |
|||
{ |
|||
private $test = false; |
|||
private $originalName; |
|||
private $mimeType; |
|||
private $error; |
|||
|
|||
/** |
|||
* Accepts the information of the uploaded file as provided by the PHP global $_FILES. |
|||
* |
|||
* The file object is only created when the uploaded file is valid (i.e. when the |
|||
* isValid() method returns true). Otherwise the only methods that could be called |
|||
* on an UploadedFile instance are: |
|||
* |
|||
* * getClientOriginalName, |
|||
* * getClientMimeType, |
|||
* * isValid, |
|||
* * getError. |
|||
* |
|||
* Calling any other method on an non-valid instance will cause an unpredictable result. |
|||
* |
|||
* @param string $path The full temporary path to the file |
|||
* @param string $originalName The original file name of the uploaded file |
|||
* @param string|null $mimeType The type of the file as provided by PHP; null defaults to application/octet-stream |
|||
* @param int|null $error The error constant of the upload (one of PHP's UPLOAD_ERR_XXX constants); null defaults to UPLOAD_ERR_OK |
|||
* @param bool $test Whether the test mode is active |
|||
* Local files are used in test mode hence the code should not enforce HTTP uploads |
|||
* |
|||
* @throws FileException If file_uploads is disabled |
|||
* @throws FileNotFoundException If the file does not exist |
|||
*/ |
|||
public function __construct(string $path, string $originalName, string $mimeType = null, int $error = null, $test = false) |
|||
{ |
|||
$this->originalName = $this->getName($originalName); |
|||
$this->mimeType = $mimeType ?: 'application/octet-stream'; |
|||
|
|||
if (4 < \func_num_args() ? !\is_bool($test) : null !== $error && @filesize($path) === $error) { |
|||
@trigger_error(sprintf('Passing a size as 4th argument to the constructor of "%s" is deprecated since Symfony 4.1.', __CLASS__), E_USER_DEPRECATED); |
|||
$error = $test; |
|||
$test = 5 < \func_num_args() ? func_get_arg(5) : false; |
|||
} |
|||
|
|||
$this->error = $error ?: UPLOAD_ERR_OK; |
|||
$this->test = $test; |
|||
|
|||
parent::__construct($path, UPLOAD_ERR_OK === $this->error); |
|||
} |
|||
|
|||
/** |
|||
* Returns the original file name. |
|||
* |
|||
* It is extracted from the request from which the file has been uploaded. |
|||
* Then it should not be considered as a safe value. |
|||
* |
|||
* @return string|null The original name |
|||
*/ |
|||
public function getClientOriginalName() |
|||
{ |
|||
return $this->originalName; |
|||
} |
|||
|
|||
/** |
|||
* Returns the original file extension. |
|||
* |
|||
* It is extracted from the original file name that was uploaded. |
|||
* Then it should not be considered as a safe value. |
|||
* |
|||
* @return string The extension |
|||
*/ |
|||
public function getClientOriginalExtension() |
|||
{ |
|||
return pathinfo($this->originalName, PATHINFO_EXTENSION); |
|||
} |
|||
|
|||
/** |
|||
* Returns the file mime type. |
|||
* |
|||
* The client mime type is extracted from the request from which the file |
|||
* was uploaded, so it should not be considered as a safe value. |
|||
* |
|||
* For a trusted mime type, use getMimeType() instead (which guesses the mime |
|||
* type based on the file content). |
|||
* |
|||
* @return string|null The mime type |
|||
* |
|||
* @see getMimeType() |
|||
*/ |
|||
public function getClientMimeType() |
|||
{ |
|||
return $this->mimeType; |
|||
} |
|||
|
|||
/** |
|||
* Returns the extension based on the client mime type. |
|||
* |
|||
* If the mime type is unknown, returns null. |
|||
* |
|||
* This method uses the mime type as guessed by getClientMimeType() |
|||
* to guess the file extension. As such, the extension returned |
|||
* by this method cannot be trusted. |
|||
* |
|||
* For a trusted extension, use guessExtension() instead (which guesses |
|||
* the extension based on the guessed mime type for the file). |
|||
* |
|||
* @return string|null The guessed extension or null if it cannot be guessed |
|||
* |
|||
* @see guessExtension() |
|||
* @see getClientMimeType() |
|||
*/ |
|||
public function guessClientExtension() |
|||
{ |
|||
return MimeTypes::getDefault()->getExtensions($this->getClientMimeType())[0] ?? null; |
|||
} |
|||
|
|||
/** |
|||
* Returns the file size. |
|||
* |
|||
* It is extracted from the request from which the file has been uploaded. |
|||
* Then it should not be considered as a safe value. |
|||
* |
|||
* @deprecated since Symfony 4.1, use getSize() instead. |
|||
* |
|||
* @return int|null The file sizes |
|||
*/ |
|||
public function getClientSize() |
|||
{ |
|||
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1. Use getSize() instead.', __METHOD__), E_USER_DEPRECATED); |
|||
|
|||
return $this->getSize(); |
|||
} |
|||
|
|||
/** |
|||
* Returns the upload error. |
|||
* |
|||
* If the upload was successful, the constant UPLOAD_ERR_OK is returned. |
|||
* Otherwise one of the other UPLOAD_ERR_XXX constants is returned. |
|||
* |
|||
* @return int The upload error |
|||
*/ |
|||
public function getError() |
|||
{ |
|||
return $this->error; |
|||
} |
|||
|
|||
/** |
|||
* Returns whether the file was uploaded successfully. |
|||
* |
|||
* @return bool True if the file has been uploaded with HTTP and no error occurred |
|||
*/ |
|||
public function isValid() |
|||
{ |
|||
$isOk = UPLOAD_ERR_OK === $this->error; |
|||
|
|||
return $this->test ? $isOk : $isOk && is_uploaded_file($this->getPathname()); |
|||
} |
|||
|
|||
/** |
|||
* Moves the file to a new location. |
|||
* |
|||
* @param string $directory The destination folder |
|||
* @param string $name The new file name |
|||
* |
|||
* @return File A File object representing the new file |
|||
* |
|||
* @throws FileException if, for any reason, the file could not have been moved |
|||
*/ |
|||
public function move($directory, $name = null) |
|||
{ |
|||
if ($this->isValid()) { |
|||
if ($this->test) { |
|||
return parent::move($directory, $name); |
|||
} |
|||
|
|||
$target = $this->getTargetFile($directory, $name); |
|||
|
|||
set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; }); |
|||
$moved = move_uploaded_file($this->getPathname(), $target); |
|||
restore_error_handler(); |
|||
if (!$moved) { |
|||
throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $this->getPathname(), $target, strip_tags($error))); |
|||
} |
|||
|
|||
@chmod($target, 0666 & ~umask()); |
|||
|
|||
return $target; |
|||
} |
|||
|
|||
switch ($this->error) { |
|||
case UPLOAD_ERR_INI_SIZE: |
|||
throw new IniSizeFileException($this->getErrorMessage()); |
|||
case UPLOAD_ERR_FORM_SIZE: |
|||
throw new FormSizeFileException($this->getErrorMessage()); |
|||
case UPLOAD_ERR_PARTIAL: |
|||
throw new PartialFileException($this->getErrorMessage()); |
|||
case UPLOAD_ERR_NO_FILE: |
|||
throw new NoFileException($this->getErrorMessage()); |
|||
case UPLOAD_ERR_CANT_WRITE: |
|||
throw new CannotWriteFileException($this->getErrorMessage()); |
|||
case UPLOAD_ERR_NO_TMP_DIR: |
|||
throw new NoTmpDirFileException($this->getErrorMessage()); |
|||
case UPLOAD_ERR_EXTENSION: |
|||
throw new ExtensionFileException($this->getErrorMessage()); |
|||
} |
|||
|
|||
throw new FileException($this->getErrorMessage()); |
|||
} |
|||
|
|||
/** |
|||
* Returns the maximum size of an uploaded file as configured in php.ini. |
|||
* |
|||
* @return int The maximum size of an uploaded file in bytes |
|||
*/ |
|||
public static function getMaxFilesize() |
|||
{ |
|||
$sizePostMax = self::parseFilesize(ini_get('post_max_size')); |
|||
$sizeUploadMax = self::parseFilesize(ini_get('upload_max_filesize')); |
|||
|
|||
return min($sizePostMax ?: PHP_INT_MAX, $sizeUploadMax ?: PHP_INT_MAX); |
|||
} |
|||
|
|||
/** |
|||
* Returns the given size from an ini value in bytes. |
|||
* |
|||
* @return int The given size in bytes |
|||
*/ |
|||
private static function parseFilesize($size) |
|||
{ |
|||
if ('' === $size) { |
|||
return 0; |
|||
} |
|||
|
|||
$size = strtolower($size); |
|||
|
|||
$max = ltrim($size, '+'); |
|||
if (0 === strpos($max, '0x')) { |
|||
$max = \intval($max, 16); |
|||
} elseif (0 === strpos($max, '0')) { |
|||
$max = \intval($max, 8); |
|||
} else { |
|||
$max = (int) $max; |
|||
} |
|||
|
|||
switch (substr($size, -1)) { |
|||
case 't': $max *= 1024; |
|||
// no break |
|||
case 'g': $max *= 1024; |
|||
// no break |
|||
case 'm': $max *= 1024; |
|||
// no break |
|||
case 'k': $max *= 1024; |
|||
} |
|||
|
|||
return $max; |
|||
} |
|||
|
|||
/** |
|||
* Returns an informative upload error message. |
|||
* |
|||
* @return string The error message regarding the specified error code |
|||
*/ |
|||
public function getErrorMessage() |
|||
{ |
|||
static $errors = [ |
|||
UPLOAD_ERR_INI_SIZE => 'The file "%s" exceeds your upload_max_filesize ini directive (limit is %d KiB).', |
|||
UPLOAD_ERR_FORM_SIZE => 'The file "%s" exceeds the upload limit defined in your form.', |
|||
UPLOAD_ERR_PARTIAL => 'The file "%s" was only partially uploaded.', |
|||
UPLOAD_ERR_NO_FILE => 'No file was uploaded.', |
|||
UPLOAD_ERR_CANT_WRITE => 'The file "%s" could not be written on disk.', |
|||
UPLOAD_ERR_NO_TMP_DIR => 'File could not be uploaded: missing temporary directory.', |
|||
UPLOAD_ERR_EXTENSION => 'File upload was stopped by a PHP extension.', |
|||
]; |
|||
|
|||
$errorCode = $this->error; |
|||
$maxFilesize = UPLOAD_ERR_INI_SIZE === $errorCode ? self::getMaxFilesize() / 1024 : 0; |
|||
$message = isset($errors[$errorCode]) ? $errors[$errorCode] : 'The file "%s" was not uploaded due to an unknown error.'; |
|||
|
|||
return sprintf($message, $this->getClientOriginalName(), $maxFilesize); |
|||
} |
|||
} |
|||
@ -0,0 +1,142 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation; |
|||
|
|||
use Symfony\Component\HttpFoundation\File\UploadedFile; |
|||
|
|||
/** |
|||
* FileBag is a container for uploaded files. |
|||
* |
|||
* @author Fabien Potencier <fabien@symfony.com> |
|||
* @author Bulat Shakirzyanov <mallluhuct@gmail.com> |
|||
*/ |
|||
class FileBag extends ParameterBag |
|||
{ |
|||
private static $fileKeys = ['error', 'name', 'size', 'tmp_name', 'type']; |
|||
|
|||
/** |
|||
* @param array $parameters An array of HTTP files |
|||
*/ |
|||
public function __construct(array $parameters = []) |
|||
{ |
|||
$this->replace($parameters); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function replace(array $files = []) |
|||
{ |
|||
$this->parameters = []; |
|||
$this->add($files); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function set($key, $value) |
|||
{ |
|||
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)); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function add(array $files = []) |
|||
{ |
|||
foreach ($files as $key => $file) { |
|||
$this->set($key, $file); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Converts uploaded files to UploadedFile instances. |
|||
* |
|||
* @param array|UploadedFile $file A (multi-dimensional) array of uploaded file information |
|||
* |
|||
* @return UploadedFile[]|UploadedFile|null A (multi-dimensional) array of UploadedFile instances |
|||
*/ |
|||
protected function convertFileInformation($file) |
|||
{ |
|||
if ($file instanceof UploadedFile) { |
|||
return $file; |
|||
} |
|||
|
|||
if (\is_array($file)) { |
|||
$file = $this->fixPhpFilesArray($file); |
|||
$keys = array_keys($file); |
|||
sort($keys); |
|||
|
|||
if ($keys == self::$fileKeys) { |
|||
if (UPLOAD_ERR_NO_FILE == $file['error']) { |
|||
$file = null; |
|||
} else { |
|||
$file = new UploadedFile($file['tmp_name'], $file['name'], $file['type'], $file['error'], false); |
|||
} |
|||
} else { |
|||
$file = array_map([$this, 'convertFileInformation'], $file); |
|||
if (array_keys($keys) === $keys) { |
|||
$file = array_filter($file); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return $file; |
|||
} |
|||
|
|||
/** |
|||
* Fixes a malformed PHP $_FILES array. |
|||
* |
|||
* PHP has a bug that the format of the $_FILES array differs, depending on |
|||
* whether the uploaded file fields had normal field names or array-like |
|||
* field names ("normal" vs. "parent[child]"). |
|||
* |
|||
* This method fixes the array to look like the "normal" $_FILES array. |
|||
* |
|||
* It's safe to pass an already converted array, in which case this method |
|||
* just returns the original array unmodified. |
|||
* |
|||
* @param array $data |
|||
* |
|||
* @return array |
|||
*/ |
|||
protected function fixPhpFilesArray($data) |
|||
{ |
|||
$keys = array_keys($data); |
|||
sort($keys); |
|||
|
|||
if (self::$fileKeys != $keys || !isset($data['name']) || !\is_array($data['name'])) { |
|||
return $data; |
|||
} |
|||
|
|||
$files = $data; |
|||
foreach (self::$fileKeys as $k) { |
|||
unset($files[$k]); |
|||
} |
|||
|
|||
foreach ($data['name'] as $key => $name) { |
|||
$files[$key] = $this->fixPhpFilesArray([ |
|||
'error' => $data['error'][$key], |
|||
'name' => $name, |
|||
'type' => $data['type'][$key], |
|||
'tmp_name' => $data['tmp_name'][$key], |
|||
'size' => $data['size'][$key], |
|||
]); |
|||
} |
|||
|
|||
return $files; |
|||
} |
|||
} |
|||
@ -0,0 +1,315 @@ |
|||
<?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; |
|||
|
|||
/** |
|||
* HeaderBag is a container for HTTP headers. |
|||
* |
|||
* @author Fabien Potencier <fabien@symfony.com> |
|||
*/ |
|||
class HeaderBag implements \IteratorAggregate, \Countable |
|||
{ |
|||
protected $headers = []; |
|||
protected $cacheControl = []; |
|||
|
|||
/** |
|||
* @param array $headers An array of HTTP headers |
|||
*/ |
|||
public function __construct(array $headers = []) |
|||
{ |
|||
foreach ($headers as $key => $values) { |
|||
$this->set($key, $values); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Returns the headers as a string. |
|||
* |
|||
* @return string The headers |
|||
*/ |
|||
public function __toString() |
|||
{ |
|||
if (!$headers = $this->all()) { |
|||
return ''; |
|||
} |
|||
|
|||
ksort($headers); |
|||
$max = max(array_map('strlen', array_keys($headers))) + 1; |
|||
$content = ''; |
|||
foreach ($headers as $name => $values) { |
|||
$name = ucwords($name, '-'); |
|||
foreach ($values as $value) { |
|||
$content .= sprintf("%-{$max}s %s\r\n", $name.':', $value); |
|||
} |
|||
} |
|||
|
|||
return $content; |
|||
} |
|||
|
|||
/** |
|||
* Returns the headers. |
|||
* |
|||
* @return array An array of headers |
|||
*/ |
|||
public function all() |
|||
{ |
|||
return $this->headers; |
|||
} |
|||
|
|||
/** |
|||
* Returns the parameter keys. |
|||
* |
|||
* @return array An array of parameter keys |
|||
*/ |
|||
public function keys() |
|||
{ |
|||
return array_keys($this->all()); |
|||
} |
|||
|
|||
/** |
|||
* Replaces the current HTTP headers by a new set. |
|||
* |
|||
* @param array $headers An array of HTTP headers |
|||
*/ |
|||
public function replace(array $headers = []) |
|||
{ |
|||
$this->headers = []; |
|||
$this->add($headers); |
|||
} |
|||
|
|||
/** |
|||
* Adds new headers the current HTTP headers set. |
|||
* |
|||
* @param array $headers An array of HTTP headers |
|||
*/ |
|||
public function add(array $headers) |
|||
{ |
|||
foreach ($headers as $key => $values) { |
|||
$this->set($key, $values); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Returns a header value by name. |
|||
* |
|||
* @param string $key The header name |
|||
* @param string|null $default The default value |
|||
* @param bool $first Whether to return the first value or all header values |
|||
* |
|||
* @return string|string[]|null The first header value or default value if $first is true, an array of values otherwise |
|||
*/ |
|||
public function get($key, $default = null, $first = true) |
|||
{ |
|||
$key = str_replace('_', '-', strtolower($key)); |
|||
$headers = $this->all(); |
|||
|
|||
if (!\array_key_exists($key, $headers)) { |
|||
if (null === $default) { |
|||
return $first ? null : []; |
|||
} |
|||
|
|||
return $first ? $default : [$default]; |
|||
} |
|||
|
|||
if ($first) { |
|||
return \count($headers[$key]) ? (string) $headers[$key][0] : $default; |
|||
} |
|||
|
|||
return $headers[$key]; |
|||
} |
|||
|
|||
/** |
|||
* Sets a header by name. |
|||
* |
|||
* @param string $key The key |
|||
* @param string|string[] $values The value or an array of values |
|||
* @param bool $replace Whether to replace the actual value or not (true by default) |
|||
*/ |
|||
public function set($key, $values, $replace = true) |
|||
{ |
|||
$key = str_replace('_', '-', strtolower($key)); |
|||
|
|||
if (\is_array($values)) { |
|||
$values = array_values($values); |
|||
|
|||
if (true === $replace || !isset($this->headers[$key])) { |
|||
$this->headers[$key] = $values; |
|||
} else { |
|||
$this->headers[$key] = array_merge($this->headers[$key], $values); |
|||
} |
|||
} else { |
|||
if (true === $replace || !isset($this->headers[$key])) { |
|||
$this->headers[$key] = [$values]; |
|||
} else { |
|||
$this->headers[$key][] = $values; |
|||
} |
|||
} |
|||
|
|||
if ('cache-control' === $key) { |
|||
$this->cacheControl = $this->parseCacheControl(implode(', ', $this->headers[$key])); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Returns true if the HTTP header is defined. |
|||
* |
|||
* @param string $key The HTTP header |
|||
* |
|||
* @return bool true if the parameter exists, false otherwise |
|||
*/ |
|||
public function has($key) |
|||
{ |
|||
return \array_key_exists(str_replace('_', '-', strtolower($key)), $this->all()); |
|||
} |
|||
|
|||
/** |
|||
* Returns true if the given HTTP header contains the given value. |
|||
* |
|||
* @param string $key The HTTP header name |
|||
* @param string $value The HTTP value |
|||
* |
|||
* @return bool true if the value is contained in the header, false otherwise |
|||
*/ |
|||
public function contains($key, $value) |
|||
{ |
|||
return \in_array($value, $this->get($key, null, false)); |
|||
} |
|||
|
|||
/** |
|||
* Removes a header. |
|||
* |
|||
* @param string $key The HTTP header name |
|||
*/ |
|||
public function remove($key) |
|||
{ |
|||
$key = str_replace('_', '-', strtolower($key)); |
|||
|
|||
unset($this->headers[$key]); |
|||
|
|||
if ('cache-control' === $key) { |
|||
$this->cacheControl = []; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Returns the HTTP header value converted to a date. |
|||
* |
|||
* @param string $key The parameter key |
|||
* @param \DateTime $default The default value |
|||
* |
|||
* @return \DateTime|null The parsed DateTime or the default value if the header does not exist |
|||
* |
|||
* @throws \RuntimeException When the HTTP header is not parseable |
|||
*/ |
|||
public function getDate($key, \DateTime $default = null) |
|||
{ |
|||
if (null === $value = $this->get($key)) { |
|||
return $default; |
|||
} |
|||
|
|||
if (false === $date = \DateTime::createFromFormat(DATE_RFC2822, $value)) { |
|||
throw new \RuntimeException(sprintf('The %s HTTP header is not parseable (%s).', $key, $value)); |
|||
} |
|||
|
|||
return $date; |
|||
} |
|||
|
|||
/** |
|||
* Adds a custom Cache-Control directive. |
|||
* |
|||
* @param string $key The Cache-Control directive name |
|||
* @param mixed $value The Cache-Control directive value |
|||
*/ |
|||
public function addCacheControlDirective($key, $value = true) |
|||
{ |
|||
$this->cacheControl[$key] = $value; |
|||
|
|||
$this->set('Cache-Control', $this->getCacheControlHeader()); |
|||
} |
|||
|
|||
/** |
|||
* Returns true if the Cache-Control directive is defined. |
|||
* |
|||
* @param string $key The Cache-Control directive |
|||
* |
|||
* @return bool true if the directive exists, false otherwise |
|||
*/ |
|||
public function hasCacheControlDirective($key) |
|||
{ |
|||
return \array_key_exists($key, $this->cacheControl); |
|||
} |
|||
|
|||
/** |
|||
* Returns a Cache-Control directive value by name. |
|||
* |
|||
* @param string $key The directive name |
|||
* |
|||
* @return mixed|null The directive value if defined, null otherwise |
|||
*/ |
|||
public function getCacheControlDirective($key) |
|||
{ |
|||
return \array_key_exists($key, $this->cacheControl) ? $this->cacheControl[$key] : null; |
|||
} |
|||
|
|||
/** |
|||
* Removes a Cache-Control directive. |
|||
* |
|||
* @param string $key The Cache-Control directive |
|||
*/ |
|||
public function removeCacheControlDirective($key) |
|||
{ |
|||
unset($this->cacheControl[$key]); |
|||
|
|||
$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() |
|||
{ |
|||
ksort($this->cacheControl); |
|||
|
|||
return HeaderUtils::toString($this->cacheControl, ','); |
|||
} |
|||
|
|||
/** |
|||
* Parses a Cache-Control HTTP header. |
|||
* |
|||
* @param string $header The value of the Cache-Control HTTP header |
|||
* |
|||
* @return array An array representing the attribute values |
|||
*/ |
|||
protected function parseCacheControl($header) |
|||
{ |
|||
$parts = HeaderUtils::split($header, ',='); |
|||
|
|||
return HeaderUtils::combine($parts); |
|||
} |
|||
} |
|||
@ -0,0 +1,224 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation; |
|||
|
|||
/** |
|||
* HTTP header utility functions. |
|||
* |
|||
* @author Christian Schmidt <github@chsc.dk> |
|||
*/ |
|||
class HeaderUtils |
|||
{ |
|||
public const DISPOSITION_ATTACHMENT = 'attachment'; |
|||
public const DISPOSITION_INLINE = 'inline'; |
|||
|
|||
/** |
|||
* This class should not be instantiated. |
|||
*/ |
|||
private function __construct() |
|||
{ |
|||
} |
|||
|
|||
/** |
|||
* Splits an HTTP header by one or more separators. |
|||
* |
|||
* Example: |
|||
* |
|||
* HeaderUtils::split("da, en-gb;q=0.8", ",;") |
|||
* // => ['da'], ['en-gb', 'q=0.8']] |
|||
* |
|||
* @param string $separators List of characters to split on, ordered by |
|||
* precedence, e.g. ",", ";=", or ",;=" |
|||
* |
|||
* @return array Nested array with as many levels as there are characters in |
|||
* $separators |
|||
*/ |
|||
public static function split(string $header, string $separators): array |
|||
{ |
|||
$quotedSeparators = preg_quote($separators, '/'); |
|||
|
|||
preg_match_all(' |
|||
/ |
|||
(?!\s) |
|||
(?: |
|||
# quoted-string |
|||
"(?:[^"\\\\]|\\\\.)*(?:"|\\\\|$) |
|||
| |
|||
# token |
|||
[^"'.$quotedSeparators.']+ |
|||
)+ |
|||
(?<!\s) |
|||
| |
|||
# separator |
|||
\s* |
|||
(?<separator>['.$quotedSeparators.']) |
|||
\s* |
|||
/x', trim($header), $matches, PREG_SET_ORDER); |
|||
|
|||
return self::groupParts($matches, $separators); |
|||
} |
|||
|
|||
/** |
|||
* Combines an array of arrays into one associative array. |
|||
* |
|||
* Each of the nested arrays should have one or two elements. The first |
|||
* value will be used as the keys in the associative array, and the second |
|||
* will be used as the values, or true if the nested array only contains one |
|||
* element. Array keys are lowercased. |
|||
* |
|||
* Example: |
|||
* |
|||
* HeaderUtils::combine([["foo", "abc"], ["bar"]]) |
|||
* // => ["foo" => "abc", "bar" => true] |
|||
*/ |
|||
public static function combine(array $parts): array |
|||
{ |
|||
$assoc = []; |
|||
foreach ($parts as $part) { |
|||
$name = strtolower($part[0]); |
|||
$value = $part[1] ?? true; |
|||
$assoc[$name] = $value; |
|||
} |
|||
|
|||
return $assoc; |
|||
} |
|||
|
|||
/** |
|||
* Joins an associative array into a string for use in an HTTP header. |
|||
* |
|||
* The key and value of each entry are joined with "=", and all entries |
|||
* are joined with the specified separator and an additional space (for |
|||
* readability). Values are quoted if necessary. |
|||
* |
|||
* Example: |
|||
* |
|||
* HeaderUtils::toString(["foo" => "abc", "bar" => true, "baz" => "a b c"], ",") |
|||
* // => 'foo=abc, bar, baz="a b c"' |
|||
*/ |
|||
public static function toString(array $assoc, string $separator): string |
|||
{ |
|||
$parts = []; |
|||
foreach ($assoc as $name => $value) { |
|||
if (true === $value) { |
|||
$parts[] = $name; |
|||
} else { |
|||
$parts[] = $name.'='.self::quote($value); |
|||
} |
|||
} |
|||
|
|||
return implode($separator.' ', $parts); |
|||
} |
|||
|
|||
/** |
|||
* Encodes a string as a quoted string, if necessary. |
|||
* |
|||
* If a string contains characters not allowed by the "token" construct in |
|||
* the HTTP specification, it is backslash-escaped and enclosed in quotes |
|||
* to match the "quoted-string" construct. |
|||
*/ |
|||
public static function quote(string $s): string |
|||
{ |
|||
if (preg_match('/^[a-z0-9!#$%&\'*.^_`|~-]+$/i', $s)) { |
|||
return $s; |
|||
} |
|||
|
|||
return '"'.addcslashes($s, '"\\"').'"'; |
|||
} |
|||
|
|||
/** |
|||
* Decodes a quoted string. |
|||
* |
|||
* If passed an unquoted string that matches the "token" construct (as |
|||
* defined in the HTTP specification), it is passed through verbatimly. |
|||
*/ |
|||
public static function unquote(string $s): string |
|||
{ |
|||
return preg_replace('/\\\\(.)|"/', '$1', $s); |
|||
} |
|||
|
|||
/** |
|||
* Generates a HTTP Content-Disposition field-value. |
|||
* |
|||
* @param string $disposition One of "inline" or "attachment" |
|||
* @param string $filename A unicode string |
|||
* @param string $filenameFallback A string containing only ASCII characters that |
|||
* is semantically equivalent to $filename. If the filename is already ASCII, |
|||
* it can be omitted, or just copied from $filename |
|||
* |
|||
* @return string A string suitable for use as a Content-Disposition field-value |
|||
* |
|||
* @throws \InvalidArgumentException |
|||
* |
|||
* @see RFC 6266 |
|||
*/ |
|||
public static function makeDisposition(string $disposition, string $filename, string $filenameFallback = ''): string |
|||
{ |
|||
if (!\in_array($disposition, [self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE])) { |
|||
throw new \InvalidArgumentException(sprintf('The disposition must be either "%s" or "%s".', self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE)); |
|||
} |
|||
|
|||
if ('' === $filenameFallback) { |
|||
$filenameFallback = $filename; |
|||
} |
|||
|
|||
// filenameFallback is not ASCII. |
|||
if (!preg_match('/^[\x20-\x7e]*$/', $filenameFallback)) { |
|||
throw new \InvalidArgumentException('The filename fallback must only contain ASCII characters.'); |
|||
} |
|||
|
|||
// percent characters aren't safe in fallback. |
|||
if (false !== strpos($filenameFallback, '%')) { |
|||
throw new \InvalidArgumentException('The filename fallback cannot contain the "%" character.'); |
|||
} |
|||
|
|||
// path separators aren't allowed in either. |
|||
if (false !== strpos($filename, '/') || false !== strpos($filename, '\\') || false !== strpos($filenameFallback, '/') || false !== strpos($filenameFallback, '\\')) { |
|||
throw new \InvalidArgumentException('The filename and the fallback cannot contain the "/" and "\\" characters.'); |
|||
} |
|||
|
|||
$params = ['filename' => $filenameFallback]; |
|||
if ($filename !== $filenameFallback) { |
|||
$params['filename*'] = "utf-8''".rawurlencode($filename); |
|||
} |
|||
|
|||
return $disposition.'; '.self::toString($params, ';'); |
|||
} |
|||
|
|||
private static function groupParts(array $matches, string $separators): array |
|||
{ |
|||
$separator = $separators[0]; |
|||
$partSeparators = substr($separators, 1); |
|||
|
|||
$i = 0; |
|||
$partMatches = []; |
|||
foreach ($matches as $match) { |
|||
if (isset($match['separator']) && $match['separator'] === $separator) { |
|||
++$i; |
|||
} else { |
|||
$partMatches[$i][] = $match; |
|||
} |
|||
} |
|||
|
|||
$parts = []; |
|||
if ($partSeparators) { |
|||
foreach ($partMatches as $matches) { |
|||
$parts[] = self::groupParts($matches, $partSeparators); |
|||
} |
|||
} else { |
|||
foreach ($partMatches as $matches) { |
|||
$parts[] = self::unquote($matches[0][0]); |
|||
} |
|||
} |
|||
|
|||
return $parts; |
|||
} |
|||
} |
|||
@ -0,0 +1,156 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation; |
|||
|
|||
/** |
|||
* Http utility functions. |
|||
* |
|||
* @author Fabien Potencier <fabien@symfony.com> |
|||
*/ |
|||
class IpUtils |
|||
{ |
|||
private static $checkedIps = []; |
|||
|
|||
/** |
|||
* This class should not be instantiated. |
|||
*/ |
|||
private function __construct() |
|||
{ |
|||
} |
|||
|
|||
/** |
|||
* Checks if an IPv4 or IPv6 address is contained in the list of given IPs or subnets. |
|||
* |
|||
* @param string $requestIp IP to check |
|||
* @param string|array $ips List of IPs or subnets (can be a string if only a single one) |
|||
* |
|||
* @return bool Whether the IP is valid |
|||
*/ |
|||
public static function checkIp($requestIp, $ips) |
|||
{ |
|||
if (!\is_array($ips)) { |
|||
$ips = [$ips]; |
|||
} |
|||
|
|||
$method = substr_count($requestIp, ':') > 1 ? 'checkIp6' : 'checkIp4'; |
|||
|
|||
foreach ($ips as $ip) { |
|||
if (self::$method($requestIp, $ip)) { |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* Compares two IPv4 addresses. |
|||
* In case a subnet is given, it checks if it contains the request IP. |
|||
* |
|||
* @param string $requestIp IPv4 address to check |
|||
* @param string $ip IPv4 address or subnet in CIDR notation |
|||
* |
|||
* @return bool Whether the request IP matches the IP, or whether the request IP is within the CIDR subnet |
|||
*/ |
|||
public static function checkIp4($requestIp, $ip) |
|||
{ |
|||
$cacheKey = $requestIp.'-'.$ip; |
|||
if (isset(self::$checkedIps[$cacheKey])) { |
|||
return self::$checkedIps[$cacheKey]; |
|||
} |
|||
|
|||
if (!filter_var($requestIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { |
|||
return self::$checkedIps[$cacheKey] = false; |
|||
} |
|||
|
|||
if (false !== strpos($ip, '/')) { |
|||
list($address, $netmask) = explode('/', $ip, 2); |
|||
|
|||
if ('0' === $netmask) { |
|||
return self::$checkedIps[$cacheKey] = filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); |
|||
} |
|||
|
|||
if ($netmask < 0 || $netmask > 32) { |
|||
return self::$checkedIps[$cacheKey] = false; |
|||
} |
|||
} else { |
|||
$address = $ip; |
|||
$netmask = 32; |
|||
} |
|||
|
|||
if (false === ip2long($address)) { |
|||
return self::$checkedIps[$cacheKey] = false; |
|||
} |
|||
|
|||
return self::$checkedIps[$cacheKey] = 0 === substr_compare(sprintf('%032b', ip2long($requestIp)), sprintf('%032b', ip2long($address)), 0, $netmask); |
|||
} |
|||
|
|||
/** |
|||
* Compares two IPv6 addresses. |
|||
* In case a subnet is given, it checks if it contains the request IP. |
|||
* |
|||
* @author David Soria Parra <dsp at php dot net> |
|||
* |
|||
* @see https://github.com/dsp/v6tools |
|||
* |
|||
* @param string $requestIp IPv6 address to check |
|||
* @param string $ip IPv6 address or subnet in CIDR notation |
|||
* |
|||
* @return bool Whether the IP is valid |
|||
* |
|||
* @throws \RuntimeException When IPV6 support is not enabled |
|||
*/ |
|||
public static function checkIp6($requestIp, $ip) |
|||
{ |
|||
$cacheKey = $requestIp.'-'.$ip; |
|||
if (isset(self::$checkedIps[$cacheKey])) { |
|||
return self::$checkedIps[$cacheKey]; |
|||
} |
|||
|
|||
if (!((\extension_loaded('sockets') && \defined('AF_INET6')) || @inet_pton('::1'))) { |
|||
throw new \RuntimeException('Unable to check Ipv6. Check that PHP was not compiled with option "disable-ipv6".'); |
|||
} |
|||
|
|||
if (false !== strpos($ip, '/')) { |
|||
list($address, $netmask) = explode('/', $ip, 2); |
|||
|
|||
if ('0' === $netmask) { |
|||
return (bool) unpack('n*', @inet_pton($address)); |
|||
} |
|||
|
|||
if ($netmask < 1 || $netmask > 128) { |
|||
return self::$checkedIps[$cacheKey] = false; |
|||
} |
|||
} else { |
|||
$address = $ip; |
|||
$netmask = 128; |
|||
} |
|||
|
|||
$bytesAddr = unpack('n*', @inet_pton($address)); |
|||
$bytesTest = unpack('n*', @inet_pton($requestIp)); |
|||
|
|||
if (!$bytesAddr || !$bytesTest) { |
|||
return self::$checkedIps[$cacheKey] = false; |
|||
} |
|||
|
|||
for ($i = 1, $ceil = ceil($netmask / 16); $i <= $ceil; ++$i) { |
|||
$left = $netmask - 16 * ($i - 1); |
|||
$left = ($left <= 16) ? $left : 16; |
|||
$mask = ~(0xffff >> $left) & 0xffff; |
|||
if (($bytesAddr[$i] & $mask) != ($bytesTest[$i] & $mask)) { |
|||
return self::$checkedIps[$cacheKey] = false; |
|||
} |
|||
} |
|||
|
|||
return self::$checkedIps[$cacheKey] = true; |
|||
} |
|||
} |
|||
@ -0,0 +1,219 @@ |
|||
<?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; |
|||
|
|||
/** |
|||
* Response represents an HTTP response in JSON format. |
|||
* |
|||
* Note that this class does not force the returned JSON content to be an |
|||
* object. It is however recommended that you do return an object as it |
|||
* protects yourself against XSSI and JSON-JavaScript Hijacking. |
|||
* |
|||
* @see https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/AJAX_Security_Cheat_Sheet.md#always-return-json-with-an-object-on-the-outside |
|||
* |
|||
* @author Igor Wiedler <igor@wiedler.ch> |
|||
*/ |
|||
class JsonResponse extends Response |
|||
{ |
|||
protected $data; |
|||
protected $callback; |
|||
|
|||
// Encode <, >, ', &, and " characters in the JSON, making it also safe to be embedded into HTML. |
|||
// 15 === JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT |
|||
const DEFAULT_ENCODING_OPTIONS = 15; |
|||
|
|||
protected $encodingOptions = self::DEFAULT_ENCODING_OPTIONS; |
|||
|
|||
/** |
|||
* @param mixed $data The response data |
|||
* @param int $status The response status code |
|||
* @param array $headers An array of response headers |
|||
* @param bool $json If the data is already a JSON string |
|||
*/ |
|||
public function __construct($data = null, int $status = 200, array $headers = [], bool $json = false) |
|||
{ |
|||
parent::__construct('', $status, $headers); |
|||
|
|||
if (null === $data) { |
|||
$data = new \ArrayObject(); |
|||
} |
|||
|
|||
$json ? $this->setJson($data) : $this->setData($data); |
|||
} |
|||
|
|||
/** |
|||
* Factory method for chainability. |
|||
* |
|||
* Example: |
|||
* |
|||
* return JsonResponse::create(['key' => 'value']) |
|||
* ->setSharedMaxAge(300); |
|||
* |
|||
* @param mixed $data The JSON response data |
|||
* @param int $status The response status code |
|||
* @param array $headers An array of response headers |
|||
* |
|||
* @return static |
|||
*/ |
|||
public static function create($data = null, $status = 200, $headers = []) |
|||
{ |
|||
return new static($data, $status, $headers); |
|||
} |
|||
|
|||
/** |
|||
* Factory method for chainability. |
|||
* |
|||
* Example: |
|||
* |
|||
* return JsonResponse::fromJsonString('{"key": "value"}') |
|||
* ->setSharedMaxAge(300); |
|||
* |
|||
* @param string|null $data The JSON response string |
|||
* @param int $status The response status code |
|||
* @param array $headers An array of response headers |
|||
* |
|||
* @return static |
|||
*/ |
|||
public static function fromJsonString($data = null, $status = 200, $headers = []) |
|||
{ |
|||
return new static($data, $status, $headers, true); |
|||
} |
|||
|
|||
/** |
|||
* Sets the JSONP callback. |
|||
* |
|||
* @param string|null $callback The JSONP callback or null to use none |
|||
* |
|||
* @return $this |
|||
* |
|||
* @throws \InvalidArgumentException When the callback name is not valid |
|||
*/ |
|||
public function setCallback($callback = null) |
|||
{ |
|||
if (null !== $callback) { |
|||
// partially taken from https://geekality.net/2011/08/03/valid-javascript-identifier/ |
|||
// partially taken from https://github.com/willdurand/JsonpCallbackValidator |
|||
// JsonpCallbackValidator is released under the MIT License. See https://github.com/willdurand/JsonpCallbackValidator/blob/v1.1.0/LICENSE for details. |
|||
// (c) William Durand <william.durand1@gmail.com> |
|||
$pattern = '/^[$_\p{L}][$_\p{L}\p{Mn}\p{Mc}\p{Nd}\p{Pc}\x{200C}\x{200D}]*(?:\[(?:"(?:\\\.|[^"\\\])*"|\'(?:\\\.|[^\'\\\])*\'|\d+)\])*?$/u'; |
|||
$reserved = [ |
|||
'break', 'do', 'instanceof', 'typeof', 'case', 'else', 'new', 'var', 'catch', 'finally', 'return', 'void', 'continue', 'for', 'switch', 'while', |
|||
'debugger', 'function', 'this', 'with', 'default', 'if', 'throw', 'delete', 'in', 'try', 'class', 'enum', 'extends', 'super', 'const', 'export', |
|||
'import', 'implements', 'let', 'private', 'public', 'yield', 'interface', 'package', 'protected', 'static', 'null', 'true', 'false', |
|||
]; |
|||
$parts = explode('.', $callback); |
|||
foreach ($parts as $part) { |
|||
if (!preg_match($pattern, $part) || \in_array($part, $reserved, true)) { |
|||
throw new \InvalidArgumentException('The callback name is not valid.'); |
|||
} |
|||
} |
|||
} |
|||
|
|||
$this->callback = $callback; |
|||
|
|||
return $this->update(); |
|||
} |
|||
|
|||
/** |
|||
* Sets a raw string containing a JSON document to be sent. |
|||
* |
|||
* @param string $json |
|||
* |
|||
* @return $this |
|||
* |
|||
* @throws \InvalidArgumentException |
|||
*/ |
|||
public function setJson($json) |
|||
{ |
|||
$this->data = $json; |
|||
|
|||
return $this->update(); |
|||
} |
|||
|
|||
/** |
|||
* Sets the data to be sent as JSON. |
|||
* |
|||
* @param mixed $data |
|||
* |
|||
* @return $this |
|||
* |
|||
* @throws \InvalidArgumentException |
|||
*/ |
|||
public function setData($data = []) |
|||
{ |
|||
try { |
|||
$data = json_encode($data, $this->encodingOptions); |
|||
} catch (\Exception $e) { |
|||
if ('Exception' === \get_class($e) && 0 === strpos($e->getMessage(), 'Failed calling ')) { |
|||
throw $e->getPrevious() ?: $e; |
|||
} |
|||
throw $e; |
|||
} |
|||
|
|||
if (\PHP_VERSION_ID >= 70300 && (JSON_THROW_ON_ERROR & $this->encodingOptions)) { |
|||
return $this->setJson($data); |
|||
} |
|||
|
|||
if (JSON_ERROR_NONE !== json_last_error()) { |
|||
throw new \InvalidArgumentException(json_last_error_msg()); |
|||
} |
|||
|
|||
return $this->setJson($data); |
|||
} |
|||
|
|||
/** |
|||
* Returns options used while encoding data to JSON. |
|||
* |
|||
* @return int |
|||
*/ |
|||
public function getEncodingOptions() |
|||
{ |
|||
return $this->encodingOptions; |
|||
} |
|||
|
|||
/** |
|||
* Sets options used while encoding data to JSON. |
|||
* |
|||
* @param int $encodingOptions |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function setEncodingOptions($encodingOptions) |
|||
{ |
|||
$this->encodingOptions = (int) $encodingOptions; |
|||
|
|||
return $this->setData(json_decode($this->data)); |
|||
} |
|||
|
|||
/** |
|||
* Updates the content and headers according to the JSON data and callback. |
|||
* |
|||
* @return $this |
|||
*/ |
|||
protected function update() |
|||
{ |
|||
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); |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
Copyright (c) 2004-2019 Fabien Potencier |
|||
|
|||
Permission is hereby granted, free of charge, to any person obtaining a copy |
|||
of this software and associated documentation files (the "Software"), to deal |
|||
in the Software without restriction, including without limitation the rights |
|||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|||
copies of the Software, and to permit persons to whom the Software is furnished |
|||
to do so, subject to the following conditions: |
|||
|
|||
The above copyright notice and this permission notice shall be included in all |
|||
copies or substantial portions of the Software. |
|||
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
|||
THE SOFTWARE. |
|||
@ -0,0 +1,234 @@ |
|||
<?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; |
|||
|
|||
/** |
|||
* ParameterBag is a container for key/value pairs. |
|||
* |
|||
* @author Fabien Potencier <fabien@symfony.com> |
|||
*/ |
|||
class ParameterBag implements \IteratorAggregate, \Countable |
|||
{ |
|||
/** |
|||
* Parameter storage. |
|||
*/ |
|||
protected $parameters; |
|||
|
|||
/** |
|||
* @param array $parameters An array of parameters |
|||
*/ |
|||
public function __construct(array $parameters = []) |
|||
{ |
|||
$this->parameters = $parameters; |
|||
} |
|||
|
|||
/** |
|||
* Returns the parameters. |
|||
* |
|||
* @return array An array of parameters |
|||
*/ |
|||
public function all() |
|||
{ |
|||
return $this->parameters; |
|||
} |
|||
|
|||
/** |
|||
* Returns the parameter keys. |
|||
* |
|||
* @return array An array of parameter keys |
|||
*/ |
|||
public function keys() |
|||
{ |
|||
return array_keys($this->parameters); |
|||
} |
|||
|
|||
/** |
|||
* Replaces the current parameters by a new set. |
|||
* |
|||
* @param array $parameters An array of parameters |
|||
*/ |
|||
public function replace(array $parameters = []) |
|||
{ |
|||
$this->parameters = $parameters; |
|||
} |
|||
|
|||
/** |
|||
* Adds parameters. |
|||
* |
|||
* @param array $parameters An array of parameters |
|||
*/ |
|||
public function add(array $parameters = []) |
|||
{ |
|||
$this->parameters = array_replace($this->parameters, $parameters); |
|||
} |
|||
|
|||
/** |
|||
* Returns a parameter by name. |
|||
* |
|||
* @param string $key The key |
|||
* @param mixed $default The default value if the parameter key does not exist |
|||
* |
|||
* @return mixed |
|||
*/ |
|||
public function get($key, $default = null) |
|||
{ |
|||
return \array_key_exists($key, $this->parameters) ? $this->parameters[$key] : $default; |
|||
} |
|||
|
|||
/** |
|||
* Sets a parameter by name. |
|||
* |
|||
* @param string $key The key |
|||
* @param mixed $value The value |
|||
*/ |
|||
public function set($key, $value) |
|||
{ |
|||
$this->parameters[$key] = $value; |
|||
} |
|||
|
|||
/** |
|||
* Returns true if the parameter is defined. |
|||
* |
|||
* @param string $key The key |
|||
* |
|||
* @return bool true if the parameter exists, false otherwise |
|||
*/ |
|||
public function has($key) |
|||
{ |
|||
return \array_key_exists($key, $this->parameters); |
|||
} |
|||
|
|||
/** |
|||
* Removes a parameter. |
|||
* |
|||
* @param string $key The key |
|||
*/ |
|||
public function remove($key) |
|||
{ |
|||
unset($this->parameters[$key]); |
|||
} |
|||
|
|||
/** |
|||
* Returns the alphabetic characters of the parameter value. |
|||
* |
|||
* @param string $key The parameter key |
|||
* @param string $default The default value if the parameter key does not exist |
|||
* |
|||
* @return string The filtered value |
|||
*/ |
|||
public function getAlpha($key, $default = '') |
|||
{ |
|||
return preg_replace('/[^[:alpha:]]/', '', $this->get($key, $default)); |
|||
} |
|||
|
|||
/** |
|||
* Returns the alphabetic characters and digits of the parameter value. |
|||
* |
|||
* @param string $key The parameter key |
|||
* @param string $default The default value if the parameter key does not exist |
|||
* |
|||
* @return string The filtered value |
|||
*/ |
|||
public function getAlnum($key, $default = '') |
|||
{ |
|||
return preg_replace('/[^[:alnum:]]/', '', $this->get($key, $default)); |
|||
} |
|||
|
|||
/** |
|||
* Returns the digits of the parameter value. |
|||
* |
|||
* @param string $key The parameter key |
|||
* @param string $default The default value if the parameter key does not exist |
|||
* |
|||
* @return string The filtered value |
|||
*/ |
|||
public function getDigits($key, $default = '') |
|||
{ |
|||
// we need to remove - and + because they're allowed in the filter |
|||
return str_replace(['-', '+'], '', $this->filter($key, $default, FILTER_SANITIZE_NUMBER_INT)); |
|||
} |
|||
|
|||
/** |
|||
* Returns the parameter value converted to integer. |
|||
* |
|||
* @param string $key The parameter key |
|||
* @param int $default The default value if the parameter key does not exist |
|||
* |
|||
* @return int The filtered value |
|||
*/ |
|||
public function getInt($key, $default = 0) |
|||
{ |
|||
return (int) $this->get($key, $default); |
|||
} |
|||
|
|||
/** |
|||
* Returns the parameter value converted to boolean. |
|||
* |
|||
* @param string $key The parameter key |
|||
* @param bool $default The default value if the parameter key does not exist |
|||
* |
|||
* @return bool The filtered value |
|||
*/ |
|||
public function getBoolean($key, $default = false) |
|||
{ |
|||
return $this->filter($key, $default, FILTER_VALIDATE_BOOLEAN); |
|||
} |
|||
|
|||
/** |
|||
* Filter key. |
|||
* |
|||
* @param string $key Key |
|||
* @param mixed $default Default = null |
|||
* @param int $filter FILTER_* constant |
|||
* @param mixed $options Filter options |
|||
* |
|||
* @see https://php.net/filter-var |
|||
* |
|||
* @return mixed |
|||
*/ |
|||
public function filter($key, $default = null, $filter = FILTER_DEFAULT, $options = []) |
|||
{ |
|||
$value = $this->get($key, $default); |
|||
|
|||
// Always turn $options into an array - this allows filter_var option shortcuts. |
|||
if (!\is_array($options) && $options) { |
|||
$options = ['flags' => $options]; |
|||
} |
|||
|
|||
// Add a convenience check for arrays. |
|||
if (\is_array($value) && !isset($options['flags'])) { |
|||
$options['flags'] = FILTER_REQUIRE_ARRAY; |
|||
} |
|||
|
|||
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); |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
HttpFoundation Component |
|||
======================== |
|||
|
|||
The HttpFoundation component defines an object-oriented layer for the HTTP |
|||
specification. |
|||
|
|||
Resources |
|||
--------- |
|||
|
|||
* [Documentation](https://symfony.com/doc/current/components/http_foundation/index.html) |
|||
* [Contributing](https://symfony.com/doc/current/contributing/index.html) |
|||
* [Report issues](https://github.com/symfony/symfony/issues) and |
|||
[send Pull Requests](https://github.com/symfony/symfony/pulls) |
|||
in the [main Symfony repository](https://github.com/symfony/symfony) |
|||
@ -0,0 +1,111 @@ |
|||
<?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; |
|||
|
|||
/** |
|||
* RedirectResponse represents an HTTP response doing a redirect. |
|||
* |
|||
* @author Fabien Potencier <fabien@symfony.com> |
|||
*/ |
|||
class RedirectResponse extends Response |
|||
{ |
|||
protected $targetUrl; |
|||
|
|||
/** |
|||
* Creates a redirect response so that it conforms to the rules defined for a redirect status code. |
|||
* |
|||
* @param string $url The URL to redirect to. The URL should be a full URL, with schema etc., |
|||
* but practically every browser redirects on paths only as well |
|||
* @param int $status The status code (302 by default) |
|||
* @param array $headers The headers (Location is always set to the given URL) |
|||
* |
|||
* @throws \InvalidArgumentException |
|||
* |
|||
* @see https://tools.ietf.org/html/rfc2616#section-10.3 |
|||
*/ |
|||
public function __construct(?string $url, int $status = 302, array $headers = []) |
|||
{ |
|||
parent::__construct('', $status, $headers); |
|||
|
|||
$this->setTargetUrl($url); |
|||
|
|||
|
|||
|
|||
if (!$this->isRedirect()) { |
|||
throw new \InvalidArgumentException(sprintf('The HTTP status code is not a redirect ("%s" given).', $status)); |
|||
} |
|||
|
|||
if (301 == $status && !\array_key_exists('cache-control', array_change_key_case($headers, \CASE_LOWER))) { |
|||
$this->headers->remove('cache-control'); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Factory method for chainability. |
|||
* |
|||
* @param string $url The url to redirect to |
|||
* @param int $status The response status code |
|||
* @param array $headers An array of response headers |
|||
* |
|||
* @return static |
|||
*/ |
|||
public static function create($url = '', $status = 302, $headers = []) |
|||
{ |
|||
return new static($url, $status, $headers); |
|||
} |
|||
|
|||
/** |
|||
* Returns the target URL. |
|||
* |
|||
* @return string target URL |
|||
*/ |
|||
public function getTargetUrl() |
|||
{ |
|||
return $this->targetUrl; |
|||
} |
|||
|
|||
/** |
|||
* Sets the redirect target of this response. |
|||
* |
|||
* @param string $url The URL to redirect to |
|||
* |
|||
* @return $this |
|||
* |
|||
* @throws \InvalidArgumentException |
|||
*/ |
|||
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 charset="UTF-8" /> |
|||
<meta http-equiv="refresh" content="0;url=%1$s" /> |
|||
|
|||
<title>Redirecting to %1$s</title> |
|||
</head> |
|||
<body> |
|||
Redirecting to <a href="%1$s">%1$s</a>. |
|||
</body> |
|||
</html>', htmlspecialchars($url, ENT_QUOTES, 'UTF-8'))); |
|||
|
|||
$this->headers->set('Location', $url); |
|||
|
|||
return $this; |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,195 @@ |
|||
<?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; |
|||
|
|||
/** |
|||
* RequestMatcher compares a pre-defined set of checks against a Request instance. |
|||
* |
|||
* @author Fabien Potencier <fabien@symfony.com> |
|||
*/ |
|||
class RequestMatcher implements RequestMatcherInterface |
|||
{ |
|||
/** |
|||
* @var string|null |
|||
*/ |
|||
private $path; |
|||
|
|||
/** |
|||
* @var string|null |
|||
*/ |
|||
private $host; |
|||
|
|||
/** |
|||
* @var int|null |
|||
*/ |
|||
private $port; |
|||
|
|||
/** |
|||
* @var string[] |
|||
*/ |
|||
private $methods = []; |
|||
|
|||
/** |
|||
* @var string[] |
|||
*/ |
|||
private $ips = []; |
|||
|
|||
/** |
|||
* @var array |
|||
*/ |
|||
private $attributes = []; |
|||
|
|||
/** |
|||
* @var string[] |
|||
*/ |
|||
private $schemes = []; |
|||
|
|||
/** |
|||
* @param string|string[]|null $methods |
|||
* @param string|string[]|null $ips |
|||
* @param string|string[]|null $schemes |
|||
*/ |
|||
public function __construct(string $path = null, string $host = null, $methods = null, $ips = null, array $attributes = [], $schemes = null, int $port = null) |
|||
{ |
|||
$this->matchPath($path); |
|||
$this->matchHost($host); |
|||
$this->matchMethod($methods); |
|||
$this->matchIps($ips); |
|||
$this->matchScheme($schemes); |
|||
$this->matchPort($port); |
|||
|
|||
foreach ($attributes as $k => $v) { |
|||
$this->matchAttribute($k, $v); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Adds a check for the HTTP scheme. |
|||
* |
|||
* @param string|string[]|null $scheme An HTTP scheme or an array of HTTP schemes |
|||
*/ |
|||
public function matchScheme($scheme) |
|||
{ |
|||
$this->schemes = null !== $scheme ? array_map('strtolower', (array) $scheme) : []; |
|||
} |
|||
|
|||
/** |
|||
* Adds a check for the URL host name. |
|||
* |
|||
* @param string|null $regexp A Regexp |
|||
*/ |
|||
public function matchHost($regexp) |
|||
{ |
|||
$this->host = $regexp; |
|||
} |
|||
|
|||
/** |
|||
* Adds a check for the the URL port. |
|||
* |
|||
* @param int|null $port The port number to connect to |
|||
*/ |
|||
public function matchPort(?int $port) |
|||
{ |
|||
$this->port = $port; |
|||
} |
|||
|
|||
/** |
|||
* Adds a check for the URL path info. |
|||
* |
|||
* @param string|null $regexp A Regexp |
|||
*/ |
|||
public function matchPath($regexp) |
|||
{ |
|||
$this->path = $regexp; |
|||
} |
|||
|
|||
/** |
|||
* Adds a check for the client IP. |
|||
* |
|||
* @param string $ip A specific IP address or a range specified using IP/netmask like 192.168.1.0/24 |
|||
*/ |
|||
public function matchIp($ip) |
|||
{ |
|||
$this->matchIps($ip); |
|||
} |
|||
|
|||
/** |
|||
* Adds a check for the client IP. |
|||
* |
|||
* @param string|string[]|null $ips A specific IP address or a range specified using IP/netmask like 192.168.1.0/24 |
|||
*/ |
|||
public function matchIps($ips) |
|||
{ |
|||
$this->ips = null !== $ips ? (array) $ips : []; |
|||
} |
|||
|
|||
/** |
|||
* Adds a check for the HTTP method. |
|||
* |
|||
* @param string|string[]|null $method An HTTP method or an array of HTTP methods |
|||
*/ |
|||
public function matchMethod($method) |
|||
{ |
|||
$this->methods = null !== $method ? array_map('strtoupper', (array) $method) : []; |
|||
} |
|||
|
|||
/** |
|||
* Adds a check for request attribute. |
|||
* |
|||
* @param string $key The request attribute name |
|||
* @param string $regexp A Regexp |
|||
*/ |
|||
public function matchAttribute($key, $regexp) |
|||
{ |
|||
$this->attributes[$key] = $regexp; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function matches(Request $request) |
|||
{ |
|||
if ($this->schemes && !\in_array($request->getScheme(), $this->schemes, true)) { |
|||
return false; |
|||
} |
|||
|
|||
if ($this->methods && !\in_array($request->getMethod(), $this->methods, true)) { |
|||
return false; |
|||
} |
|||
|
|||
foreach ($this->attributes as $key => $pattern) { |
|||
if (!preg_match('{'.$pattern.'}', $request->attributes->get($key))) { |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
if (null !== $this->path && !preg_match('{'.$this->path.'}', rawurldecode($request->getPathInfo()))) { |
|||
return false; |
|||
} |
|||
|
|||
if (null !== $this->host && !preg_match('{'.$this->host.'}i', $request->getHost())) { |
|||
return false; |
|||
} |
|||
|
|||
if (null !== $this->port && 0 < $this->port && $request->getPort() !== $this->port) { |
|||
return false; |
|||
} |
|||
|
|||
if (IpUtils::checkIp($request->getClientIp(), $this->ips)) { |
|||
return true; |
|||
} |
|||
|
|||
// Note to future implementors: add additional checks above the |
|||
// foreach above or else your check might not be run! |
|||
return 0 === \count($this->ips); |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation; |
|||
|
|||
/** |
|||
* RequestMatcherInterface is an interface for strategies to match a Request. |
|||
* |
|||
* @author Fabien Potencier <fabien@symfony.com> |
|||
*/ |
|||
interface RequestMatcherInterface |
|||
{ |
|||
/** |
|||
* Decides whether the rule(s) implemented by the strategy matches the supplied request. |
|||
* |
|||
* @return bool true if the request matches, false otherwise |
|||
*/ |
|||
public function matches(Request $request); |
|||
} |
|||
@ -0,0 +1,103 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation; |
|||
|
|||
/** |
|||
* Request stack that controls the lifecycle of requests. |
|||
* |
|||
* @author Benjamin Eberlei <kontakt@beberlei.de> |
|||
*/ |
|||
class RequestStack |
|||
{ |
|||
/** |
|||
* @var Request[] |
|||
*/ |
|||
private $requests = []; |
|||
|
|||
/** |
|||
* Pushes a Request on the stack. |
|||
* |
|||
* This method should generally not be called directly as the stack |
|||
* management should be taken care of by the application itself. |
|||
*/ |
|||
public function push(Request $request) |
|||
{ |
|||
$this->requests[] = $request; |
|||
} |
|||
|
|||
/** |
|||
* Pops the current request from the stack. |
|||
* |
|||
* This operation lets the current request go out of scope. |
|||
* |
|||
* This method should generally not be called directly as the stack |
|||
* management should be taken care of by the application itself. |
|||
* |
|||
* @return Request|null |
|||
*/ |
|||
public function pop() |
|||
{ |
|||
if (!$this->requests) { |
|||
return null; |
|||
} |
|||
|
|||
return array_pop($this->requests); |
|||
} |
|||
|
|||
/** |
|||
* @return Request|null |
|||
*/ |
|||
public function getCurrentRequest() |
|||
{ |
|||
return end($this->requests) ?: null; |
|||
} |
|||
|
|||
/** |
|||
* Gets the master Request. |
|||
* |
|||
* Be warned that making your code aware of the master request |
|||
* might make it un-compatible with other features of your framework |
|||
* like ESI support. |
|||
* |
|||
* @return Request|null |
|||
*/ |
|||
public function getMasterRequest() |
|||
{ |
|||
if (!$this->requests) { |
|||
return null; |
|||
} |
|||
|
|||
return $this->requests[0]; |
|||
} |
|||
|
|||
/** |
|||
* Returns the parent request of the current. |
|||
* |
|||
* Be warned that making your code aware of the parent request |
|||
* might make it un-compatible with other features of your framework |
|||
* like ESI support. |
|||
* |
|||
* If current Request is the master request, it returns null. |
|||
* |
|||
* @return Request|null |
|||
*/ |
|||
public function getParentRequest() |
|||
{ |
|||
$pos = \count($this->requests) - 2; |
|||
|
|||
if (!isset($this->requests[$pos])) { |
|||
return null; |
|||
} |
|||
|
|||
return $this->requests[$pos]; |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,298 @@ |
|||
<?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; |
|||
|
|||
/** |
|||
* ResponseHeaderBag is a container for Response HTTP headers. |
|||
* |
|||
* @author Fabien Potencier <fabien@symfony.com> |
|||
*/ |
|||
class ResponseHeaderBag extends HeaderBag |
|||
{ |
|||
const COOKIES_FLAT = 'flat'; |
|||
const COOKIES_ARRAY = 'array'; |
|||
|
|||
const DISPOSITION_ATTACHMENT = 'attachment'; |
|||
const DISPOSITION_INLINE = 'inline'; |
|||
|
|||
protected $computedCacheControl = []; |
|||
protected $cookies = []; |
|||
protected $headerNames = []; |
|||
|
|||
public function __construct(array $headers = []) |
|||
{ |
|||
parent::__construct($headers); |
|||
|
|||
if (!isset($this->headers['cache-control'])) { |
|||
$this->set('Cache-Control', ''); |
|||
} |
|||
|
|||
/* RFC2616 - 14.18 says all Responses need to have a Date */ |
|||
if (!isset($this->headers['date'])) { |
|||
$this->initDate(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Returns the headers, with original capitalizations. |
|||
* |
|||
* @return array An array of headers |
|||
*/ |
|||
public function allPreserveCase() |
|||
{ |
|||
$headers = []; |
|||
foreach ($this->all() as $name => $value) { |
|||
$headers[isset($this->headerNames[$name]) ? $this->headerNames[$name] : $name] = $value; |
|||
} |
|||
|
|||
return $headers; |
|||
} |
|||
|
|||
public function allPreserveCaseWithoutCookies() |
|||
{ |
|||
$headers = $this->allPreserveCase(); |
|||
if (isset($this->headerNames['set-cookie'])) { |
|||
unset($headers[$this->headerNames['set-cookie']]); |
|||
} |
|||
|
|||
return $headers; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function replace(array $headers = []) |
|||
{ |
|||
$this->headerNames = []; |
|||
|
|||
parent::replace($headers); |
|||
|
|||
if (!isset($this->headers['cache-control'])) { |
|||
$this->set('Cache-Control', ''); |
|||
} |
|||
|
|||
if (!isset($this->headers['date'])) { |
|||
$this->initDate(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function all() |
|||
{ |
|||
$headers = parent::all(); |
|||
foreach ($this->getCookies() as $cookie) { |
|||
$headers['set-cookie'][] = (string) $cookie; |
|||
} |
|||
|
|||
return $headers; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function set($key, $values, $replace = true) |
|||
{ |
|||
$uniqueKey = str_replace('_', '-', strtolower($key)); |
|||
|
|||
if ('set-cookie' === $uniqueKey) { |
|||
if ($replace) { |
|||
$this->cookies = []; |
|||
} |
|||
foreach ((array) $values as $cookie) { |
|||
$this->setCookie(Cookie::fromString($cookie)); |
|||
} |
|||
$this->headerNames[$uniqueKey] = $key; |
|||
|
|||
return; |
|||
} |
|||
|
|||
$this->headerNames[$uniqueKey] = $key; |
|||
|
|||
parent::set($key, $values, $replace); |
|||
|
|||
// ensure the cache-control header has sensible defaults |
|||
if (\in_array($uniqueKey, ['cache-control', 'etag', 'last-modified', 'expires'], true) && '' !== $computed = $this->computeCacheControlValue()) { |
|||
$this->headers['cache-control'] = [$computed]; |
|||
$this->headerNames['cache-control'] = 'Cache-Control'; |
|||
$this->computedCacheControl = $this->parseCacheControl($computed); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function remove($key) |
|||
{ |
|||
$uniqueKey = str_replace('_', '-', strtolower($key)); |
|||
unset($this->headerNames[$uniqueKey]); |
|||
|
|||
if ('set-cookie' === $uniqueKey) { |
|||
$this->cookies = []; |
|||
|
|||
return; |
|||
} |
|||
|
|||
parent::remove($key); |
|||
|
|||
if ('cache-control' === $uniqueKey) { |
|||
$this->computedCacheControl = []; |
|||
} |
|||
|
|||
if ('date' === $uniqueKey) { |
|||
$this->initDate(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function hasCacheControlDirective($key) |
|||
{ |
|||
return \array_key_exists($key, $this->computedCacheControl); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getCacheControlDirective($key) |
|||
{ |
|||
return \array_key_exists($key, $this->computedCacheControl) ? $this->computedCacheControl[$key] : null; |
|||
} |
|||
|
|||
public function setCookie(Cookie $cookie) |
|||
{ |
|||
$this->cookies[$cookie->getDomain()][$cookie->getPath()][$cookie->getName()] = $cookie; |
|||
$this->headerNames['set-cookie'] = 'Set-Cookie'; |
|||
} |
|||
|
|||
/** |
|||
* Removes a cookie from the array, but does not unset it in the browser. |
|||
* |
|||
* @param string $name |
|||
* @param string $path |
|||
* @param string $domain |
|||
*/ |
|||
public function removeCookie($name, $path = '/', $domain = null) |
|||
{ |
|||
if (null === $path) { |
|||
$path = '/'; |
|||
} |
|||
|
|||
unset($this->cookies[$domain][$path][$name]); |
|||
|
|||
if (empty($this->cookies[$domain][$path])) { |
|||
unset($this->cookies[$domain][$path]); |
|||
|
|||
if (empty($this->cookies[$domain])) { |
|||
unset($this->cookies[$domain]); |
|||
} |
|||
} |
|||
|
|||
if (empty($this->cookies)) { |
|||
unset($this->headerNames['set-cookie']); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Returns an array with all cookies. |
|||
* |
|||
* @param string $format |
|||
* |
|||
* @return Cookie[] |
|||
* |
|||
* @throws \InvalidArgumentException When the $format is invalid |
|||
*/ |
|||
public function getCookies($format = self::COOKIES_FLAT) |
|||
{ |
|||
if (!\in_array($format, [self::COOKIES_FLAT, self::COOKIES_ARRAY])) { |
|||
throw new \InvalidArgumentException(sprintf('Format "%s" invalid (%s).', $format, implode(', ', [self::COOKIES_FLAT, self::COOKIES_ARRAY]))); |
|||
} |
|||
|
|||
if (self::COOKIES_ARRAY === $format) { |
|||
return $this->cookies; |
|||
} |
|||
|
|||
$flattenedCookies = []; |
|||
foreach ($this->cookies as $path) { |
|||
foreach ($path as $cookies) { |
|||
foreach ($cookies as $cookie) { |
|||
$flattenedCookies[] = $cookie; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return $flattenedCookies; |
|||
} |
|||
|
|||
/** |
|||
* Clears a cookie in the browser. |
|||
* |
|||
* @param string $name |
|||
* @param string $path |
|||
* @param string $domain |
|||
* @param bool $secure |
|||
* @param bool $httpOnly |
|||
*/ |
|||
public function clearCookie($name, $path = '/', $domain = null, $secure = false, $httpOnly = true) |
|||
{ |
|||
$this->setCookie(new Cookie($name, null, 1, $path, $domain, $secure, $httpOnly, false, null)); |
|||
} |
|||
|
|||
/** |
|||
* @see HeaderUtils::makeDisposition() |
|||
*/ |
|||
public function makeDisposition($disposition, $filename, $filenameFallback = '') |
|||
{ |
|||
return HeaderUtils::makeDisposition((string) $disposition, (string) $filename, (string) $filenameFallback); |
|||
} |
|||
|
|||
/** |
|||
* Returns the calculated value of the cache-control header. |
|||
* |
|||
* This considers several other headers and calculates or modifies the |
|||
* cache-control header to a sensible, conservative value. |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function computeCacheControlValue() |
|||
{ |
|||
if (!$this->cacheControl && !$this->has('ETag') && !$this->has('Last-Modified') && !$this->has('Expires')) { |
|||
return 'no-cache, private'; |
|||
} |
|||
|
|||
if (!$this->cacheControl) { |
|||
// conservative by default |
|||
return 'private, must-revalidate'; |
|||
} |
|||
|
|||
$header = $this->getCacheControlHeader(); |
|||
if (isset($this->cacheControl['public']) || isset($this->cacheControl['private'])) { |
|||
return $header; |
|||
} |
|||
|
|||
// public if s-maxage is defined, private otherwise |
|||
if (!isset($this->cacheControl['s-maxage'])) { |
|||
return $header.', private'; |
|||
} |
|||
|
|||
return $header; |
|||
} |
|||
|
|||
private function initDate() |
|||
{ |
|||
$now = \DateTime::createFromFormat('U', time()); |
|||
$now->setTimezone(new \DateTimeZone('UTC')); |
|||
$this->set('Date', $now->format('D, d M Y H:i:s').' GMT'); |
|||
} |
|||
} |
|||
@ -0,0 +1,102 @@ |
|||
<?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; |
|||
|
|||
/** |
|||
* ServerBag is a container for HTTP headers from the $_SERVER variable. |
|||
* |
|||
* @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 array |
|||
*/ |
|||
public function getHeaders() |
|||
{ |
|||
$headers = []; |
|||
$contentHeaders = ['CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true]; |
|||
foreach ($this->parameters as $key => $value) { |
|||
if (0 === strpos($key, 'HTTP_')) { |
|||
$headers[substr($key, 5)] = $value; |
|||
} |
|||
// CONTENT_* are not prefixed with HTTP_ |
|||
elseif (isset($contentHeaders[$key])) { |
|||
$headers[$key] = $value; |
|||
} |
|||
} |
|||
|
|||
if (isset($this->parameters['PHP_AUTH_USER'])) { |
|||
$headers['PHP_AUTH_USER'] = $this->parameters['PHP_AUTH_USER']; |
|||
$headers['PHP_AUTH_PW'] = isset($this->parameters['PHP_AUTH_PW']) ? $this->parameters['PHP_AUTH_PW'] : ''; |
|||
} 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']; |
|||
} |
|||
|
|||
if (null !== $authorizationHeader) { |
|||
if (0 === stripos($authorizationHeader, 'basic ')) { |
|||
// Decode AUTHORIZATION header into PHP_AUTH_USER and PHP_AUTH_PW when authorization header is basic |
|||
$exploded = explode(':', base64_decode(substr($authorizationHeader, 6)), 2); |
|||
if (2 == \count($exploded)) { |
|||
list($headers['PHP_AUTH_USER'], $headers['PHP_AUTH_PW']) = $exploded; |
|||
} |
|||
} elseif (empty($this->parameters['PHP_AUTH_DIGEST']) && (0 === stripos($authorizationHeader, 'digest '))) { |
|||
// In some circumstances PHP_AUTH_DIGEST needs to be set |
|||
$headers['PHP_AUTH_DIGEST'] = $authorizationHeader; |
|||
$this->parameters['PHP_AUTH_DIGEST'] = $authorizationHeader; |
|||
} elseif (0 === stripos($authorizationHeader, 'bearer ')) { |
|||
/* |
|||
* XXX: Since there is no PHP_AUTH_BEARER in PHP predefined variables, |
|||
* I'll just set $headers['AUTHORIZATION'] here. |
|||
* https://php.net/reserved.variables.server |
|||
*/ |
|||
$headers['AUTHORIZATION'] = $authorizationHeader; |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (isset($headers['AUTHORIZATION'])) { |
|||
return $headers; |
|||
} |
|||
|
|||
// 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']); |
|||
} elseif (isset($headers['PHP_AUTH_DIGEST'])) { |
|||
$headers['AUTHORIZATION'] = $headers['PHP_AUTH_DIGEST']; |
|||
} |
|||
|
|||
return $headers; |
|||
} |
|||
} |
|||
@ -0,0 +1,148 @@ |
|||
<?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\Attribute; |
|||
|
|||
/** |
|||
* This class relates to session attribute storage. |
|||
*/ |
|||
class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Countable |
|||
{ |
|||
private $name = 'attributes'; |
|||
private $storageKey; |
|||
|
|||
protected $attributes = []; |
|||
|
|||
/** |
|||
* @param string $storageKey The key used to store attributes in the session |
|||
*/ |
|||
public function __construct(string $storageKey = '_sf2_attributes') |
|||
{ |
|||
$this->storageKey = $storageKey; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getName() |
|||
{ |
|||
return $this->name; |
|||
} |
|||
|
|||
public function setName($name) |
|||
{ |
|||
$this->name = $name; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function initialize(array &$attributes) |
|||
{ |
|||
$this->attributes = &$attributes; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getStorageKey() |
|||
{ |
|||
return $this->storageKey; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function has($name) |
|||
{ |
|||
return \array_key_exists($name, $this->attributes); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function get($name, $default = null) |
|||
{ |
|||
return \array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function set($name, $value) |
|||
{ |
|||
$this->attributes[$name] = $value; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function all() |
|||
{ |
|||
return $this->attributes; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function replace(array $attributes) |
|||
{ |
|||
$this->attributes = []; |
|||
foreach ($attributes as $key => $value) { |
|||
$this->set($key, $value); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function remove($name) |
|||
{ |
|||
$retval = null; |
|||
if (\array_key_exists($name, $this->attributes)) { |
|||
$retval = $this->attributes[$name]; |
|||
unset($this->attributes[$name]); |
|||
} |
|||
|
|||
return $retval; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function clear() |
|||
{ |
|||
$return = $this->attributes; |
|||
$this->attributes = []; |
|||
|
|||
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); |
|||
} |
|||
} |
|||
@ -0,0 +1,72 @@ |
|||
<?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\Attribute; |
|||
|
|||
use Symfony\Component\HttpFoundation\Session\SessionBagInterface; |
|||
|
|||
/** |
|||
* Attributes store. |
|||
* |
|||
* @author Drak <drak@zikula.org> |
|||
*/ |
|||
interface AttributeBagInterface extends SessionBagInterface |
|||
{ |
|||
/** |
|||
* Checks if an attribute is defined. |
|||
* |
|||
* @param string $name The attribute name |
|||
* |
|||
* @return bool true if the attribute is defined, false otherwise |
|||
*/ |
|||
public function has($name); |
|||
|
|||
/** |
|||
* Returns an attribute. |
|||
* |
|||
* @param string $name The attribute name |
|||
* @param mixed $default The default value if not found |
|||
* |
|||
* @return mixed |
|||
*/ |
|||
public function get($name, $default = null); |
|||
|
|||
/** |
|||
* Sets an attribute. |
|||
* |
|||
* @param string $name |
|||
* @param mixed $value |
|||
*/ |
|||
public function set($name, $value); |
|||
|
|||
/** |
|||
* Returns attributes. |
|||
* |
|||
* @return array Attributes |
|||
*/ |
|||
public function all(); |
|||
|
|||
/** |
|||
* Sets attributes. |
|||
* |
|||
* @param array $attributes Attributes |
|||
*/ |
|||
public function replace(array $attributes); |
|||
|
|||
/** |
|||
* Removes an attribute. |
|||
* |
|||
* @param string $name |
|||
* |
|||
* @return mixed The removed value or null when it does not exist |
|||
*/ |
|||
public function remove($name); |
|||
} |
|||
@ -0,0 +1,159 @@ |
|||
<?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\Attribute; |
|||
|
|||
/** |
|||
* This class provides structured storage of session attributes using |
|||
* a name spacing character in the key. |
|||
* |
|||
* @author Drak <drak@zikula.org> |
|||
*/ |
|||
class NamespacedAttributeBag extends AttributeBag |
|||
{ |
|||
private $namespaceCharacter; |
|||
|
|||
/** |
|||
* @param string $storageKey Session storage key |
|||
* @param string $namespaceCharacter Namespace character to use in keys |
|||
*/ |
|||
public function __construct(string $storageKey = '_sf2_attributes', string $namespaceCharacter = '/') |
|||
{ |
|||
$this->namespaceCharacter = $namespaceCharacter; |
|||
parent::__construct($storageKey); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function has($name) |
|||
{ |
|||
// reference mismatch: if fixed, re-introduced in array_key_exists; keep as it is |
|||
$attributes = $this->resolveAttributePath($name); |
|||
$name = $this->resolveKey($name); |
|||
|
|||
if (null === $attributes) { |
|||
return false; |
|||
} |
|||
|
|||
return \array_key_exists($name, $attributes); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function get($name, $default = null) |
|||
{ |
|||
// reference mismatch: if fixed, re-introduced in array_key_exists; keep as it is |
|||
$attributes = $this->resolveAttributePath($name); |
|||
$name = $this->resolveKey($name); |
|||
|
|||
if (null === $attributes) { |
|||
return $default; |
|||
} |
|||
|
|||
return \array_key_exists($name, $attributes) ? $attributes[$name] : $default; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function set($name, $value) |
|||
{ |
|||
$attributes = &$this->resolveAttributePath($name, true); |
|||
$name = $this->resolveKey($name); |
|||
$attributes[$name] = $value; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function remove($name) |
|||
{ |
|||
$retval = null; |
|||
$attributes = &$this->resolveAttributePath($name); |
|||
$name = $this->resolveKey($name); |
|||
if (null !== $attributes && \array_key_exists($name, $attributes)) { |
|||
$retval = $attributes[$name]; |
|||
unset($attributes[$name]); |
|||
} |
|||
|
|||
return $retval; |
|||
} |
|||
|
|||
/** |
|||
* Resolves a path in attributes property and returns it as a reference. |
|||
* |
|||
* This method allows structured namespacing of session attributes. |
|||
* |
|||
* @param string $name Key name |
|||
* @param bool $writeContext Write context, default false |
|||
* |
|||
* @return array|null |
|||
*/ |
|||
protected function &resolveAttributePath($name, $writeContext = false) |
|||
{ |
|||
$array = &$this->attributes; |
|||
$name = (0 === strpos($name, $this->namespaceCharacter)) ? substr($name, 1) : $name; |
|||
|
|||
// Check if there is anything to do, else return |
|||
if (!$name) { |
|||
return $array; |
|||
} |
|||
|
|||
$parts = explode($this->namespaceCharacter, $name); |
|||
if (\count($parts) < 2) { |
|||
if (!$writeContext) { |
|||
return $array; |
|||
} |
|||
|
|||
$array[$parts[0]] = []; |
|||
|
|||
return $array; |
|||
} |
|||
|
|||
unset($parts[\count($parts) - 1]); |
|||
|
|||
foreach ($parts as $part) { |
|||
if (null !== $array && !\array_key_exists($part, $array)) { |
|||
if (!$writeContext) { |
|||
$null = null; |
|||
|
|||
return $null; |
|||
} |
|||
|
|||
$array[$part] = []; |
|||
} |
|||
|
|||
$array = &$array[$part]; |
|||
} |
|||
|
|||
return $array; |
|||
} |
|||
|
|||
/** |
|||
* Resolves the key from the name. |
|||
* |
|||
* This is the last part in a dot separated string. |
|||
* |
|||
* @param string $name |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function resolveKey($name) |
|||
{ |
|||
if (false !== $pos = strrpos($name, $this->namespaceCharacter)) { |
|||
$name = substr($name, $pos + 1); |
|||
} |
|||
|
|||
return $name; |
|||
} |
|||
} |
|||
@ -0,0 +1,161 @@ |
|||
<?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\Flash; |
|||
|
|||
/** |
|||
* AutoExpireFlashBag flash message container. |
|||
* |
|||
* @author Drak <drak@zikula.org> |
|||
*/ |
|||
class AutoExpireFlashBag implements FlashBagInterface |
|||
{ |
|||
private $name = 'flashes'; |
|||
private $flashes = ['display' => [], 'new' => []]; |
|||
private $storageKey; |
|||
|
|||
/** |
|||
* @param string $storageKey The key used to store flashes in the session |
|||
*/ |
|||
public function __construct(string $storageKey = '_symfony_flashes') |
|||
{ |
|||
$this->storageKey = $storageKey; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getName() |
|||
{ |
|||
return $this->name; |
|||
} |
|||
|
|||
public function setName($name) |
|||
{ |
|||
$this->name = $name; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function initialize(array &$flashes) |
|||
{ |
|||
$this->flashes = &$flashes; |
|||
|
|||
// The logic: messages from the last request will be stored in new, so we move them to previous |
|||
// This request we will show what is in 'display'. What is placed into 'new' this time round will |
|||
// be moved to display next time round. |
|||
$this->flashes['display'] = \array_key_exists('new', $this->flashes) ? $this->flashes['new'] : []; |
|||
$this->flashes['new'] = []; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function add($type, $message) |
|||
{ |
|||
$this->flashes['new'][$type][] = $message; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function peek($type, array $default = []) |
|||
{ |
|||
return $this->has($type) ? $this->flashes['display'][$type] : $default; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function peekAll() |
|||
{ |
|||
return \array_key_exists('display', $this->flashes) ? (array) $this->flashes['display'] : []; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function get($type, array $default = []) |
|||
{ |
|||
$return = $default; |
|||
|
|||
if (!$this->has($type)) { |
|||
return $return; |
|||
} |
|||
|
|||
if (isset($this->flashes['display'][$type])) { |
|||
$return = $this->flashes['display'][$type]; |
|||
unset($this->flashes['display'][$type]); |
|||
} |
|||
|
|||
return $return; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function all() |
|||
{ |
|||
$return = $this->flashes['display']; |
|||
$this->flashes['display'] = []; |
|||
|
|||
return $return; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function setAll(array $messages) |
|||
{ |
|||
$this->flashes['new'] = $messages; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function set($type, $messages) |
|||
{ |
|||
$this->flashes['new'][$type] = (array) $messages; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function has($type) |
|||
{ |
|||
return \array_key_exists($type, $this->flashes['display']) && $this->flashes['display'][$type]; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function keys() |
|||
{ |
|||
return array_keys($this->flashes['display']); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getStorageKey() |
|||
{ |
|||
return $this->storageKey; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function clear() |
|||
{ |
|||
return $this->all(); |
|||
} |
|||
} |
|||
@ -0,0 +1,152 @@ |
|||
<?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\Flash; |
|||
|
|||
/** |
|||
* FlashBag flash message container. |
|||
* |
|||
* @author Drak <drak@zikula.org> |
|||
*/ |
|||
class FlashBag implements FlashBagInterface |
|||
{ |
|||
private $name = 'flashes'; |
|||
private $flashes = []; |
|||
private $storageKey; |
|||
|
|||
/** |
|||
* @param string $storageKey The key used to store flashes in the session |
|||
*/ |
|||
public function __construct(string $storageKey = '_symfony_flashes') |
|||
{ |
|||
$this->storageKey = $storageKey; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getName() |
|||
{ |
|||
return $this->name; |
|||
} |
|||
|
|||
public function setName($name) |
|||
{ |
|||
$this->name = $name; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function initialize(array &$flashes) |
|||
{ |
|||
$this->flashes = &$flashes; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function add($type, $message) |
|||
{ |
|||
$this->flashes[$type][] = $message; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function peek($type, array $default = []) |
|||
{ |
|||
return $this->has($type) ? $this->flashes[$type] : $default; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function peekAll() |
|||
{ |
|||
return $this->flashes; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function get($type, array $default = []) |
|||
{ |
|||
if (!$this->has($type)) { |
|||
return $default; |
|||
} |
|||
|
|||
$return = $this->flashes[$type]; |
|||
|
|||
unset($this->flashes[$type]); |
|||
|
|||
return $return; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function all() |
|||
{ |
|||
$return = $this->peekAll(); |
|||
$this->flashes = []; |
|||
|
|||
return $return; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function set($type, $messages) |
|||
{ |
|||
$this->flashes[$type] = (array) $messages; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function setAll(array $messages) |
|||
{ |
|||
$this->flashes = $messages; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function has($type) |
|||
{ |
|||
return \array_key_exists($type, $this->flashes) && $this->flashes[$type]; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function keys() |
|||
{ |
|||
return array_keys($this->flashes); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getStorageKey() |
|||
{ |
|||
return $this->storageKey; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function clear() |
|||
{ |
|||
return $this->all(); |
|||
} |
|||
} |
|||
@ -0,0 +1,93 @@ |
|||
<?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\Flash; |
|||
|
|||
use Symfony\Component\HttpFoundation\Session\SessionBagInterface; |
|||
|
|||
/** |
|||
* FlashBagInterface. |
|||
* |
|||
* @author Drak <drak@zikula.org> |
|||
*/ |
|||
interface FlashBagInterface extends SessionBagInterface |
|||
{ |
|||
/** |
|||
* Adds a flash message for the given type. |
|||
* |
|||
* @param string $type |
|||
* @param mixed $message |
|||
*/ |
|||
public function add($type, $message); |
|||
|
|||
/** |
|||
* Registers one or more messages for a given type. |
|||
* |
|||
* @param string $type |
|||
* @param string|array $messages |
|||
*/ |
|||
public function set($type, $messages); |
|||
|
|||
/** |
|||
* Gets flash messages for a given type. |
|||
* |
|||
* @param string $type Message category type |
|||
* @param array $default Default value if $type does not exist |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function peek($type, array $default = []); |
|||
|
|||
/** |
|||
* Gets all flash messages. |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function peekAll(); |
|||
|
|||
/** |
|||
* Gets and clears flash from the stack. |
|||
* |
|||
* @param string $type |
|||
* @param array $default Default value if $type does not exist |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function get($type, array $default = []); |
|||
|
|||
/** |
|||
* Gets and clears flashes from the stack. |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function all(); |
|||
|
|||
/** |
|||
* Sets all flash messages. |
|||
*/ |
|||
public function setAll(array $messages); |
|||
|
|||
/** |
|||
* Has flash messages for a given type? |
|||
* |
|||
* @param string $type |
|||
* |
|||
* @return bool |
|||
*/ |
|||
public function has($type); |
|||
|
|||
/** |
|||
* Returns a list of all defined types. |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function keys(); |
|||
} |
|||
@ -0,0 +1,282 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\Session; |
|||
|
|||
use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag; |
|||
use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface; |
|||
use Symfony\Component\HttpFoundation\Session\Flash\FlashBag; |
|||
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface; |
|||
use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; |
|||
use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface; |
|||
|
|||
/** |
|||
* @author Fabien Potencier <fabien@symfony.com> |
|||
* @author Drak <drak@zikula.org> |
|||
*/ |
|||
class Session implements SessionInterface, \IteratorAggregate, \Countable |
|||
{ |
|||
protected $storage; |
|||
|
|||
private $flashName; |
|||
private $attributeName; |
|||
private $data = []; |
|||
private $usageIndex = 0; |
|||
|
|||
/** |
|||
* @param SessionStorageInterface $storage A SessionStorageInterface instance |
|||
* @param AttributeBagInterface $attributes An AttributeBagInterface instance, (defaults null for default AttributeBag) |
|||
* @param FlashBagInterface $flashes A FlashBagInterface instance (defaults null for default FlashBag) |
|||
*/ |
|||
public function __construct(SessionStorageInterface $storage = null, AttributeBagInterface $attributes = null, FlashBagInterface $flashes = null) |
|||
{ |
|||
$this->storage = $storage ?: new NativeSessionStorage(); |
|||
|
|||
$attributes = $attributes ?: new AttributeBag(); |
|||
$this->attributeName = $attributes->getName(); |
|||
$this->registerBag($attributes); |
|||
|
|||
$flashes = $flashes ?: new FlashBag(); |
|||
$this->flashName = $flashes->getName(); |
|||
$this->registerBag($flashes); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function start() |
|||
{ |
|||
return $this->storage->start(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function has($name) |
|||
{ |
|||
return $this->getAttributeBag()->has($name); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function get($name, $default = null) |
|||
{ |
|||
return $this->getAttributeBag()->get($name, $default); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function set($name, $value) |
|||
{ |
|||
$this->getAttributeBag()->set($name, $value); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function all() |
|||
{ |
|||
return $this->getAttributeBag()->all(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function replace(array $attributes) |
|||
{ |
|||
$this->getAttributeBag()->replace($attributes); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function remove($name) |
|||
{ |
|||
return $this->getAttributeBag()->remove($name); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function clear() |
|||
{ |
|||
$this->getAttributeBag()->clear(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function isStarted() |
|||
{ |
|||
return $this->storage->isStarted(); |
|||
} |
|||
|
|||
/** |
|||
* Returns an iterator for attributes. |
|||
* |
|||
* @return \ArrayIterator An \ArrayIterator instance |
|||
*/ |
|||
public function getIterator() |
|||
{ |
|||
return new \ArrayIterator($this->getAttributeBag()->all()); |
|||
} |
|||
|
|||
/** |
|||
* Returns the number of attributes. |
|||
* |
|||
* @return int The number of attributes |
|||
*/ |
|||
public function count() |
|||
{ |
|||
return \count($this->getAttributeBag()->all()); |
|||
} |
|||
|
|||
/** |
|||
* @return int |
|||
* |
|||
* @internal |
|||
*/ |
|||
public function getUsageIndex() |
|||
{ |
|||
return $this->usageIndex; |
|||
} |
|||
|
|||
/** |
|||
* @return bool |
|||
* |
|||
* @internal |
|||
*/ |
|||
public function isEmpty() |
|||
{ |
|||
if ($this->isStarted()) { |
|||
++$this->usageIndex; |
|||
} |
|||
foreach ($this->data as &$data) { |
|||
if (!empty($data)) { |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function invalidate($lifetime = null) |
|||
{ |
|||
$this->storage->clear(); |
|||
|
|||
return $this->migrate(true, $lifetime); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function migrate($destroy = false, $lifetime = null) |
|||
{ |
|||
return $this->storage->regenerate($destroy, $lifetime); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function save() |
|||
{ |
|||
$this->storage->save(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getId() |
|||
{ |
|||
return $this->storage->getId(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function setId($id) |
|||
{ |
|||
if ($this->storage->getId() !== $id) { |
|||
$this->storage->setId($id); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getName() |
|||
{ |
|||
return $this->storage->getName(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function setName($name) |
|||
{ |
|||
$this->storage->setName($name); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getMetadataBag() |
|||
{ |
|||
++$this->usageIndex; |
|||
|
|||
return $this->storage->getMetadataBag(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function registerBag(SessionBagInterface $bag) |
|||
{ |
|||
$this->storage->registerBag(new SessionBagProxy($bag, $this->data, $this->usageIndex)); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getBag($name) |
|||
{ |
|||
$bag = $this->storage->getBag($name); |
|||
|
|||
return method_exists($bag, 'getBag') ? $bag->getBag() : $bag; |
|||
} |
|||
|
|||
/** |
|||
* Gets the flashbag interface. |
|||
* |
|||
* @return FlashBagInterface |
|||
*/ |
|||
public function getFlashBag() |
|||
{ |
|||
return $this->getBag($this->flashName); |
|||
} |
|||
|
|||
/** |
|||
* Gets the attributebag interface. |
|||
* |
|||
* Note that this method was added to help with IDE autocompletion. |
|||
* |
|||
* @return AttributeBagInterface |
|||
*/ |
|||
private function getAttributeBag() |
|||
{ |
|||
return $this->getBag($this->attributeName); |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
<?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; |
|||
|
|||
/** |
|||
* Session Bag store. |
|||
* |
|||
* @author Drak <drak@zikula.org> |
|||
*/ |
|||
interface SessionBagInterface |
|||
{ |
|||
/** |
|||
* Gets this bag's name. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getName(); |
|||
|
|||
/** |
|||
* Initializes the Bag. |
|||
*/ |
|||
public function initialize(array &$array); |
|||
|
|||
/** |
|||
* Gets the storage key for this bag. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getStorageKey(); |
|||
|
|||
/** |
|||
* Clears out data from bag. |
|||
* |
|||
* @return mixed Whatever data was contained |
|||
*/ |
|||
public function clear(); |
|||
} |
|||
@ -0,0 +1,89 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\Session; |
|||
|
|||
/** |
|||
* @author Nicolas Grekas <p@tchwork.com> |
|||
* |
|||
* @internal |
|||
*/ |
|||
final class SessionBagProxy implements SessionBagInterface |
|||
{ |
|||
private $bag; |
|||
private $data; |
|||
private $usageIndex; |
|||
|
|||
public function __construct(SessionBagInterface $bag, array &$data, &$usageIndex) |
|||
{ |
|||
$this->bag = $bag; |
|||
$this->data = &$data; |
|||
$this->usageIndex = &$usageIndex; |
|||
} |
|||
|
|||
/** |
|||
* @return SessionBagInterface |
|||
*/ |
|||
public function getBag() |
|||
{ |
|||
++$this->usageIndex; |
|||
|
|||
return $this->bag; |
|||
} |
|||
|
|||
/** |
|||
* @return bool |
|||
*/ |
|||
public function isEmpty() |
|||
{ |
|||
if (!isset($this->data[$this->bag->getStorageKey()])) { |
|||
return true; |
|||
} |
|||
++$this->usageIndex; |
|||
|
|||
return empty($this->data[$this->bag->getStorageKey()]); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getName() |
|||
{ |
|||
return $this->bag->getName(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function initialize(array &$array) |
|||
{ |
|||
++$this->usageIndex; |
|||
$this->data[$this->bag->getStorageKey()] = &$array; |
|||
|
|||
$this->bag->initialize($array); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getStorageKey() |
|||
{ |
|||
return $this->bag->getStorageKey(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function clear() |
|||
{ |
|||
return $this->bag->clear(); |
|||
} |
|||
} |
|||
@ -0,0 +1,180 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\Session; |
|||
|
|||
use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag; |
|||
|
|||
/** |
|||
* Interface for the session. |
|||
* |
|||
* @author Drak <drak@zikula.org> |
|||
*/ |
|||
interface SessionInterface |
|||
{ |
|||
/** |
|||
* Starts the session storage. |
|||
* |
|||
* @return bool True if session started |
|||
* |
|||
* @throws \RuntimeException if session fails to start |
|||
*/ |
|||
public function start(); |
|||
|
|||
/** |
|||
* Returns the session ID. |
|||
* |
|||
* @return string The session ID |
|||
*/ |
|||
public function getId(); |
|||
|
|||
/** |
|||
* Sets the session ID. |
|||
* |
|||
* @param string $id |
|||
*/ |
|||
public function setId($id); |
|||
|
|||
/** |
|||
* Returns the session name. |
|||
* |
|||
* @return mixed The session name |
|||
*/ |
|||
public function getName(); |
|||
|
|||
/** |
|||
* Sets the session name. |
|||
* |
|||
* @param string $name |
|||
*/ |
|||
public function setName($name); |
|||
|
|||
/** |
|||
* Invalidates the current session. |
|||
* |
|||
* Clears all session attributes and flashes and regenerates the |
|||
* session and deletes the old session from persistence. |
|||
* |
|||
* @param int $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 bool True if session invalidated, false if error |
|||
*/ |
|||
public function invalidate($lifetime = null); |
|||
|
|||
/** |
|||
* Migrates the current session to a new session id while maintaining all |
|||
* session attributes. |
|||
* |
|||
* @param bool $destroy Whether to delete the old session or leave it to garbage collection |
|||
* @param int $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 bool True if session migrated, false if error |
|||
*/ |
|||
public function migrate($destroy = false, $lifetime = null); |
|||
|
|||
/** |
|||
* Force the session to be saved and closed. |
|||
* |
|||
* This method is generally not required for real sessions as |
|||
* the session will be automatically saved at the end of |
|||
* code execution. |
|||
*/ |
|||
public function save(); |
|||
|
|||
/** |
|||
* Checks if an attribute is defined. |
|||
* |
|||
* @param string $name The attribute name |
|||
* |
|||
* @return bool true if the attribute is defined, false otherwise |
|||
*/ |
|||
public function has($name); |
|||
|
|||
/** |
|||
* Returns an attribute. |
|||
* |
|||
* @param string $name The attribute name |
|||
* @param mixed $default The default value if not found |
|||
* |
|||
* @return mixed |
|||
*/ |
|||
public function get($name, $default = null); |
|||
|
|||
/** |
|||
* Sets an attribute. |
|||
* |
|||
* @param string $name |
|||
* @param mixed $value |
|||
*/ |
|||
public function set($name, $value); |
|||
|
|||
/** |
|||
* Returns attributes. |
|||
* |
|||
* @return array Attributes |
|||
*/ |
|||
public function all(); |
|||
|
|||
/** |
|||
* Sets attributes. |
|||
* |
|||
* @param array $attributes Attributes |
|||
*/ |
|||
public function replace(array $attributes); |
|||
|
|||
/** |
|||
* Removes an attribute. |
|||
* |
|||
* @param string $name |
|||
* |
|||
* @return mixed The removed value or null when it does not exist |
|||
*/ |
|||
public function remove($name); |
|||
|
|||
/** |
|||
* Clears all attributes. |
|||
*/ |
|||
public function clear(); |
|||
|
|||
/** |
|||
* Checks if the session was started. |
|||
* |
|||
* @return bool |
|||
*/ |
|||
public function isStarted(); |
|||
|
|||
/** |
|||
* Registers a SessionBagInterface with the session. |
|||
*/ |
|||
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(); |
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
<?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; |
|||
|
|||
/** |
|||
* Session utility functions. |
|||
* |
|||
* @author Nicolas Grekas <p@tchwork.com> |
|||
* @author Rémon van de Kamp <rpkamp@gmail.com> |
|||
* |
|||
* @internal |
|||
*/ |
|||
final class SessionUtils |
|||
{ |
|||
/** |
|||
* Finds the session header amongst the headers that are to be sent, removes it, and returns |
|||
* it so the caller can process it further. |
|||
*/ |
|||
public static function popSessionCookie(string $sessionName, string $sessionId): ?string |
|||
{ |
|||
$sessionCookie = null; |
|||
$sessionCookiePrefix = sprintf(' %s=', urlencode($sessionName)); |
|||
$sessionCookieWithId = sprintf('%s%s;', $sessionCookiePrefix, urlencode($sessionId)); |
|||
$otherCookies = []; |
|||
foreach (headers_list() as $h) { |
|||
if (0 !== stripos($h, 'Set-Cookie:')) { |
|||
continue; |
|||
} |
|||
if (11 === strpos($h, $sessionCookiePrefix, 11)) { |
|||
$sessionCookie = $h; |
|||
|
|||
if (11 !== strpos($h, $sessionCookieWithId, 11)) { |
|||
$otherCookies[] = $h; |
|||
} |
|||
} else { |
|||
$otherCookies[] = $h; |
|||
} |
|||
} |
|||
if (null === $sessionCookie) { |
|||
return null; |
|||
} |
|||
|
|||
header_remove('Set-Cookie'); |
|||
foreach ($otherCookies as $h) { |
|||
header($h, false); |
|||
} |
|||
|
|||
return $sessionCookie; |
|||
} |
|||
} |
|||
@ -0,0 +1,148 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; |
|||
|
|||
use Symfony\Component\HttpFoundation\Session\SessionUtils; |
|||
|
|||
/** |
|||
* This abstract session handler provides a generic implementation |
|||
* of the PHP 7.0 SessionUpdateTimestampHandlerInterface, |
|||
* enabling strict and lazy session handling. |
|||
* |
|||
* @author Nicolas Grekas <p@tchwork.com> |
|||
*/ |
|||
abstract class AbstractSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface |
|||
{ |
|||
private $sessionName; |
|||
private $prefetchId; |
|||
private $prefetchData; |
|||
private $newSessionId; |
|||
private $igbinaryEmptyData; |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function open($savePath, $sessionName) |
|||
{ |
|||
$this->sessionName = $sessionName; |
|||
if (!headers_sent() && !ini_get('session.cache_limiter') && '0' !== ini_get('session.cache_limiter')) { |
|||
header(sprintf('Cache-Control: max-age=%d, private, must-revalidate', 60 * (int) ini_get('session.cache_expire'))); |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* @param string $sessionId |
|||
* |
|||
* @return string |
|||
*/ |
|||
abstract protected function doRead($sessionId); |
|||
|
|||
/** |
|||
* @param string $sessionId |
|||
* @param string $data |
|||
* |
|||
* @return bool |
|||
*/ |
|||
abstract protected function doWrite($sessionId, $data); |
|||
|
|||
/** |
|||
* @param string $sessionId |
|||
* |
|||
* @return bool |
|||
*/ |
|||
abstract protected function doDestroy($sessionId); |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function validateId($sessionId) |
|||
{ |
|||
$this->prefetchData = $this->read($sessionId); |
|||
$this->prefetchId = $sessionId; |
|||
|
|||
return '' !== $this->prefetchData; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function read($sessionId) |
|||
{ |
|||
if (null !== $this->prefetchId) { |
|||
$prefetchId = $this->prefetchId; |
|||
$prefetchData = $this->prefetchData; |
|||
$this->prefetchId = $this->prefetchData = null; |
|||
|
|||
if ($prefetchId === $sessionId || '' === $prefetchData) { |
|||
$this->newSessionId = '' === $prefetchData ? $sessionId : null; |
|||
|
|||
return $prefetchData; |
|||
} |
|||
} |
|||
|
|||
$data = $this->doRead($sessionId); |
|||
$this->newSessionId = '' === $data ? $sessionId : null; |
|||
|
|||
return $data; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function write($sessionId, $data) |
|||
{ |
|||
if (null === $this->igbinaryEmptyData) { |
|||
// see https://github.com/igbinary/igbinary/issues/146 |
|||
$this->igbinaryEmptyData = \function_exists('igbinary_serialize') ? igbinary_serialize([]) : ''; |
|||
} |
|||
if ('' === $data || $this->igbinaryEmptyData === $data) { |
|||
return $this->destroy($sessionId); |
|||
} |
|||
$this->newSessionId = null; |
|||
|
|||
return $this->doWrite($sessionId, $data); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function destroy($sessionId) |
|||
{ |
|||
if (!headers_sent() && filter_var(ini_get('session.use_cookies'), FILTER_VALIDATE_BOOLEAN)) { |
|||
if (!$this->sessionName) { |
|||
throw new \LogicException(sprintf('Session name cannot be empty, did you forget to call "parent::open()" in "%s"?.', \get_class($this))); |
|||
} |
|||
$cookie = SessionUtils::popSessionCookie($this->sessionName, $sessionId); |
|||
|
|||
/* |
|||
* We send an invalidation Set-Cookie header (zero lifetime) |
|||
* when either the session was started or a cookie with |
|||
* the session name was sent by the client (in which case |
|||
* we know it's invalid as a valid session cookie would've |
|||
* started the session). |
|||
*/ |
|||
if (null === $cookie || isset($_COOKIE[$this->sessionName])) { |
|||
if (\PHP_VERSION_ID < 70300) { |
|||
setcookie($this->sessionName, '', 0, ini_get('session.cookie_path'), ini_get('session.cookie_domain'), filter_var(ini_get('session.cookie_secure'), FILTER_VALIDATE_BOOLEAN), filter_var(ini_get('session.cookie_httponly'), FILTER_VALIDATE_BOOLEAN)); |
|||
} else { |
|||
$params = session_get_cookie_params(); |
|||
unset($params['lifetime']); |
|||
setcookie($this->sessionName, '', $params); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return $this->newSessionId === $sessionId || $this->doDestroy($sessionId); |
|||
} |
|||
} |
|||
@ -0,0 +1,119 @@ |
|||
<?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; |
|||
|
|||
/** |
|||
* Memcached based session storage handler based on the Memcached class |
|||
* provided by the PHP memcached extension. |
|||
* |
|||
* @see https://php.net/memcached |
|||
* |
|||
* @author Drak <drak@zikula.org> |
|||
*/ |
|||
class MemcachedSessionHandler extends AbstractSessionHandler |
|||
{ |
|||
private $memcached; |
|||
|
|||
/** |
|||
* @var int Time to live in seconds |
|||
*/ |
|||
private $ttl; |
|||
|
|||
/** |
|||
* @var string Key prefix for shared environments |
|||
*/ |
|||
private $prefix; |
|||
|
|||
/** |
|||
* Constructor. |
|||
* |
|||
* 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. |
|||
* |
|||
* @throws \InvalidArgumentException When unsupported options are passed |
|||
*/ |
|||
public function __construct(\Memcached $memcached, array $options = []) |
|||
{ |
|||
$this->memcached = $memcached; |
|||
|
|||
if ($diff = array_diff(array_keys($options), ['prefix', 'expiretime'])) { |
|||
throw new \InvalidArgumentException(sprintf('The following options are not supported "%s"', implode(', ', $diff))); |
|||
} |
|||
|
|||
$this->ttl = isset($options['expiretime']) ? (int) $options['expiretime'] : 86400; |
|||
$this->prefix = isset($options['prefix']) ? $options['prefix'] : 'sf2s'; |
|||
} |
|||
|
|||
/** |
|||
* @return bool |
|||
*/ |
|||
public function close() |
|||
{ |
|||
return $this->memcached->quit(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doRead($sessionId) |
|||
{ |
|||
return $this->memcached->get($this->prefix.$sessionId) ?: ''; |
|||
} |
|||
|
|||
/** |
|||
* @return bool |
|||
*/ |
|||
public function updateTimestamp($sessionId, $data) |
|||
{ |
|||
$this->memcached->touch($this->prefix.$sessionId, time() + $this->ttl); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doWrite($sessionId, $data) |
|||
{ |
|||
return $this->memcached->set($this->prefix.$sessionId, $data, time() + $this->ttl); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doDestroy($sessionId) |
|||
{ |
|||
$result = $this->memcached->delete($this->prefix.$sessionId); |
|||
|
|||
return $result || \Memcached::RES_NOTFOUND == $this->memcached->getResultCode(); |
|||
} |
|||
|
|||
/** |
|||
* @return bool |
|||
*/ |
|||
public function gc($maxlifetime) |
|||
{ |
|||
// not required here because memcached will auto expire the records anyhow. |
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* Return a Memcached instance. |
|||
* |
|||
* @return \Memcached |
|||
*/ |
|||
protected function getMemcached() |
|||
{ |
|||
return $this->memcached; |
|||
} |
|||
} |
|||
@ -0,0 +1,124 @@ |
|||
<?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; |
|||
|
|||
/** |
|||
* Migrating session handler for migrating from one handler to another. It reads |
|||
* from the current handler and writes both the current and new ones. |
|||
* |
|||
* It ignores errors from the new handler. |
|||
* |
|||
* @author Ross Motley <ross.motley@amara.com> |
|||
* @author Oliver Radwell <oliver.radwell@amara.com> |
|||
*/ |
|||
class MigratingSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface |
|||
{ |
|||
private $currentHandler; |
|||
private $writeOnlyHandler; |
|||
|
|||
public function __construct(\SessionHandlerInterface $currentHandler, \SessionHandlerInterface $writeOnlyHandler) |
|||
{ |
|||
if (!$currentHandler instanceof \SessionUpdateTimestampHandlerInterface) { |
|||
$currentHandler = new StrictSessionHandler($currentHandler); |
|||
} |
|||
if (!$writeOnlyHandler instanceof \SessionUpdateTimestampHandlerInterface) { |
|||
$writeOnlyHandler = new StrictSessionHandler($writeOnlyHandler); |
|||
} |
|||
|
|||
$this->currentHandler = $currentHandler; |
|||
$this->writeOnlyHandler = $writeOnlyHandler; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function close() |
|||
{ |
|||
$result = $this->currentHandler->close(); |
|||
$this->writeOnlyHandler->close(); |
|||
|
|||
return $result; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function destroy($sessionId) |
|||
{ |
|||
$result = $this->currentHandler->destroy($sessionId); |
|||
$this->writeOnlyHandler->destroy($sessionId); |
|||
|
|||
return $result; |
|||
} |
|||
|
|||
/** |
|||
* @return bool |
|||
*/ |
|||
public function gc($maxlifetime) |
|||
{ |
|||
$result = $this->currentHandler->gc($maxlifetime); |
|||
$this->writeOnlyHandler->gc($maxlifetime); |
|||
|
|||
return $result; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function open($savePath, $sessionName) |
|||
{ |
|||
$result = $this->currentHandler->open($savePath, $sessionName); |
|||
$this->writeOnlyHandler->open($savePath, $sessionName); |
|||
|
|||
return $result; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function read($sessionId) |
|||
{ |
|||
// No reading from new handler until switch-over |
|||
return $this->currentHandler->read($sessionId); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function write($sessionId, $sessionData) |
|||
{ |
|||
$result = $this->currentHandler->write($sessionId, $sessionData); |
|||
$this->writeOnlyHandler->write($sessionId, $sessionData); |
|||
|
|||
return $result; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function validateId($sessionId) |
|||
{ |
|||
// No reading from new handler until switch-over |
|||
return $this->currentHandler->validateId($sessionId); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function updateTimestamp($sessionId, $sessionData) |
|||
{ |
|||
$result = $this->currentHandler->updateTimestamp($sessionId, $sessionData); |
|||
$this->writeOnlyHandler->updateTimestamp($sessionId, $sessionData); |
|||
|
|||
return $result; |
|||
} |
|||
} |
|||
@ -0,0 +1,193 @@ |
|||
<?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; |
|||
|
|||
/** |
|||
* Session handler using the mongodb/mongodb package and MongoDB driver extension. |
|||
* |
|||
* @author Markus Bachmann <markus.bachmann@bachi.biz> |
|||
* |
|||
* @see https://packagist.org/packages/mongodb/mongodb |
|||
* @see https://php.net/mongodb |
|||
*/ |
|||
class MongoDbSessionHandler extends AbstractSessionHandler |
|||
{ |
|||
private $mongo; |
|||
|
|||
/** |
|||
* @var \MongoDB\Collection |
|||
*/ |
|||
private $collection; |
|||
|
|||
/** |
|||
* @var array |
|||
*/ |
|||
private $options; |
|||
|
|||
/** |
|||
* Constructor. |
|||
* |
|||
* List of available options: |
|||
* * database: The name of the database [required] |
|||
* * collection: The name of the collection [required] |
|||
* * id_field: The field name for storing the session id [default: _id] |
|||
* * data_field: The field name for storing the session data [default: data] |
|||
* * time_field: The field name for storing the timestamp [default: time] |
|||
* * expiry_field: The field name for storing the expiry-timestamp [default: expires_at]. |
|||
* |
|||
* It is strongly recommended to put an index on the `expiry_field` for |
|||
* garbage-collection. Alternatively it's possible to automatically expire |
|||
* the sessions in the database as described below: |
|||
* |
|||
* A TTL collections can be used on MongoDB 2.2+ to cleanup expired sessions |
|||
* automatically. Such an index can for example look like this: |
|||
* |
|||
* db.<session-collection>.ensureIndex( |
|||
* { "<expiry-field>": 1 }, |
|||
* { "expireAfterSeconds": 0 } |
|||
* ) |
|||
* |
|||
* More details on: https://docs.mongodb.org/manual/tutorial/expire-data/ |
|||
* |
|||
* If you use such an index, you can drop `gc_probability` to 0 since |
|||
* no garbage-collection is required. |
|||
* |
|||
* @param \MongoDB\Client $mongo A MongoDB\Client instance |
|||
* @param array $options An associative array of field options |
|||
* |
|||
* @throws \InvalidArgumentException When "database" or "collection" not provided |
|||
*/ |
|||
public function __construct(\MongoDB\Client $mongo, array $options) |
|||
{ |
|||
if (!isset($options['database']) || !isset($options['collection'])) { |
|||
throw new \InvalidArgumentException('You must provide the "database" and "collection" option for MongoDBSessionHandler'); |
|||
} |
|||
|
|||
$this->mongo = $mongo; |
|||
|
|||
$this->options = array_merge([ |
|||
'id_field' => '_id', |
|||
'data_field' => 'data', |
|||
'time_field' => 'time', |
|||
'expiry_field' => 'expires_at', |
|||
], $options); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function close() |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doDestroy($sessionId) |
|||
{ |
|||
$this->getCollection()->deleteOne([ |
|||
$this->options['id_field'] => $sessionId, |
|||
]); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function gc($maxlifetime) |
|||
{ |
|||
$this->getCollection()->deleteMany([ |
|||
$this->options['expiry_field'] => ['$lt' => new \MongoDB\BSON\UTCDateTime()], |
|||
]); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doWrite($sessionId, $data) |
|||
{ |
|||
$expiry = new \MongoDB\BSON\UTCDateTime((time() + (int) ini_get('session.gc_maxlifetime')) * 1000); |
|||
|
|||
$fields = [ |
|||
$this->options['time_field'] => new \MongoDB\BSON\UTCDateTime(), |
|||
$this->options['expiry_field'] => $expiry, |
|||
$this->options['data_field'] => new \MongoDB\BSON\Binary($data, \MongoDB\BSON\Binary::TYPE_OLD_BINARY), |
|||
]; |
|||
|
|||
$this->getCollection()->updateOne( |
|||
[$this->options['id_field'] => $sessionId], |
|||
['$set' => $fields], |
|||
['upsert' => true] |
|||
); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function updateTimestamp($sessionId, $data) |
|||
{ |
|||
$expiry = new \MongoDB\BSON\UTCDateTime((time() + (int) ini_get('session.gc_maxlifetime')) * 1000); |
|||
|
|||
$this->getCollection()->updateOne( |
|||
[$this->options['id_field'] => $sessionId], |
|||
['$set' => [ |
|||
$this->options['time_field'] => new \MongoDB\BSON\UTCDateTime(), |
|||
$this->options['expiry_field'] => $expiry, |
|||
]] |
|||
); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doRead($sessionId) |
|||
{ |
|||
$dbData = $this->getCollection()->findOne([ |
|||
$this->options['id_field'] => $sessionId, |
|||
$this->options['expiry_field'] => ['$gte' => new \MongoDB\BSON\UTCDateTime()], |
|||
]); |
|||
|
|||
if (null === $dbData) { |
|||
return ''; |
|||
} |
|||
|
|||
return $dbData[$this->options['data_field']]->getData(); |
|||
} |
|||
|
|||
/** |
|||
* @return \MongoDB\Collection |
|||
*/ |
|||
private function getCollection() |
|||
{ |
|||
if (null === $this->collection) { |
|||
$this->collection = $this->mongo->selectCollection($this->options['database'], $this->options['collection']); |
|||
} |
|||
|
|||
return $this->collection; |
|||
} |
|||
|
|||
/** |
|||
* @return \MongoDB\Client |
|||
*/ |
|||
protected function getMongo() |
|||
{ |
|||
return $this->mongo; |
|||
} |
|||
} |
|||
@ -0,0 +1,55 @@ |
|||
<?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; |
|||
|
|||
/** |
|||
* Native session handler using PHP's built in file storage. |
|||
* |
|||
* @author Drak <drak@zikula.org> |
|||
*/ |
|||
class NativeFileSessionHandler extends \SessionHandler |
|||
{ |
|||
/** |
|||
* @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 https://php.net/session.configuration#ini.session.save-path for further details. |
|||
* |
|||
* @throws \InvalidArgumentException On invalid $savePath |
|||
* @throws \RuntimeException When failing to create the save directory |
|||
*/ |
|||
public function __construct(string $savePath = null) |
|||
{ |
|||
if (null === $savePath) { |
|||
$savePath = ini_get('session.save_path'); |
|||
} |
|||
|
|||
$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) && !is_dir($baseDir)) { |
|||
throw new \RuntimeException(sprintf('Session Storage was not able to create directory "%s"', $baseDir)); |
|||
} |
|||
|
|||
ini_set('session.save_path', $savePath); |
|||
ini_set('session.save_handler', 'files'); |
|||
} |
|||
} |
|||
@ -0,0 +1,76 @@ |
|||
<?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; |
|||
|
|||
/** |
|||
* Can be used in unit testing or in a situations where persisted sessions are not desired. |
|||
* |
|||
* @author Drak <drak@zikula.org> |
|||
*/ |
|||
class NullSessionHandler extends AbstractSessionHandler |
|||
{ |
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function close() |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function validateId($sessionId) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doRead($sessionId) |
|||
{ |
|||
return ''; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function updateTimestamp($sessionId, $data) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doWrite($sessionId, $data) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doDestroy($sessionId) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* @return bool |
|||
*/ |
|||
public function gc($maxlifetime) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
@ -0,0 +1,904 @@ |
|||
<?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; |
|||
|
|||
/** |
|||
* Session handler using a PDO connection to read and write data. |
|||
* |
|||
* It works with MySQL, PostgreSQL, Oracle, SQL Server and SQLite and implements |
|||
* different locking strategies to handle concurrent access to the same session. |
|||
* Locking is necessary to prevent loss of data due to race conditions and to keep |
|||
* the session data consistent between read() and write(). With locking, requests |
|||
* for the same session will wait until the other one finished writing. For this |
|||
* reason it's best practice to close a session as early as possible to improve |
|||
* concurrency. PHPs internal files session handler also implements locking. |
|||
* |
|||
* Attention: Since SQLite does not support row level locks but locks the whole database, |
|||
* it means only one session can be accessed at a time. Even different sessions would wait |
|||
* for another to finish. So saving session in SQLite should only be considered for |
|||
* development or prototypes. |
|||
* |
|||
* Session data is a binary string that can contain non-printable characters like the null byte. |
|||
* For this reason it must be saved in a binary column in the database like BLOB in MySQL. |
|||
* Saving it in a character column could corrupt the data. You can use createTable() |
|||
* to initialize a correctly defined table. |
|||
* |
|||
* @see https://php.net/sessionhandlerinterface |
|||
* |
|||
* @author Fabien Potencier <fabien@symfony.com> |
|||
* @author Michael Williams <michael.williams@funsational.com> |
|||
* @author Tobias Schultze <http://tobion.de> |
|||
*/ |
|||
class PdoSessionHandler extends AbstractSessionHandler |
|||
{ |
|||
/** |
|||
* No locking is done. This means sessions are prone to loss of data due to |
|||
* race conditions of concurrent requests to the same session. The last session |
|||
* write will win in this case. It might be useful when you implement your own |
|||
* logic to deal with this like an optimistic approach. |
|||
*/ |
|||
const LOCK_NONE = 0; |
|||
|
|||
/** |
|||
* Creates an application-level lock on a session. The disadvantage is that the |
|||
* lock is not enforced by the database and thus other, unaware parts of the |
|||
* application could still concurrently modify the session. The advantage is it |
|||
* does not require a transaction. |
|||
* This mode is not available for SQLite and not yet implemented for oci and sqlsrv. |
|||
*/ |
|||
const LOCK_ADVISORY = 1; |
|||
|
|||
/** |
|||
* Issues a real row lock. Since it uses a transaction between opening and |
|||
* closing a session, you have to be careful when you use same database connection |
|||
* that you also use for your application logic. This mode is the default because |
|||
* it's the only reliable solution across DBMSs. |
|||
*/ |
|||
const LOCK_TRANSACTIONAL = 2; |
|||
|
|||
/** |
|||
* @var \PDO|null PDO instance or null when not connected yet |
|||
*/ |
|||
private $pdo; |
|||
|
|||
/** |
|||
* @var string|false|null DSN string or null for session.save_path or false when lazy connection disabled |
|||
*/ |
|||
private $dsn = false; |
|||
|
|||
/** |
|||
* @var string Database driver |
|||
*/ |
|||
private $driver; |
|||
|
|||
/** |
|||
* @var string Table name |
|||
*/ |
|||
private $table = 'sessions'; |
|||
|
|||
/** |
|||
* @var string Column for session id |
|||
*/ |
|||
private $idCol = 'sess_id'; |
|||
|
|||
/** |
|||
* @var string Column for session data |
|||
*/ |
|||
private $dataCol = 'sess_data'; |
|||
|
|||
/** |
|||
* @var string Column for lifetime |
|||
*/ |
|||
private $lifetimeCol = 'sess_lifetime'; |
|||
|
|||
/** |
|||
* @var string Column for timestamp |
|||
*/ |
|||
private $timeCol = 'sess_time'; |
|||
|
|||
/** |
|||
* @var string Username when lazy-connect |
|||
*/ |
|||
private $username = ''; |
|||
|
|||
/** |
|||
* @var string Password when lazy-connect |
|||
*/ |
|||
private $password = ''; |
|||
|
|||
/** |
|||
* @var array Connection options when lazy-connect |
|||
*/ |
|||
private $connectionOptions = []; |
|||
|
|||
/** |
|||
* @var int The strategy for locking, see constants |
|||
*/ |
|||
private $lockMode = self::LOCK_TRANSACTIONAL; |
|||
|
|||
/** |
|||
* It's an array to support multiple reads before closing which is manual, non-standard usage. |
|||
* |
|||
* @var \PDOStatement[] An array of statements to release advisory locks |
|||
*/ |
|||
private $unlockStatements = []; |
|||
|
|||
/** |
|||
* @var bool True when the current session exists but expired according to session.gc_maxlifetime |
|||
*/ |
|||
private $sessionExpired = false; |
|||
|
|||
/** |
|||
* @var bool Whether a transaction is active |
|||
*/ |
|||
private $inTransaction = false; |
|||
|
|||
/** |
|||
* @var bool Whether gc() has been called |
|||
*/ |
|||
private $gcCalled = false; |
|||
|
|||
/** |
|||
* You can either pass an existing database connection as PDO instance or |
|||
* pass a DSN string that will be used to lazy-connect to the database |
|||
* when the session is actually used. Furthermore it's possible to pass null |
|||
* which will then use the session.save_path ini setting as PDO DSN parameter. |
|||
* |
|||
* List of available options: |
|||
* * db_table: The name of the table [default: sessions] |
|||
* * 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_lifetime_col: The column where to store the lifetime [default: sess_lifetime] |
|||
* * db_time_col: The column where to store the timestamp [default: sess_time] |
|||
* * db_username: The username when lazy-connect [default: ''] |
|||
* * db_password: The password when lazy-connect [default: ''] |
|||
* * db_connection_options: An array of driver-specific connection options [default: []] |
|||
* * lock_mode: The strategy for locking, see constants [default: LOCK_TRANSACTIONAL] |
|||
* |
|||
* @param \PDO|string|null $pdoOrDsn A \PDO instance or DSN string or URL string or null |
|||
* @param array $options An associative array of options |
|||
* |
|||
* @throws \InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION |
|||
*/ |
|||
public function __construct($pdoOrDsn = null, array $options = []) |
|||
{ |
|||
if ($pdoOrDsn instanceof \PDO) { |
|||
if (\PDO::ERRMODE_EXCEPTION !== $pdoOrDsn->getAttribute(\PDO::ATTR_ERRMODE)) { |
|||
throw new \InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION))', __CLASS__)); |
|||
} |
|||
|
|||
$this->pdo = $pdoOrDsn; |
|||
$this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); |
|||
} elseif (\is_string($pdoOrDsn) && false !== strpos($pdoOrDsn, '://')) { |
|||
$this->dsn = $this->buildDsnFromUrl($pdoOrDsn); |
|||
} else { |
|||
$this->dsn = $pdoOrDsn; |
|||
} |
|||
|
|||
$this->table = isset($options['db_table']) ? $options['db_table'] : $this->table; |
|||
$this->idCol = isset($options['db_id_col']) ? $options['db_id_col'] : $this->idCol; |
|||
$this->dataCol = isset($options['db_data_col']) ? $options['db_data_col'] : $this->dataCol; |
|||
$this->lifetimeCol = isset($options['db_lifetime_col']) ? $options['db_lifetime_col'] : $this->lifetimeCol; |
|||
$this->timeCol = isset($options['db_time_col']) ? $options['db_time_col'] : $this->timeCol; |
|||
$this->username = isset($options['db_username']) ? $options['db_username'] : $this->username; |
|||
$this->password = isset($options['db_password']) ? $options['db_password'] : $this->password; |
|||
$this->connectionOptions = isset($options['db_connection_options']) ? $options['db_connection_options'] : $this->connectionOptions; |
|||
$this->lockMode = isset($options['lock_mode']) ? $options['lock_mode'] : $this->lockMode; |
|||
} |
|||
|
|||
/** |
|||
* Creates the table to store sessions which can be called once for setup. |
|||
* |
|||
* Session ID is saved in a column of maximum length 128 because that is enough even |
|||
* for a 512 bit configured session.hash_function like Whirlpool. Session data is |
|||
* saved in a BLOB. One could also use a shorter inlined varbinary column |
|||
* if one was sure the data fits into it. |
|||
* |
|||
* @throws \PDOException When the table already exists |
|||
* @throws \DomainException When an unsupported PDO driver is used |
|||
*/ |
|||
public function createTable() |
|||
{ |
|||
// connect if we are not yet |
|||
$this->getConnection(); |
|||
|
|||
switch ($this->driver) { |
|||
case 'mysql': |
|||
// We use varbinary for the ID column because it prevents unwanted conversions: |
|||
// - character set conversions between server and client |
|||
// - trailing space removal |
|||
// - case-insensitivity |
|||
// - language processing like é == e |
|||
$sql = "CREATE TABLE $this->table ($this->idCol VARBINARY(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol MEDIUMINT NOT NULL, $this->timeCol INTEGER UNSIGNED NOT NULL) COLLATE utf8_bin, ENGINE = InnoDB"; |
|||
break; |
|||
case 'sqlite': |
|||
$sql = "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)"; |
|||
break; |
|||
case 'pgsql': |
|||
$sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(128) NOT NULL PRIMARY KEY, $this->dataCol BYTEA NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)"; |
|||
break; |
|||
case 'oci': |
|||
$sql = "CREATE TABLE $this->table ($this->idCol VARCHAR2(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)"; |
|||
break; |
|||
case 'sqlsrv': |
|||
$sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(128) NOT NULL PRIMARY KEY, $this->dataCol VARBINARY(MAX) NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)"; |
|||
break; |
|||
default: |
|||
throw new \DomainException(sprintf('Creating the session table is currently not implemented for PDO driver "%s".', $this->driver)); |
|||
} |
|||
|
|||
try { |
|||
$this->pdo->exec($sql); |
|||
} catch (\PDOException $e) { |
|||
$this->rollback(); |
|||
|
|||
throw $e; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Returns true when the current session exists but expired according to session.gc_maxlifetime. |
|||
* |
|||
* Can be used to distinguish between a new session and one that expired due to inactivity. |
|||
* |
|||
* @return bool Whether current session expired |
|||
*/ |
|||
public function isSessionExpired() |
|||
{ |
|||
return $this->sessionExpired; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function open($savePath, $sessionName) |
|||
{ |
|||
$this->sessionExpired = false; |
|||
|
|||
if (null === $this->pdo) { |
|||
$this->connect($this->dsn ?: $savePath); |
|||
} |
|||
|
|||
return parent::open($savePath, $sessionName); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function read($sessionId) |
|||
{ |
|||
try { |
|||
return parent::read($sessionId); |
|||
} catch (\PDOException $e) { |
|||
$this->rollback(); |
|||
|
|||
throw $e; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* @return bool |
|||
*/ |
|||
public function gc($maxlifetime) |
|||
{ |
|||
// We delay gc() to close() so that it is executed outside the transactional and blocking read-write process. |
|||
// This way, pruning expired sessions does not block them from being started while the current session is used. |
|||
$this->gcCalled = true; |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doDestroy($sessionId) |
|||
{ |
|||
// delete the record associated with this id |
|||
$sql = "DELETE FROM $this->table WHERE $this->idCol = :id"; |
|||
|
|||
try { |
|||
$stmt = $this->pdo->prepare($sql); |
|||
$stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); |
|||
$stmt->execute(); |
|||
} catch (\PDOException $e) { |
|||
$this->rollback(); |
|||
|
|||
throw $e; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doWrite($sessionId, $data) |
|||
{ |
|||
$maxlifetime = (int) ini_get('session.gc_maxlifetime'); |
|||
|
|||
try { |
|||
// We use a single MERGE SQL query when supported by the database. |
|||
$mergeStmt = $this->getMergeStatement($sessionId, $data, $maxlifetime); |
|||
if (null !== $mergeStmt) { |
|||
$mergeStmt->execute(); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
$updateStmt = $this->getUpdateStatement($sessionId, $data, $maxlifetime); |
|||
$updateStmt->execute(); |
|||
|
|||
// When MERGE is not supported, like in Postgres < 9.5, we have to use this approach that can result in |
|||
// duplicate key errors when the same session is written simultaneously (given the LOCK_NONE behavior). |
|||
// We can just catch such an error and re-execute the update. This is similar to a serializable |
|||
// transaction with retry logic on serialization failures but without the overhead and without possible |
|||
// false positives due to longer gap locking. |
|||
if (!$updateStmt->rowCount()) { |
|||
try { |
|||
$insertStmt = $this->getInsertStatement($sessionId, $data, $maxlifetime); |
|||
$insertStmt->execute(); |
|||
} catch (\PDOException $e) { |
|||
// Handle integrity violation SQLSTATE 23000 (or a subclass like 23505 in Postgres) for duplicate keys |
|||
if (0 === strpos($e->getCode(), '23')) { |
|||
$updateStmt->execute(); |
|||
} else { |
|||
throw $e; |
|||
} |
|||
} |
|||
} |
|||
} catch (\PDOException $e) { |
|||
$this->rollback(); |
|||
|
|||
throw $e; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function updateTimestamp($sessionId, $data) |
|||
{ |
|||
$maxlifetime = (int) ini_get('session.gc_maxlifetime'); |
|||
|
|||
try { |
|||
$updateStmt = $this->pdo->prepare( |
|||
"UPDATE $this->table SET $this->lifetimeCol = :lifetime, $this->timeCol = :time WHERE $this->idCol = :id" |
|||
); |
|||
$updateStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); |
|||
$updateStmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT); |
|||
$updateStmt->bindValue(':time', time(), \PDO::PARAM_INT); |
|||
$updateStmt->execute(); |
|||
} catch (\PDOException $e) { |
|||
$this->rollback(); |
|||
|
|||
throw $e; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function close() |
|||
{ |
|||
$this->commit(); |
|||
|
|||
while ($unlockStmt = array_shift($this->unlockStatements)) { |
|||
$unlockStmt->execute(); |
|||
} |
|||
|
|||
if ($this->gcCalled) { |
|||
$this->gcCalled = false; |
|||
|
|||
// delete the session records that have expired |
|||
if ('mysql' === $this->driver) { |
|||
$sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol < :time"; |
|||
} else { |
|||
$sql = "DELETE FROM $this->table WHERE $this->lifetimeCol < :time - $this->timeCol"; |
|||
} |
|||
|
|||
$stmt = $this->pdo->prepare($sql); |
|||
$stmt->bindValue(':time', time(), \PDO::PARAM_INT); |
|||
$stmt->execute(); |
|||
} |
|||
|
|||
if (false !== $this->dsn) { |
|||
$this->pdo = null; // only close lazy-connection |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* Lazy-connects to the database. |
|||
* |
|||
* @param string $dsn DSN string |
|||
*/ |
|||
private function connect($dsn) |
|||
{ |
|||
$this->pdo = new \PDO($dsn, $this->username, $this->password, $this->connectionOptions); |
|||
$this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); |
|||
$this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); |
|||
} |
|||
|
|||
/** |
|||
* Builds a PDO DSN from a URL-like connection string. |
|||
* |
|||
* @param string $dsnOrUrl |
|||
* |
|||
* @return string |
|||
* |
|||
* @todo implement missing support for oci DSN (which look totally different from other PDO ones) |
|||
*/ |
|||
private function buildDsnFromUrl($dsnOrUrl) |
|||
{ |
|||
// (pdo_)?sqlite3?:///... => (pdo_)?sqlite3?://localhost/... or else the URL will be invalid |
|||
$url = preg_replace('#^((?:pdo_)?sqlite3?):///#', '$1://localhost/', $dsnOrUrl); |
|||
|
|||
$params = parse_url($url); |
|||
|
|||
if (false === $params) { |
|||
return $dsnOrUrl; // If the URL is not valid, let's assume it might be a DSN already. |
|||
} |
|||
|
|||
$params = array_map('rawurldecode', $params); |
|||
|
|||
// Override the default username and password. Values passed through options will still win over these in the constructor. |
|||
if (isset($params['user'])) { |
|||
$this->username = $params['user']; |
|||
} |
|||
|
|||
if (isset($params['pass'])) { |
|||
$this->password = $params['pass']; |
|||
} |
|||
|
|||
if (!isset($params['scheme'])) { |
|||
throw new \InvalidArgumentException('URLs without scheme are not supported to configure the PdoSessionHandler'); |
|||
} |
|||
|
|||
$driverAliasMap = [ |
|||
'mssql' => 'sqlsrv', |
|||
'mysql2' => 'mysql', // Amazon RDS, for some weird reason |
|||
'postgres' => 'pgsql', |
|||
'postgresql' => 'pgsql', |
|||
'sqlite3' => 'sqlite', |
|||
]; |
|||
|
|||
$driver = isset($driverAliasMap[$params['scheme']]) ? $driverAliasMap[$params['scheme']] : $params['scheme']; |
|||
|
|||
// Doctrine DBAL supports passing its internal pdo_* driver names directly too (allowing both dashes and underscores). This allows supporting the same here. |
|||
if (0 === strpos($driver, 'pdo_') || 0 === strpos($driver, 'pdo-')) { |
|||
$driver = substr($driver, 4); |
|||
} |
|||
|
|||
switch ($driver) { |
|||
case 'mysql': |
|||
case 'pgsql': |
|||
$dsn = $driver.':'; |
|||
|
|||
if (isset($params['host']) && '' !== $params['host']) { |
|||
$dsn .= 'host='.$params['host'].';'; |
|||
} |
|||
|
|||
if (isset($params['port']) && '' !== $params['port']) { |
|||
$dsn .= 'port='.$params['port'].';'; |
|||
} |
|||
|
|||
if (isset($params['path'])) { |
|||
$dbName = substr($params['path'], 1); // Remove the leading slash |
|||
$dsn .= 'dbname='.$dbName.';'; |
|||
} |
|||
|
|||
return $dsn; |
|||
|
|||
case 'sqlite': |
|||
return 'sqlite:'.substr($params['path'], 1); |
|||
|
|||
case 'sqlsrv': |
|||
$dsn = 'sqlsrv:server='; |
|||
|
|||
if (isset($params['host'])) { |
|||
$dsn .= $params['host']; |
|||
} |
|||
|
|||
if (isset($params['port']) && '' !== $params['port']) { |
|||
$dsn .= ','.$params['port']; |
|||
} |
|||
|
|||
if (isset($params['path'])) { |
|||
$dbName = substr($params['path'], 1); // Remove the leading slash |
|||
$dsn .= ';Database='.$dbName; |
|||
} |
|||
|
|||
return $dsn; |
|||
|
|||
default: |
|||
throw new \InvalidArgumentException(sprintf('The scheme "%s" is not supported by the PdoSessionHandler URL configuration. Pass a PDO DSN directly.', $params['scheme'])); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Helper method to begin a transaction. |
|||
* |
|||
* Since SQLite does not support row level locks, we have to acquire a reserved lock |
|||
* on the database immediately. Because of https://bugs.php.net/42766 we have to create |
|||
* such a transaction manually which also means we cannot use PDO::commit or |
|||
* PDO::rollback or PDO::inTransaction for SQLite. |
|||
* |
|||
* Also MySQLs default isolation, REPEATABLE READ, causes deadlock for different sessions |
|||
* due to https://percona.com/blog/2013/12/12/one-more-innodb-gap-lock-to-avoid/ . |
|||
* So we change it to READ COMMITTED. |
|||
*/ |
|||
private function beginTransaction() |
|||
{ |
|||
if (!$this->inTransaction) { |
|||
if ('sqlite' === $this->driver) { |
|||
$this->pdo->exec('BEGIN IMMEDIATE TRANSACTION'); |
|||
} else { |
|||
if ('mysql' === $this->driver) { |
|||
$this->pdo->exec('SET TRANSACTION ISOLATION LEVEL READ COMMITTED'); |
|||
} |
|||
$this->pdo->beginTransaction(); |
|||
} |
|||
$this->inTransaction = true; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Helper method to commit a transaction. |
|||
*/ |
|||
private function commit() |
|||
{ |
|||
if ($this->inTransaction) { |
|||
try { |
|||
// commit read-write transaction which also releases the lock |
|||
if ('sqlite' === $this->driver) { |
|||
$this->pdo->exec('COMMIT'); |
|||
} else { |
|||
$this->pdo->commit(); |
|||
} |
|||
$this->inTransaction = false; |
|||
} catch (\PDOException $e) { |
|||
$this->rollback(); |
|||
|
|||
throw $e; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Helper method to rollback a transaction. |
|||
*/ |
|||
private function rollback() |
|||
{ |
|||
// We only need to rollback if we are in a transaction. Otherwise the resulting |
|||
// error would hide the real problem why rollback was called. We might not be |
|||
// in a transaction when not using the transactional locking behavior or when |
|||
// two callbacks (e.g. destroy and write) are invoked that both fail. |
|||
if ($this->inTransaction) { |
|||
if ('sqlite' === $this->driver) { |
|||
$this->pdo->exec('ROLLBACK'); |
|||
} else { |
|||
$this->pdo->rollBack(); |
|||
} |
|||
$this->inTransaction = false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Reads the session data in respect to the different locking strategies. |
|||
* |
|||
* We need to make sure we do not return session data that is already considered garbage according |
|||
* to the session.gc_maxlifetime setting because gc() is called after read() and only sometimes. |
|||
* |
|||
* @param string $sessionId Session ID |
|||
* |
|||
* @return string The session data |
|||
*/ |
|||
protected function doRead($sessionId) |
|||
{ |
|||
if (self::LOCK_ADVISORY === $this->lockMode) { |
|||
$this->unlockStatements[] = $this->doAdvisoryLock($sessionId); |
|||
} |
|||
|
|||
$selectSql = $this->getSelectSql(); |
|||
$selectStmt = $this->pdo->prepare($selectSql); |
|||
$selectStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); |
|||
$insertStmt = null; |
|||
|
|||
do { |
|||
$selectStmt->execute(); |
|||
$sessionRows = $selectStmt->fetchAll(\PDO::FETCH_NUM); |
|||
|
|||
if ($sessionRows) { |
|||
if ($sessionRows[0][1] + $sessionRows[0][2] < time()) { |
|||
$this->sessionExpired = true; |
|||
|
|||
return ''; |
|||
} |
|||
|
|||
return \is_resource($sessionRows[0][0]) ? stream_get_contents($sessionRows[0][0]) : $sessionRows[0][0]; |
|||
} |
|||
|
|||
if (null !== $insertStmt) { |
|||
$this->rollback(); |
|||
throw new \RuntimeException('Failed to read session: INSERT reported a duplicate id but next SELECT did not return any data.'); |
|||
} |
|||
|
|||
if (!filter_var(ini_get('session.use_strict_mode'), FILTER_VALIDATE_BOOLEAN) && self::LOCK_TRANSACTIONAL === $this->lockMode && 'sqlite' !== $this->driver) { |
|||
// In strict mode, session fixation is not possible: new sessions always start with a unique |
|||
// random id, so that concurrency is not possible and this code path can be skipped. |
|||
// Exclusive-reading of non-existent rows does not block, so we need to do an insert to block |
|||
// until other connections to the session are committed. |
|||
try { |
|||
$insertStmt = $this->getInsertStatement($sessionId, '', 0); |
|||
$insertStmt->execute(); |
|||
} catch (\PDOException $e) { |
|||
// Catch duplicate key error because other connection created the session already. |
|||
// It would only not be the case when the other connection destroyed the session. |
|||
if (0 === strpos($e->getCode(), '23')) { |
|||
// Retrieve finished session data written by concurrent connection by restarting the loop. |
|||
// We have to start a new transaction as a failed query will mark the current transaction as |
|||
// aborted in PostgreSQL and disallow further queries within it. |
|||
$this->rollback(); |
|||
$this->beginTransaction(); |
|||
continue; |
|||
} |
|||
|
|||
throw $e; |
|||
} |
|||
} |
|||
|
|||
return ''; |
|||
} while (true); |
|||
} |
|||
|
|||
/** |
|||
* Executes an application-level lock on the database. |
|||
* |
|||
* @return \PDOStatement The statement that needs to be executed later to release the lock |
|||
* |
|||
* @throws \DomainException When an unsupported PDO driver is used |
|||
* |
|||
* @todo implement missing advisory locks |
|||
* - for oci using DBMS_LOCK.REQUEST |
|||
* - for sqlsrv using sp_getapplock with LockOwner = Session |
|||
*/ |
|||
private function doAdvisoryLock(string $sessionId) |
|||
{ |
|||
switch ($this->driver) { |
|||
case 'mysql': |
|||
// MySQL 5.7.5 and later enforces a maximum length on lock names of 64 characters. Previously, no limit was enforced. |
|||
$lockId = substr($sessionId, 0, 64); |
|||
// should we handle the return value? 0 on timeout, null on error |
|||
// we use a timeout of 50 seconds which is also the default for innodb_lock_wait_timeout |
|||
$stmt = $this->pdo->prepare('SELECT GET_LOCK(:key, 50)'); |
|||
$stmt->bindValue(':key', $lockId, \PDO::PARAM_STR); |
|||
$stmt->execute(); |
|||
|
|||
$releaseStmt = $this->pdo->prepare('DO RELEASE_LOCK(:key)'); |
|||
$releaseStmt->bindValue(':key', $lockId, \PDO::PARAM_STR); |
|||
|
|||
return $releaseStmt; |
|||
case 'pgsql': |
|||
// Obtaining an exclusive session level advisory lock requires an integer key. |
|||
// When session.sid_bits_per_character > 4, the session id can contain non-hex-characters. |
|||
// So we cannot just use hexdec(). |
|||
if (4 === \PHP_INT_SIZE) { |
|||
$sessionInt1 = $this->convertStringToInt($sessionId); |
|||
$sessionInt2 = $this->convertStringToInt(substr($sessionId, 4, 4)); |
|||
|
|||
$stmt = $this->pdo->prepare('SELECT pg_advisory_lock(:key1, :key2)'); |
|||
$stmt->bindValue(':key1', $sessionInt1, \PDO::PARAM_INT); |
|||
$stmt->bindValue(':key2', $sessionInt2, \PDO::PARAM_INT); |
|||
$stmt->execute(); |
|||
|
|||
$releaseStmt = $this->pdo->prepare('SELECT pg_advisory_unlock(:key1, :key2)'); |
|||
$releaseStmt->bindValue(':key1', $sessionInt1, \PDO::PARAM_INT); |
|||
$releaseStmt->bindValue(':key2', $sessionInt2, \PDO::PARAM_INT); |
|||
} else { |
|||
$sessionBigInt = $this->convertStringToInt($sessionId); |
|||
|
|||
$stmt = $this->pdo->prepare('SELECT pg_advisory_lock(:key)'); |
|||
$stmt->bindValue(':key', $sessionBigInt, \PDO::PARAM_INT); |
|||
$stmt->execute(); |
|||
|
|||
$releaseStmt = $this->pdo->prepare('SELECT pg_advisory_unlock(:key)'); |
|||
$releaseStmt->bindValue(':key', $sessionBigInt, \PDO::PARAM_INT); |
|||
} |
|||
|
|||
return $releaseStmt; |
|||
case 'sqlite': |
|||
throw new \DomainException('SQLite does not support advisory locks.'); |
|||
default: |
|||
throw new \DomainException(sprintf('Advisory locks are currently not implemented for PDO driver "%s".', $this->driver)); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Encodes the first 4 (when PHP_INT_SIZE == 4) or 8 characters of the string as an integer. |
|||
* |
|||
* Keep in mind, PHP integers are signed. |
|||
*/ |
|||
private function convertStringToInt(string $string): int |
|||
{ |
|||
if (4 === \PHP_INT_SIZE) { |
|||
return (\ord($string[3]) << 24) + (\ord($string[2]) << 16) + (\ord($string[1]) << 8) + \ord($string[0]); |
|||
} |
|||
|
|||
$int1 = (\ord($string[7]) << 24) + (\ord($string[6]) << 16) + (\ord($string[5]) << 8) + \ord($string[4]); |
|||
$int2 = (\ord($string[3]) << 24) + (\ord($string[2]) << 16) + (\ord($string[1]) << 8) + \ord($string[0]); |
|||
|
|||
return $int2 + ($int1 << 32); |
|||
} |
|||
|
|||
/** |
|||
* Return a locking or nonlocking SQL query to read session information. |
|||
* |
|||
* @throws \DomainException When an unsupported PDO driver is used |
|||
*/ |
|||
private function getSelectSql(): string |
|||
{ |
|||
if (self::LOCK_TRANSACTIONAL === $this->lockMode) { |
|||
$this->beginTransaction(); |
|||
|
|||
switch ($this->driver) { |
|||
case 'mysql': |
|||
case 'oci': |
|||
case 'pgsql': |
|||
return "SELECT $this->dataCol, $this->lifetimeCol, $this->timeCol FROM $this->table WHERE $this->idCol = :id FOR UPDATE"; |
|||
case 'sqlsrv': |
|||
return "SELECT $this->dataCol, $this->lifetimeCol, $this->timeCol FROM $this->table WITH (UPDLOCK, ROWLOCK) WHERE $this->idCol = :id"; |
|||
case 'sqlite': |
|||
// we already locked when starting transaction |
|||
break; |
|||
default: |
|||
throw new \DomainException(sprintf('Transactional locks are currently not implemented for PDO driver "%s".', $this->driver)); |
|||
} |
|||
} |
|||
|
|||
return "SELECT $this->dataCol, $this->lifetimeCol, $this->timeCol FROM $this->table WHERE $this->idCol = :id"; |
|||
} |
|||
|
|||
/** |
|||
* Returns an insert statement supported by the database for writing session data. |
|||
* |
|||
* @param string $sessionId Session ID |
|||
* @param string $sessionData Encoded session data |
|||
* @param int $maxlifetime session.gc_maxlifetime |
|||
* |
|||
* @return \PDOStatement The insert statement |
|||
*/ |
|||
private function getInsertStatement($sessionId, $sessionData, $maxlifetime) |
|||
{ |
|||
switch ($this->driver) { |
|||
case 'oci': |
|||
$data = fopen('php://memory', 'r+'); |
|||
fwrite($data, $sessionData); |
|||
rewind($data); |
|||
$sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, EMPTY_BLOB(), :lifetime, :time) RETURNING $this->dataCol into :data"; |
|||
break; |
|||
default: |
|||
$data = $sessionData; |
|||
$sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)"; |
|||
break; |
|||
} |
|||
|
|||
$stmt = $this->pdo->prepare($sql); |
|||
$stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); |
|||
$stmt->bindParam(':data', $data, \PDO::PARAM_LOB); |
|||
$stmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT); |
|||
$stmt->bindValue(':time', time(), \PDO::PARAM_INT); |
|||
|
|||
return $stmt; |
|||
} |
|||
|
|||
/** |
|||
* Returns an update statement supported by the database for writing session data. |
|||
* |
|||
* @param string $sessionId Session ID |
|||
* @param string $sessionData Encoded session data |
|||
* @param int $maxlifetime session.gc_maxlifetime |
|||
* |
|||
* @return \PDOStatement The update statement |
|||
*/ |
|||
private function getUpdateStatement($sessionId, $sessionData, $maxlifetime) |
|||
{ |
|||
switch ($this->driver) { |
|||
case 'oci': |
|||
$data = fopen('php://memory', 'r+'); |
|||
fwrite($data, $sessionData); |
|||
rewind($data); |
|||
$sql = "UPDATE $this->table SET $this->dataCol = EMPTY_BLOB(), $this->lifetimeCol = :lifetime, $this->timeCol = :time WHERE $this->idCol = :id RETURNING $this->dataCol into :data"; |
|||
break; |
|||
default: |
|||
$data = $sessionData; |
|||
$sql = "UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time WHERE $this->idCol = :id"; |
|||
break; |
|||
} |
|||
|
|||
$stmt = $this->pdo->prepare($sql); |
|||
$stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); |
|||
$stmt->bindParam(':data', $data, \PDO::PARAM_LOB); |
|||
$stmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT); |
|||
$stmt->bindValue(':time', time(), \PDO::PARAM_INT); |
|||
|
|||
return $stmt; |
|||
} |
|||
|
|||
/** |
|||
* Returns a merge/upsert (i.e. insert or update) statement when supported by the database for writing session data. |
|||
*/ |
|||
private function getMergeStatement(string $sessionId, string $data, int $maxlifetime): ?\PDOStatement |
|||
{ |
|||
switch (true) { |
|||
case 'mysql' === $this->driver: |
|||
$mergeSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ". |
|||
"ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)"; |
|||
break; |
|||
case 'sqlsrv' === $this->driver && version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '10', '>='): |
|||
// MERGE is only available since SQL Server 2008 and must be terminated by semicolon |
|||
// It also requires HOLDLOCK according to https://weblogs.sqlteam.com/dang/2009/01/31/upsert-race-condition-with-merge/ |
|||
$mergeSql = "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = ?) ". |
|||
"WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ". |
|||
"WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?;"; |
|||
break; |
|||
case 'sqlite' === $this->driver: |
|||
$mergeSql = "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)"; |
|||
break; |
|||
case 'pgsql' === $this->driver && version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '9.5', '>='): |
|||
$mergeSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ". |
|||
"ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->lifetimeCol, $this->timeCol) = (EXCLUDED.$this->dataCol, EXCLUDED.$this->lifetimeCol, EXCLUDED.$this->timeCol)"; |
|||
break; |
|||
default: |
|||
// MERGE is not supported with LOBs: https://oracle.com/technetwork/articles/fuecks-lobs-095315.html |
|||
return null; |
|||
} |
|||
|
|||
$mergeStmt = $this->pdo->prepare($mergeSql); |
|||
|
|||
if ('sqlsrv' === $this->driver) { |
|||
$mergeStmt->bindParam(1, $sessionId, \PDO::PARAM_STR); |
|||
$mergeStmt->bindParam(2, $sessionId, \PDO::PARAM_STR); |
|||
$mergeStmt->bindParam(3, $data, \PDO::PARAM_LOB); |
|||
$mergeStmt->bindParam(4, $maxlifetime, \PDO::PARAM_INT); |
|||
$mergeStmt->bindValue(5, time(), \PDO::PARAM_INT); |
|||
$mergeStmt->bindParam(6, $data, \PDO::PARAM_LOB); |
|||
$mergeStmt->bindParam(7, $maxlifetime, \PDO::PARAM_INT); |
|||
$mergeStmt->bindValue(8, time(), \PDO::PARAM_INT); |
|||
} else { |
|||
$mergeStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); |
|||
$mergeStmt->bindParam(':data', $data, \PDO::PARAM_LOB); |
|||
$mergeStmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT); |
|||
$mergeStmt->bindValue(':time', time(), \PDO::PARAM_INT); |
|||
} |
|||
|
|||
return $mergeStmt; |
|||
} |
|||
|
|||
/** |
|||
* Return a PDO instance. |
|||
* |
|||
* @return \PDO |
|||
*/ |
|||
protected function getConnection() |
|||
{ |
|||
if (null === $this->pdo) { |
|||
$this->connect($this->dsn ?: ini_get('session.save_path')); |
|||
} |
|||
|
|||
return $this->pdo; |
|||
} |
|||
} |
|||
@ -0,0 +1,114 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; |
|||
|
|||
use Predis\Response\ErrorInterface; |
|||
use Symfony\Component\Cache\Traits\RedisClusterProxy; |
|||
use Symfony\Component\Cache\Traits\RedisProxy; |
|||
|
|||
/** |
|||
* Redis based session storage handler based on the Redis class |
|||
* provided by the PHP redis extension. |
|||
* |
|||
* @author Dalibor Karlović <dalibor@flexolabs.io> |
|||
*/ |
|||
class RedisSessionHandler extends AbstractSessionHandler |
|||
{ |
|||
private $redis; |
|||
|
|||
/** |
|||
* @var string Key prefix for shared environments |
|||
*/ |
|||
private $prefix; |
|||
|
|||
/** |
|||
* List of available options: |
|||
* * prefix: The prefix to use for the keys in order to avoid collision on the Redis server. |
|||
* |
|||
* @param \Redis|\RedisArray|\RedisCluster|\Predis\Client|RedisProxy $redis |
|||
* @param array $options An associative array of options |
|||
* |
|||
* @throws \InvalidArgumentException When unsupported client or options are passed |
|||
*/ |
|||
public function __construct($redis, array $options = []) |
|||
{ |
|||
if ( |
|||
!$redis instanceof \Redis && |
|||
!$redis instanceof \RedisArray && |
|||
!$redis instanceof \RedisCluster && |
|||
!$redis instanceof \Predis\Client && |
|||
!$redis instanceof RedisProxy && |
|||
!$redis instanceof RedisClusterProxy |
|||
) { |
|||
throw new \InvalidArgumentException(sprintf('%s() expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\Client, %s given', __METHOD__, \is_object($redis) ? \get_class($redis) : \gettype($redis))); |
|||
} |
|||
|
|||
if ($diff = array_diff(array_keys($options), ['prefix'])) { |
|||
throw new \InvalidArgumentException(sprintf('The following options are not supported "%s"', implode(', ', $diff))); |
|||
} |
|||
|
|||
$this->redis = $redis; |
|||
$this->prefix = $options['prefix'] ?? 'sf_s'; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doRead($sessionId): string |
|||
{ |
|||
return $this->redis->get($this->prefix.$sessionId) ?: ''; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doWrite($sessionId, $data): bool |
|||
{ |
|||
$result = $this->redis->setEx($this->prefix.$sessionId, (int) ini_get('session.gc_maxlifetime'), $data); |
|||
|
|||
return $result && !$result instanceof ErrorInterface; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doDestroy($sessionId): bool |
|||
{ |
|||
$this->redis->del($this->prefix.$sessionId); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function close(): bool |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function gc($maxlifetime): bool |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function updateTimestamp($sessionId, $data) |
|||
{ |
|||
return (bool) $this->redis->expire($this->prefix.$sessionId, (int) ini_get('session.gc_maxlifetime')); |
|||
} |
|||
} |
|||
@ -0,0 +1,103 @@ |
|||
<?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; |
|||
|
|||
/** |
|||
* Adds basic `SessionUpdateTimestampHandlerInterface` behaviors to another `SessionHandlerInterface`. |
|||
* |
|||
* @author Nicolas Grekas <p@tchwork.com> |
|||
*/ |
|||
class StrictSessionHandler extends AbstractSessionHandler |
|||
{ |
|||
private $handler; |
|||
private $doDestroy; |
|||
|
|||
public function __construct(\SessionHandlerInterface $handler) |
|||
{ |
|||
if ($handler instanceof \SessionUpdateTimestampHandlerInterface) { |
|||
throw new \LogicException(sprintf('"%s" is already an instance of "SessionUpdateTimestampHandlerInterface", you cannot wrap it with "%s".', \get_class($handler), self::class)); |
|||
} |
|||
|
|||
$this->handler = $handler; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function open($savePath, $sessionName) |
|||
{ |
|||
parent::open($savePath, $sessionName); |
|||
|
|||
return $this->handler->open($savePath, $sessionName); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doRead($sessionId) |
|||
{ |
|||
return $this->handler->read($sessionId); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function updateTimestamp($sessionId, $data) |
|||
{ |
|||
return $this->write($sessionId, $data); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doWrite($sessionId, $data) |
|||
{ |
|||
return $this->handler->write($sessionId, $data); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function destroy($sessionId) |
|||
{ |
|||
$this->doDestroy = true; |
|||
$destroyed = parent::destroy($sessionId); |
|||
|
|||
return $this->doDestroy ? $this->doDestroy($sessionId) : $destroyed; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function doDestroy($sessionId) |
|||
{ |
|||
$this->doDestroy = false; |
|||
|
|||
return $this->handler->destroy($sessionId); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function close() |
|||
{ |
|||
return $this->handler->close(); |
|||
} |
|||
|
|||
/** |
|||
* @return bool |
|||
*/ |
|||
public function gc($maxlifetime) |
|||
{ |
|||
return $this->handler->gc($maxlifetime); |
|||
} |
|||
} |
|||
@ -0,0 +1,168 @@ |
|||
<?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 = [self::CREATED => 0, self::UPDATED => 0, self::LIFETIME => 0]; |
|||
|
|||
/** |
|||
* Unix timestamp. |
|||
* |
|||
* @var int |
|||
*/ |
|||
private $lastUsed; |
|||
|
|||
/** |
|||
* @var int |
|||
*/ |
|||
private $updateThreshold; |
|||
|
|||
/** |
|||
* @param string $storageKey The key used to store bag in the session |
|||
* @param int $updateThreshold The time to wait between two UPDATED updates |
|||
*/ |
|||
public function __construct(string $storageKey = '_sf2_meta', int $updateThreshold = 0) |
|||
{ |
|||
$this->storageKey = $storageKey; |
|||
$this->updateThreshold = $updateThreshold; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function initialize(array &$array) |
|||
{ |
|||
$this->meta = &$array; |
|||
|
|||
if (isset($array[self::CREATED])) { |
|||
$this->lastUsed = $this->meta[self::UPDATED]; |
|||
|
|||
$timeStamp = time(); |
|||
if ($timeStamp - $array[self::UPDATED] >= $this->updateThreshold) { |
|||
$this->meta[self::UPDATED] = $timeStamp; |
|||
} |
|||
} else { |
|||
$this->stampCreated(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Gets the lifetime that the session cookie was set with. |
|||
* |
|||
* @return int |
|||
*/ |
|||
public function getLifetime() |
|||
{ |
|||
return $this->meta[self::LIFETIME]; |
|||
} |
|||
|
|||
/** |
|||
* Stamps a new session's metadata. |
|||
* |
|||
* @param int $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 int Unix timestamp |
|||
*/ |
|||
public function getCreated() |
|||
{ |
|||
return $this->meta[self::CREATED]; |
|||
} |
|||
|
|||
/** |
|||
* Gets the last used metadata. |
|||
* |
|||
* @return int 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; |
|||
} |
|||
} |
|||
@ -0,0 +1,252 @@ |
|||
<?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; |
|||
|
|||
/** |
|||
* MockArraySessionStorage mocks the session for unit tests. |
|||
* |
|||
* No PHP session is actually started since a session can be initialized |
|||
* and shutdown only once per PHP execution cycle. |
|||
* |
|||
* When doing functional testing, you should use MockFileSessionStorage instead. |
|||
* |
|||
* @author Fabien Potencier <fabien@symfony.com> |
|||
* @author Bulat Shakirzyanov <mallluhuct@gmail.com> |
|||
* @author Drak <drak@zikula.org> |
|||
*/ |
|||
class MockArraySessionStorage implements SessionStorageInterface |
|||
{ |
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $id = ''; |
|||
|
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $name; |
|||
|
|||
/** |
|||
* @var bool |
|||
*/ |
|||
protected $started = false; |
|||
|
|||
/** |
|||
* @var bool |
|||
*/ |
|||
protected $closed = false; |
|||
|
|||
/** |
|||
* @var array |
|||
*/ |
|||
protected $data = []; |
|||
|
|||
/** |
|||
* @var MetadataBag |
|||
*/ |
|||
protected $metadataBag; |
|||
|
|||
/** |
|||
* @var array|SessionBagInterface[] |
|||
*/ |
|||
protected $bags = []; |
|||
|
|||
public function __construct(string $name = 'MOCKSESSID', MetadataBag $metaBag = null) |
|||
{ |
|||
$this->name = $name; |
|||
$this->setMetadataBag($metaBag); |
|||
} |
|||
|
|||
public function setSessionData(array $array) |
|||
{ |
|||
$this->data = $array; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function start() |
|||
{ |
|||
if ($this->started) { |
|||
return true; |
|||
} |
|||
|
|||
if (empty($this->id)) { |
|||
$this->id = $this->generateId(); |
|||
} |
|||
|
|||
$this->loadSession(); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function regenerate($destroy = false, $lifetime = null) |
|||
{ |
|||
if (!$this->started) { |
|||
$this->start(); |
|||
} |
|||
|
|||
$this->metadataBag->stampNew($lifetime); |
|||
$this->id = $this->generateId(); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getId() |
|||
{ |
|||
return $this->id; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function setId($id) |
|||
{ |
|||
if ($this->started) { |
|||
throw new \LogicException('Cannot set session ID after the session has started.'); |
|||
} |
|||
|
|||
$this->id = $id; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getName() |
|||
{ |
|||
return $this->name; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function setName($name) |
|||
{ |
|||
$this->name = $name; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
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; |
|||
$this->started = false; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function clear() |
|||
{ |
|||
// clear out the bags |
|||
foreach ($this->bags as $bag) { |
|||
$bag->clear(); |
|||
} |
|||
|
|||
// clear out the session |
|||
$this->data = []; |
|||
|
|||
// reconnect the bags to the session |
|||
$this->loadSession(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function registerBag(SessionBagInterface $bag) |
|||
{ |
|||
$this->bags[$bag->getName()] = $bag; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getBag($name) |
|||
{ |
|||
if (!isset($this->bags[$name])) { |
|||
throw new \InvalidArgumentException(sprintf('The SessionBagInterface %s is not registered.', $name)); |
|||
} |
|||
|
|||
if (!$this->started) { |
|||
$this->start(); |
|||
} |
|||
|
|||
return $this->bags[$name]; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function isStarted() |
|||
{ |
|||
return $this->started; |
|||
} |
|||
|
|||
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. |
|||
* |
|||
* This doesn't need to be particularly cryptographically secure since this is just |
|||
* a mock. |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function generateId() |
|||
{ |
|||
return hash('sha256', uniqid('ss_mock_', true)); |
|||
} |
|||
|
|||
protected function loadSession() |
|||
{ |
|||
$bags = array_merge($this->bags, [$this->metadataBag]); |
|||
|
|||
foreach ($bags as $bag) { |
|||
$key = $bag->getStorageKey(); |
|||
$this->data[$key] = isset($this->data[$key]) ? $this->data[$key] : []; |
|||
$bag->initialize($this->data[$key]); |
|||
} |
|||
|
|||
$this->started = true; |
|||
$this->closed = false; |
|||
} |
|||
} |
|||
@ -0,0 +1,152 @@ |
|||
<?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; |
|||
|
|||
/** |
|||
* MockFileSessionStorage is used to mock sessions for |
|||
* functional testing when done in a single PHP process. |
|||
* |
|||
* No PHP session is actually started since a session can be initialized |
|||
* and shutdown only once per PHP execution cycle and this class does |
|||
* not pollute any session related globals, including session_*() functions |
|||
* or session.* PHP ini directives. |
|||
* |
|||
* @author Drak <drak@zikula.org> |
|||
*/ |
|||
class MockFileSessionStorage extends MockArraySessionStorage |
|||
{ |
|||
private $savePath; |
|||
|
|||
/** |
|||
* @param string $savePath Path of directory to save session files |
|||
* @param string $name Session name |
|||
* @param MetadataBag $metaBag MetadataBag instance |
|||
*/ |
|||
public function __construct(string $savePath = null, string $name = 'MOCKSESSID', MetadataBag $metaBag = null) |
|||
{ |
|||
if (null === $savePath) { |
|||
$savePath = sys_get_temp_dir(); |
|||
} |
|||
|
|||
if (!is_dir($savePath) && !@mkdir($savePath, 0777, true) && !is_dir($savePath)) { |
|||
throw new \RuntimeException(sprintf('Session Storage was not able to create directory "%s"', $savePath)); |
|||
} |
|||
|
|||
$this->savePath = $savePath; |
|||
|
|||
parent::__construct($name, $metaBag); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function start() |
|||
{ |
|||
if ($this->started) { |
|||
return true; |
|||
} |
|||
|
|||
if (!$this->id) { |
|||
$this->id = $this->generateId(); |
|||
} |
|||
|
|||
$this->read(); |
|||
|
|||
$this->started = true; |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function regenerate($destroy = false, $lifetime = null) |
|||
{ |
|||
if (!$this->started) { |
|||
$this->start(); |
|||
} |
|||
|
|||
if ($destroy) { |
|||
$this->destroy(); |
|||
} |
|||
|
|||
return parent::regenerate($destroy, $lifetime); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function save() |
|||
{ |
|||
if (!$this->started) { |
|||
throw new \RuntimeException('Trying to save a session that was not started yet or was already closed'); |
|||
} |
|||
|
|||
$data = $this->data; |
|||
|
|||
foreach ($this->bags as $bag) { |
|||
if (empty($data[$key = $bag->getStorageKey()])) { |
|||
unset($data[$key]); |
|||
} |
|||
} |
|||
if ([$key = $this->metadataBag->getStorageKey()] === array_keys($data)) { |
|||
unset($data[$key]); |
|||
} |
|||
|
|||
try { |
|||
if ($data) { |
|||
file_put_contents($this->getFilePath(), serialize($data)); |
|||
} else { |
|||
$this->destroy(); |
|||
} |
|||
} finally { |
|||
$this->data = $data; |
|||
} |
|||
|
|||
// this is needed for Silex, where the session object is re-used across requests |
|||
// in functional tests. In Symfony, the container is rebooted, so we don't have |
|||
// this issue |
|||
$this->started = false; |
|||
} |
|||
|
|||
/** |
|||
* Deletes a session from persistent storage. |
|||
* Deliberately leaves session data in memory intact. |
|||
*/ |
|||
private function destroy() |
|||
{ |
|||
if (is_file($this->getFilePath())) { |
|||
unlink($this->getFilePath()); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Calculate path to file. |
|||
* |
|||
* @return string File path |
|||
*/ |
|||
private function getFilePath() |
|||
{ |
|||
return $this->savePath.'/'.$this->id.'.mocksess'; |
|||
} |
|||
|
|||
/** |
|||
* Reads session from storage and loads session. |
|||
*/ |
|||
private function read() |
|||
{ |
|||
$filePath = $this->getFilePath(); |
|||
$this->data = is_readable($filePath) && is_file($filePath) ? unserialize(file_get_contents($filePath)) : []; |
|||
|
|||
$this->loadSession(); |
|||
} |
|||
} |
|||
@ -0,0 +1,468 @@ |
|||
<?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; |
|||
use Symfony\Component\HttpFoundation\Session\SessionUtils; |
|||
use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler; |
|||
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy; |
|||
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy; |
|||
|
|||
/** |
|||
* This provides a base class for session attribute storage. |
|||
* |
|||
* @author Drak <drak@zikula.org> |
|||
*/ |
|||
class NativeSessionStorage implements SessionStorageInterface |
|||
{ |
|||
/** |
|||
* @var SessionBagInterface[] |
|||
*/ |
|||
protected $bags = []; |
|||
|
|||
/** |
|||
* @var bool |
|||
*/ |
|||
protected $started = false; |
|||
|
|||
/** |
|||
* @var bool |
|||
*/ |
|||
protected $closed = false; |
|||
|
|||
/** |
|||
* @var AbstractProxy|\SessionHandlerInterface |
|||
*/ |
|||
protected $saveHandler; |
|||
|
|||
/** |
|||
* @var MetadataBag |
|||
*/ |
|||
protected $metadataBag; |
|||
|
|||
/** |
|||
* @var string|null |
|||
*/ |
|||
private $emulateSameSite; |
|||
|
|||
/** |
|||
* Depending on how you want the storage driver to behave you probably |
|||
* want to override this constructor entirely. |
|||
* |
|||
* List of options for $options array with their defaults. |
|||
* |
|||
* @see https://php.net/session.configuration for options |
|||
* but we omit 'session.' from the beginning of the keys for convenience. |
|||
* |
|||
* ("auto_start", is not supported as it tells PHP to start a session before |
|||
* PHP starts to execute user-land code. Setting during runtime has no effect). |
|||
* |
|||
* cache_limiter, "" (use "0" to prevent headers from being sent entirely). |
|||
* cache_expire, "0" |
|||
* cookie_domain, "" |
|||
* cookie_httponly, "" |
|||
* cookie_lifetime, "0" |
|||
* cookie_path, "/" |
|||
* cookie_secure, "" |
|||
* cookie_samesite, null |
|||
* gc_divisor, "100" |
|||
* gc_maxlifetime, "1440" |
|||
* gc_probability, "1" |
|||
* lazy_write, "1" |
|||
* name, "PHPSESSID" |
|||
* referer_check, "" |
|||
* serialize_handler, "php" |
|||
* use_strict_mode, "0" |
|||
* use_cookies, "1" |
|||
* use_only_cookies, "1" |
|||
* use_trans_sid, "0" |
|||
* upload_progress.enabled, "1" |
|||
* upload_progress.cleanup, "1" |
|||
* upload_progress.prefix, "upload_progress_" |
|||
* upload_progress.name, "PHP_SESSION_UPLOAD_PROGRESS" |
|||
* upload_progress.freq, "1%" |
|||
* upload_progress.min-freq, "1" |
|||
* url_rewriter.tags, "a=href,area=href,frame=src,form=,fieldset=" |
|||
* sid_length, "32" |
|||
* sid_bits_per_character, "5" |
|||
* trans_sid_hosts, $_SERVER['HTTP_HOST'] |
|||
* trans_sid_tags, "a=href,area=href,frame=src,form=" |
|||
* |
|||
* @param array $options Session configuration options |
|||
* @param \SessionHandlerInterface|null $handler |
|||
* @param MetadataBag $metaBag MetadataBag |
|||
*/ |
|||
public function __construct(array $options = [], $handler = null, MetadataBag $metaBag = null) |
|||
{ |
|||
if (!\extension_loaded('session')) { |
|||
throw new \LogicException('PHP extension "session" is required.'); |
|||
} |
|||
|
|||
$options += [ |
|||
'cache_limiter' => '', |
|||
'cache_expire' => 0, |
|||
'use_cookies' => 1, |
|||
'lazy_write' => 1, |
|||
'use_strict_mode' => 1, |
|||
]; |
|||
|
|||
session_register_shutdown(); |
|||
|
|||
$this->setMetadataBag($metaBag); |
|||
$this->setOptions($options); |
|||
$this->setSaveHandler($handler); |
|||
} |
|||
|
|||
/** |
|||
* Gets the save handler instance. |
|||
* |
|||
* @return AbstractProxy|\SessionHandlerInterface |
|||
*/ |
|||
public function getSaveHandler() |
|||
{ |
|||
return $this->saveHandler; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function start() |
|||
{ |
|||
if ($this->started) { |
|||
return true; |
|||
} |
|||
|
|||
if (\PHP_SESSION_ACTIVE === session_status()) { |
|||
throw new \RuntimeException('Failed to start the session: already started by PHP.'); |
|||
} |
|||
|
|||
if (filter_var(ini_get('session.use_cookies'), FILTER_VALIDATE_BOOLEAN) && headers_sent($file, $line)) { |
|||
throw new \RuntimeException(sprintf('Failed to start the session because headers have already been sent by "%s" at line %d.', $file, $line)); |
|||
} |
|||
|
|||
// ok to try and start the session |
|||
if (!session_start()) { |
|||
throw new \RuntimeException('Failed to start the session'); |
|||
} |
|||
|
|||
if (null !== $this->emulateSameSite) { |
|||
$originalCookie = SessionUtils::popSessionCookie(session_name(), session_id()); |
|||
if (null !== $originalCookie) { |
|||
header(sprintf('%s; SameSite=%s', $originalCookie, $this->emulateSameSite), false); |
|||
} |
|||
} |
|||
|
|||
$this->loadSession(); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getId() |
|||
{ |
|||
return $this->saveHandler->getId(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function setId($id) |
|||
{ |
|||
$this->saveHandler->setId($id); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getName() |
|||
{ |
|||
return $this->saveHandler->getName(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function setName($name) |
|||
{ |
|||
$this->saveHandler->setName($name); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function regenerate($destroy = false, $lifetime = null) |
|||
{ |
|||
// Cannot regenerate the session ID for non-active sessions. |
|||
if (\PHP_SESSION_ACTIVE !== session_status()) { |
|||
return false; |
|||
} |
|||
|
|||
if (headers_sent()) { |
|||
return false; |
|||
} |
|||
|
|||
if (null !== $lifetime) { |
|||
ini_set('session.cookie_lifetime', $lifetime); |
|||
} |
|||
|
|||
if ($destroy) { |
|||
$this->metadataBag->stampNew(); |
|||
} |
|||
|
|||
$isRegenerated = session_regenerate_id($destroy); |
|||
|
|||
// The reference to $_SESSION in session bags is lost in PHP7 and we need to re-create it. |
|||
// @see https://bugs.php.net/70013 |
|||
$this->loadSession(); |
|||
|
|||
if (null !== $this->emulateSameSite) { |
|||
$originalCookie = SessionUtils::popSessionCookie(session_name(), session_id()); |
|||
if (null !== $originalCookie) { |
|||
header(sprintf('%s; SameSite=%s', $originalCookie, $this->emulateSameSite), false); |
|||
} |
|||
} |
|||
|
|||
return $isRegenerated; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function save() |
|||
{ |
|||
// Store a copy so we can restore the bags in case the session was not left empty |
|||
$session = $_SESSION; |
|||
|
|||
foreach ($this->bags as $bag) { |
|||
if (empty($_SESSION[$key = $bag->getStorageKey()])) { |
|||
unset($_SESSION[$key]); |
|||
} |
|||
} |
|||
if ([$key = $this->metadataBag->getStorageKey()] === array_keys($_SESSION)) { |
|||
unset($_SESSION[$key]); |
|||
} |
|||
|
|||
// Register error handler to add information about the current save handler |
|||
$previousHandler = set_error_handler(function ($type, $msg, $file, $line) use (&$previousHandler) { |
|||
if (E_WARNING === $type && 0 === strpos($msg, 'session_write_close():')) { |
|||
$handler = $this->saveHandler instanceof SessionHandlerProxy ? $this->saveHandler->getHandler() : $this->saveHandler; |
|||
$msg = sprintf('session_write_close(): Failed to write session data with "%s" handler', \get_class($handler)); |
|||
} |
|||
|
|||
return $previousHandler ? $previousHandler($type, $msg, $file, $line) : false; |
|||
}); |
|||
|
|||
try { |
|||
session_write_close(); |
|||
} finally { |
|||
restore_error_handler(); |
|||
|
|||
// Restore only if not empty |
|||
if ($_SESSION) { |
|||
$_SESSION = $session; |
|||
} |
|||
} |
|||
|
|||
$this->closed = true; |
|||
$this->started = false; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function clear() |
|||
{ |
|||
// clear out the bags |
|||
foreach ($this->bags as $bag) { |
|||
$bag->clear(); |
|||
} |
|||
|
|||
// clear out the session |
|||
$_SESSION = []; |
|||
|
|||
// reconnect the bags to the session |
|||
$this->loadSession(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function registerBag(SessionBagInterface $bag) |
|||
{ |
|||
if ($this->started) { |
|||
throw new \LogicException('Cannot register a bag when the session is already started.'); |
|||
} |
|||
|
|||
$this->bags[$bag->getName()] = $bag; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getBag($name) |
|||
{ |
|||
if (!isset($this->bags[$name])) { |
|||
throw new \InvalidArgumentException(sprintf('The SessionBagInterface %s is not registered.', $name)); |
|||
} |
|||
|
|||
if (!$this->started && $this->saveHandler->isActive()) { |
|||
$this->loadSession(); |
|||
} elseif (!$this->started) { |
|||
$this->start(); |
|||
} |
|||
|
|||
return $this->bags[$name]; |
|||
} |
|||
|
|||
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. |
|||
* |
|||
* For convenience we omit 'session.' from the beginning of the keys. |
|||
* Explicitly ignores other ini keys. |
|||
* |
|||
* @param array $options Session ini directives [key => value] |
|||
* |
|||
* @see https://php.net/session.configuration |
|||
*/ |
|||
public function setOptions(array $options) |
|||
{ |
|||
if (headers_sent() || \PHP_SESSION_ACTIVE === session_status()) { |
|||
return; |
|||
} |
|||
|
|||
$validOptions = array_flip([ |
|||
'cache_expire', 'cache_limiter', 'cookie_domain', 'cookie_httponly', |
|||
'cookie_lifetime', 'cookie_path', 'cookie_secure', 'cookie_samesite', |
|||
'gc_divisor', 'gc_maxlifetime', 'gc_probability', |
|||
'lazy_write', 'name', 'referer_check', |
|||
'serialize_handler', 'use_strict_mode', 'use_cookies', |
|||
'use_only_cookies', 'use_trans_sid', 'upload_progress.enabled', |
|||
'upload_progress.cleanup', 'upload_progress.prefix', 'upload_progress.name', |
|||
'upload_progress.freq', 'upload_progress.min_freq', 'url_rewriter.tags', |
|||
'sid_length', 'sid_bits_per_character', 'trans_sid_hosts', 'trans_sid_tags', |
|||
]); |
|||
|
|||
foreach ($options as $key => $value) { |
|||
if (isset($validOptions[$key])) { |
|||
if ('cookie_samesite' === $key && \PHP_VERSION_ID < 70300) { |
|||
// PHP < 7.3 does not support same_site cookies. We will emulate it in |
|||
// the start() method instead. |
|||
$this->emulateSameSite = $value; |
|||
continue; |
|||
} |
|||
ini_set('url_rewriter.tags' !== $key ? 'session.'.$key : $key, $value); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Registers session save handler as a PHP session handler. |
|||
* |
|||
* To use internal PHP session save handlers, override this method using ini_set with |
|||
* session.save_handler and session.save_path e.g. |
|||
* |
|||
* ini_set('session.save_handler', 'files'); |
|||
* ini_set('session.save_path', '/tmp'); |
|||
* |
|||
* or pass in a \SessionHandler instance which configures session.save_handler in the |
|||
* constructor, for a template see NativeFileSessionHandler or use handlers in |
|||
* composer package drak/native-session |
|||
* |
|||
* @see https://php.net/session-set-save-handler |
|||
* @see https://php.net/sessionhandlerinterface |
|||
* @see https://php.net/sessionhandler |
|||
* @see https://github.com/zikula/NativeSession |
|||
* |
|||
* @param \SessionHandlerInterface|null $saveHandler |
|||
* |
|||
* @throws \InvalidArgumentException |
|||
*/ |
|||
public function setSaveHandler($saveHandler = null) |
|||
{ |
|||
if (!$saveHandler instanceof AbstractProxy && |
|||
!$saveHandler instanceof \SessionHandlerInterface && |
|||
null !== $saveHandler) { |
|||
throw new \InvalidArgumentException('Must be instance of AbstractProxy; implement \SessionHandlerInterface; or be null.'); |
|||
} |
|||
|
|||
// Wrap $saveHandler in proxy and prevent double wrapping of proxy |
|||
if (!$saveHandler instanceof AbstractProxy && $saveHandler instanceof \SessionHandlerInterface) { |
|||
$saveHandler = new SessionHandlerProxy($saveHandler); |
|||
} elseif (!$saveHandler instanceof AbstractProxy) { |
|||
$saveHandler = new SessionHandlerProxy(new StrictSessionHandler(new \SessionHandler())); |
|||
} |
|||
$this->saveHandler = $saveHandler; |
|||
|
|||
if (headers_sent() || \PHP_SESSION_ACTIVE === session_status()) { |
|||
return; |
|||
} |
|||
|
|||
if ($this->saveHandler instanceof SessionHandlerProxy) { |
|||
session_set_save_handler($this->saveHandler, false); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Load the session with attributes. |
|||
* |
|||
* After starting the session, PHP retrieves the session from whatever handlers |
|||
* are set to (either PHP's internal, or a custom save handler set with session_set_save_handler()). |
|||
* PHP takes the return value from the read() handler, unserializes it |
|||
* and populates $_SESSION with the result automatically. |
|||
*/ |
|||
protected function loadSession(array &$session = null) |
|||
{ |
|||
if (null === $session) { |
|||
$session = &$_SESSION; |
|||
} |
|||
|
|||
$bags = array_merge($this->bags, [$this->metadataBag]); |
|||
|
|||
foreach ($bags as $bag) { |
|||
$key = $bag->getStorageKey(); |
|||
$session[$key] = isset($session[$key]) ? $session[$key] : []; |
|||
$bag->initialize($session[$key]); |
|||
} |
|||
|
|||
$this->started = true; |
|||
$this->closed = false; |
|||
} |
|||
} |
|||
@ -0,0 +1,63 @@ |
|||
<?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; |
|||
|
|||
/** |
|||
* Allows session to be started by PHP and managed by Symfony. |
|||
* |
|||
* @author Drak <drak@zikula.org> |
|||
*/ |
|||
class PhpBridgeSessionStorage extends NativeSessionStorage |
|||
{ |
|||
/** |
|||
* @param \SessionHandlerInterface|null $handler |
|||
* @param MetadataBag $metaBag MetadataBag |
|||
*/ |
|||
public function __construct($handler = null, MetadataBag $metaBag = null) |
|||
{ |
|||
if (!\extension_loaded('session')) { |
|||
throw new \LogicException('PHP extension "session" is required.'); |
|||
} |
|||
|
|||
$this->setMetadataBag($metaBag); |
|||
$this->setSaveHandler($handler); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function start() |
|||
{ |
|||
if ($this->started) { |
|||
return true; |
|||
} |
|||
|
|||
$this->loadSession(); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function clear() |
|||
{ |
|||
// clear out the bags and nothing else that may be set |
|||
// since the purpose of this driver is to share a handler |
|||
foreach ($this->bags as $bag) { |
|||
$bag->clear(); |
|||
} |
|||
|
|||
// reconnect the bags to the session |
|||
$this->loadSession(); |
|||
} |
|||
} |
|||
@ -0,0 +1,122 @@ |
|||
<?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\Proxy; |
|||
|
|||
/** |
|||
* @author Drak <drak@zikula.org> |
|||
*/ |
|||
abstract class AbstractProxy |
|||
{ |
|||
/** |
|||
* Flag if handler wraps an internal PHP session handler (using \SessionHandler). |
|||
* |
|||
* @var bool |
|||
*/ |
|||
protected $wrapper = false; |
|||
|
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $saveHandlerName; |
|||
|
|||
/** |
|||
* Gets the session.save_handler name. |
|||
* |
|||
* @return string|null |
|||
*/ |
|||
public function getSaveHandlerName() |
|||
{ |
|||
return $this->saveHandlerName; |
|||
} |
|||
|
|||
/** |
|||
* Is this proxy handler and instance of \SessionHandlerInterface. |
|||
* |
|||
* @return bool |
|||
*/ |
|||
public function isSessionHandlerInterface() |
|||
{ |
|||
return $this instanceof \SessionHandlerInterface; |
|||
} |
|||
|
|||
/** |
|||
* Returns true if this handler wraps an internal PHP session save handler using \SessionHandler. |
|||
* |
|||
* @return bool |
|||
*/ |
|||
public function isWrapper() |
|||
{ |
|||
return $this->wrapper; |
|||
} |
|||
|
|||
/** |
|||
* Has a session started? |
|||
* |
|||
* @return bool |
|||
*/ |
|||
public function isActive() |
|||
{ |
|||
return \PHP_SESSION_ACTIVE === session_status(); |
|||
} |
|||
|
|||
/** |
|||
* Gets the session ID. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getId() |
|||
{ |
|||
return session_id(); |
|||
} |
|||
|
|||
/** |
|||
* Sets the session ID. |
|||
* |
|||
* @param string $id |
|||
* |
|||
* @throws \LogicException |
|||
*/ |
|||
public function setId($id) |
|||
{ |
|||
if ($this->isActive()) { |
|||
throw new \LogicException('Cannot change the ID of an active session'); |
|||
} |
|||
|
|||
session_id($id); |
|||
} |
|||
|
|||
/** |
|||
* Gets the session name. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getName() |
|||
{ |
|||
return session_name(); |
|||
} |
|||
|
|||
/** |
|||
* Sets the session name. |
|||
* |
|||
* @param string $name |
|||
* |
|||
* @throws \LogicException |
|||
*/ |
|||
public function setName($name) |
|||
{ |
|||
if ($this->isActive()) { |
|||
throw new \LogicException('Cannot change the name of an active session'); |
|||
} |
|||
|
|||
session_name($name); |
|||
} |
|||
} |
|||
@ -0,0 +1,101 @@ |
|||
<?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\Proxy; |
|||
|
|||
/** |
|||
* @author Drak <drak@zikula.org> |
|||
*/ |
|||
class SessionHandlerProxy extends AbstractProxy implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface |
|||
{ |
|||
protected $handler; |
|||
|
|||
public function __construct(\SessionHandlerInterface $handler) |
|||
{ |
|||
$this->handler = $handler; |
|||
$this->wrapper = ($handler instanceof \SessionHandler); |
|||
$this->saveHandlerName = $this->wrapper ? ini_get('session.save_handler') : 'user'; |
|||
} |
|||
|
|||
/** |
|||
* @return \SessionHandlerInterface |
|||
*/ |
|||
public function getHandler() |
|||
{ |
|||
return $this->handler; |
|||
} |
|||
|
|||
// \SessionHandlerInterface |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function open($savePath, $sessionName) |
|||
{ |
|||
return (bool) $this->handler->open($savePath, $sessionName); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function close() |
|||
{ |
|||
return (bool) $this->handler->close(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function read($sessionId) |
|||
{ |
|||
return (string) $this->handler->read($sessionId); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function write($sessionId, $data) |
|||
{ |
|||
return (bool) $this->handler->write($sessionId, $data); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function destroy($sessionId) |
|||
{ |
|||
return (bool) $this->handler->destroy($sessionId); |
|||
} |
|||
|
|||
/** |
|||
* @return bool |
|||
*/ |
|||
public function gc($maxlifetime) |
|||
{ |
|||
return (bool) $this->handler->gc($maxlifetime); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function validateId($sessionId) |
|||
{ |
|||
return !$this->handler instanceof \SessionUpdateTimestampHandlerInterface || $this->handler->validateId($sessionId); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function updateTimestamp($sessionId, $data) |
|||
{ |
|||
return $this->handler instanceof \SessionUpdateTimestampHandlerInterface ? $this->handler->updateTimestamp($sessionId, $data) : $this->write($sessionId, $data); |
|||
} |
|||
} |
|||
@ -0,0 +1,137 @@ |
|||
<?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; |
|||
|
|||
/** |
|||
* StorageInterface. |
|||
* |
|||
* @author Fabien Potencier <fabien@symfony.com> |
|||
* @author Drak <drak@zikula.org> |
|||
*/ |
|||
interface SessionStorageInterface |
|||
{ |
|||
/** |
|||
* Starts the session. |
|||
* |
|||
* @return bool True if started |
|||
* |
|||
* @throws \RuntimeException if something goes wrong starting the session |
|||
*/ |
|||
public function start(); |
|||
|
|||
/** |
|||
* Checks if the session is started. |
|||
* |
|||
* @return bool True if started, false otherwise |
|||
*/ |
|||
public function isStarted(); |
|||
|
|||
/** |
|||
* Returns the session ID. |
|||
* |
|||
* @return string The session ID or empty |
|||
*/ |
|||
public function getId(); |
|||
|
|||
/** |
|||
* Sets the session ID. |
|||
* |
|||
* @param string $id |
|||
*/ |
|||
public function setId($id); |
|||
|
|||
/** |
|||
* Returns the session name. |
|||
* |
|||
* @return mixed The session name |
|||
*/ |
|||
public function getName(); |
|||
|
|||
/** |
|||
* Sets the session name. |
|||
* |
|||
* @param string $name |
|||
*/ |
|||
public function setName($name); |
|||
|
|||
/** |
|||
* Regenerates id that represents this storage. |
|||
* |
|||
* This method must invoke session_regenerate_id($destroy) unless |
|||
* this interface is used for a storage object designed for unit |
|||
* or functional testing where a real PHP session would interfere |
|||
* with testing. |
|||
* |
|||
* Note regenerate+destroy should not clear the session data in memory |
|||
* only delete the session data from persistent storage. |
|||
* |
|||
* Care: When regenerating the session ID no locking is involved in PHP's |
|||
* session design. See https://bugs.php.net/61470 for a discussion. |
|||
* So you must make sure the regenerated session is saved BEFORE sending the |
|||
* headers with the new ID. Symfony's HttpKernel offers a listener for this. |
|||
* See Symfony\Component\HttpKernel\EventListener\SaveSessionListener. |
|||
* Otherwise session data could get lost again for concurrent requests with the |
|||
* new ID. One result could be that you get logged out after just logging in. |
|||
* |
|||
* @param bool $destroy Destroy session when regenerating? |
|||
* @param int $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 bool True if session regenerated, false if error |
|||
* |
|||
* @throws \RuntimeException If an error occurs while regenerating this storage |
|||
*/ |
|||
public function regenerate($destroy = false, $lifetime = null); |
|||
|
|||
/** |
|||
* Force the session to be saved and closed. |
|||
* |
|||
* This method must invoke session_write_close() unless this interface is |
|||
* used for a storage object design for unit or functional testing where |
|||
* a real PHP session would interfere with testing, in which case |
|||
* it should actually persist the session data if required. |
|||
* |
|||
* @throws \RuntimeException if the session is saved without being started, or if the session |
|||
* is already closed |
|||
*/ |
|||
public function save(); |
|||
|
|||
/** |
|||
* Clear all session data in memory. |
|||
*/ |
|||
public function clear(); |
|||
|
|||
/** |
|||
* Gets a SessionBagInterface by name. |
|||
* |
|||
* @param string $name |
|||
* |
|||
* @return SessionBagInterface |
|||
* |
|||
* @throws \InvalidArgumentException If the bag does not exist |
|||
*/ |
|||
public function getBag($name); |
|||
|
|||
/** |
|||
* Registers a SessionBagInterface for use. |
|||
*/ |
|||
public function registerBag(SessionBagInterface $bag); |
|||
|
|||
/** |
|||
* @return MetadataBag |
|||
*/ |
|||
public function getMetadataBag(); |
|||
} |
|||
@ -0,0 +1,144 @@ |
|||
<?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; |
|||
|
|||
/** |
|||
* StreamedResponse represents a streamed HTTP response. |
|||
* |
|||
* A StreamedResponse uses a callback for its content. |
|||
* |
|||
* The callback should use the standard PHP functions like echo |
|||
* to stream the response back to the client. The flush() function |
|||
* can also be used if needed. |
|||
* |
|||
* @see flush() |
|||
* |
|||
* @author Fabien Potencier <fabien@symfony.com> |
|||
*/ |
|||
class StreamedResponse extends Response |
|||
{ |
|||
protected $callback; |
|||
protected $streamed; |
|||
private $headersSent; |
|||
|
|||
/** |
|||
* @param callable|null $callback A valid PHP callback or null to set it later |
|||
* @param int $status The response status code |
|||
* @param array $headers An array of response headers |
|||
*/ |
|||
public function __construct(callable $callback = null, int $status = 200, array $headers = []) |
|||
{ |
|||
parent::__construct(null, $status, $headers); |
|||
|
|||
if (null !== $callback) { |
|||
$this->setCallback($callback); |
|||
} |
|||
$this->streamed = false; |
|||
$this->headersSent = false; |
|||
} |
|||
|
|||
/** |
|||
* Factory method for chainability. |
|||
* |
|||
* @param callable|null $callback A valid PHP callback or null to set it later |
|||
* @param int $status The response status code |
|||
* @param array $headers An array of response headers |
|||
* |
|||
* @return static |
|||
*/ |
|||
public static function create($callback = null, $status = 200, $headers = []) |
|||
{ |
|||
return new static($callback, $status, $headers); |
|||
} |
|||
|
|||
/** |
|||
* Sets the PHP callback associated with this Response. |
|||
* |
|||
* @param callable $callback A valid PHP callback |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function setCallback(callable $callback) |
|||
{ |
|||
$this->callback = $callback; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
* |
|||
* This method only sends the headers once. |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function sendHeaders() |
|||
{ |
|||
if ($this->headersSent) { |
|||
return $this; |
|||
} |
|||
|
|||
$this->headersSent = true; |
|||
|
|||
return parent::sendHeaders(); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
* |
|||
* This method only sends the content once. |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function sendContent() |
|||
{ |
|||
if ($this->streamed) { |
|||
return $this; |
|||
} |
|||
|
|||
$this->streamed = true; |
|||
|
|||
if (null === $this->callback) { |
|||
throw new \LogicException('The Response callback must not be null.'); |
|||
} |
|||
|
|||
($this->callback)(); |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
* |
|||
* @throws \LogicException when the content is not null |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function setContent($content) |
|||
{ |
|||
if (null !== $content) { |
|||
throw new \LogicException('The content cannot be set on a StreamedResponse instance.'); |
|||
} |
|||
|
|||
$this->streamed = true; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getContent() |
|||
{ |
|||
return false; |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\Test\Constraint; |
|||
|
|||
use PHPUnit\Framework\Constraint\Constraint; |
|||
|
|||
final class RequestAttributeValueSame extends Constraint |
|||
{ |
|||
private $name; |
|||
private $value; |
|||
|
|||
public function __construct(string $name, string $value) |
|||
{ |
|||
$this->name = $name; |
|||
$this->value = $value; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function toString(): string |
|||
{ |
|||
return sprintf('has attribute "%s" with value "%s"', $this->name, $this->value); |
|||
} |
|||
|
|||
/** |
|||
* @param Request $request |
|||
* |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function matches($request): bool |
|||
{ |
|||
return $this->value === $request->attributes->get($this->name); |
|||
} |
|||
|
|||
/** |
|||
* @param Request $request |
|||
* |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function failureDescription($request): string |
|||
{ |
|||
return 'the Request '.$this->toString(); |
|||
} |
|||
} |
|||
@ -0,0 +1,85 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\Test\Constraint; |
|||
|
|||
use PHPUnit\Framework\Constraint\Constraint; |
|||
use Symfony\Component\HttpFoundation\Cookie; |
|||
use Symfony\Component\HttpFoundation\Response; |
|||
|
|||
final class ResponseCookieValueSame extends Constraint |
|||
{ |
|||
private $name; |
|||
private $value; |
|||
private $path; |
|||
private $domain; |
|||
|
|||
public function __construct(string $name, string $value, string $path = '/', string $domain = null) |
|||
{ |
|||
$this->name = $name; |
|||
$this->value = $value; |
|||
$this->path = $path; |
|||
$this->domain = $domain; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function toString(): string |
|||
{ |
|||
$str = sprintf('has cookie "%s"', $this->name); |
|||
if ('/' !== $this->path) { |
|||
$str .= sprintf(' with path "%s"', $this->path); |
|||
} |
|||
if ($this->domain) { |
|||
$str .= sprintf(' for domain "%s"', $this->domain); |
|||
} |
|||
$str .= sprintf(' with value "%s"', $this->value); |
|||
|
|||
return $str; |
|||
} |
|||
|
|||
/** |
|||
* @param Response $response |
|||
* |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function matches($response): bool |
|||
{ |
|||
$cookie = $this->getCookie($response); |
|||
if (!$cookie) { |
|||
return false; |
|||
} |
|||
|
|||
return $this->value === $cookie->getValue(); |
|||
} |
|||
|
|||
/** |
|||
* @param Response $response |
|||
* |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function failureDescription($response): string |
|||
{ |
|||
return 'the Response '.$this->toString(); |
|||
} |
|||
|
|||
protected function getCookie(Response $response): ?Cookie |
|||
{ |
|||
$cookies = $response->headers->getCookies(); |
|||
|
|||
$filteredCookies = array_filter($cookies, function (Cookie $cookie) { |
|||
return $cookie->getName() === $this->name && $cookie->getPath() === $this->path && $cookie->getDomain() === $this->domain; |
|||
}); |
|||
|
|||
return reset($filteredCookies) ?: null; |
|||
} |
|||
} |
|||
@ -0,0 +1,77 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\Test\Constraint; |
|||
|
|||
use PHPUnit\Framework\Constraint\Constraint; |
|||
use Symfony\Component\HttpFoundation\Cookie; |
|||
use Symfony\Component\HttpFoundation\Response; |
|||
|
|||
final class ResponseHasCookie extends Constraint |
|||
{ |
|||
private $name; |
|||
private $path; |
|||
private $domain; |
|||
|
|||
public function __construct(string $name, string $path = '/', string $domain = null) |
|||
{ |
|||
$this->name = $name; |
|||
$this->path = $path; |
|||
$this->domain = $domain; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function toString(): string |
|||
{ |
|||
$str = sprintf('has cookie "%s"', $this->name); |
|||
if ('/' !== $this->path) { |
|||
$str .= sprintf(' with path "%s"', $this->path); |
|||
} |
|||
if ($this->domain) { |
|||
$str .= sprintf(' for domain "%s"', $this->domain); |
|||
} |
|||
|
|||
return $str; |
|||
} |
|||
|
|||
/** |
|||
* @param Response $response |
|||
* |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function matches($response): bool |
|||
{ |
|||
return null !== $this->getCookie($response); |
|||
} |
|||
|
|||
/** |
|||
* @param Response $response |
|||
* |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function failureDescription($response): string |
|||
{ |
|||
return 'the Response '.$this->toString(); |
|||
} |
|||
|
|||
protected function getCookie(Response $response): ?Cookie |
|||
{ |
|||
$cookies = $response->headers->getCookies(); |
|||
|
|||
$filteredCookies = array_filter($cookies, function (Cookie $cookie) { |
|||
return $cookie->getName() === $this->name && $cookie->getPath() === $this->path && $cookie->getDomain() === $this->domain; |
|||
}); |
|||
|
|||
return reset($filteredCookies) ?: null; |
|||
} |
|||
} |
|||
@ -0,0 +1,53 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\Test\Constraint; |
|||
|
|||
use PHPUnit\Framework\Constraint\Constraint; |
|||
use Symfony\Component\HttpFoundation\Response; |
|||
|
|||
final class ResponseHasHeader extends Constraint |
|||
{ |
|||
private $headerName; |
|||
|
|||
public function __construct(string $headerName) |
|||
{ |
|||
$this->headerName = $headerName; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function toString(): string |
|||
{ |
|||
return sprintf('has header "%s"', $this->headerName); |
|||
} |
|||
|
|||
/** |
|||
* @param Response $response |
|||
* |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function matches($response): bool |
|||
{ |
|||
return $response->headers->has($this->headerName); |
|||
} |
|||
|
|||
/** |
|||
* @param Response $response |
|||
* |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function failureDescription($response): string |
|||
{ |
|||
return 'the Response '.$this->toString(); |
|||
} |
|||
} |
|||
@ -0,0 +1,55 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\Test\Constraint; |
|||
|
|||
use PHPUnit\Framework\Constraint\Constraint; |
|||
use Symfony\Component\HttpFoundation\Response; |
|||
|
|||
final class ResponseHeaderSame extends Constraint |
|||
{ |
|||
private $headerName; |
|||
private $expectedValue; |
|||
|
|||
public function __construct(string $headerName, string $expectedValue) |
|||
{ |
|||
$this->headerName = $headerName; |
|||
$this->expectedValue = $expectedValue; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function toString(): string |
|||
{ |
|||
return sprintf('has header "%s" with value "%s"', $this->headerName, $this->expectedValue); |
|||
} |
|||
|
|||
/** |
|||
* @param Response $response |
|||
* |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function matches($response): bool |
|||
{ |
|||
return $this->expectedValue === $response->headers->get($this->headerName, null, true); |
|||
} |
|||
|
|||
/** |
|||
* @param Response $response |
|||
* |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function failureDescription($response): string |
|||
{ |
|||
return 'the Response '.$this->toString(); |
|||
} |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\Test\Constraint; |
|||
|
|||
use PHPUnit\Framework\Constraint\Constraint; |
|||
use Symfony\Component\HttpFoundation\Response; |
|||
|
|||
final class ResponseIsRedirected extends Constraint |
|||
{ |
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function toString(): string |
|||
{ |
|||
return 'is redirected'; |
|||
} |
|||
|
|||
/** |
|||
* @param Response $response |
|||
* |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function matches($response): bool |
|||
{ |
|||
return $response->isRedirect(); |
|||
} |
|||
|
|||
/** |
|||
* @param Response $response |
|||
* |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function failureDescription($response): string |
|||
{ |
|||
return 'the Response '.$this->toString(); |
|||
} |
|||
|
|||
/** |
|||
* @param Response $response |
|||
* |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function additionalFailureDescription($response): string |
|||
{ |
|||
return (string) $response; |
|||
} |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\Test\Constraint; |
|||
|
|||
use PHPUnit\Framework\Constraint\Constraint; |
|||
use Symfony\Component\HttpFoundation\Response; |
|||
|
|||
final class ResponseIsSuccessful extends Constraint |
|||
{ |
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function toString(): string |
|||
{ |
|||
return 'is successful'; |
|||
} |
|||
|
|||
/** |
|||
* @param Response $response |
|||
* |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function matches($response): bool |
|||
{ |
|||
return $response->isSuccessful(); |
|||
} |
|||
|
|||
/** |
|||
* @param Response $response |
|||
* |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function failureDescription($response): string |
|||
{ |
|||
return 'the Response '.$this->toString(); |
|||
} |
|||
|
|||
/** |
|||
* @param Response $response |
|||
* |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function additionalFailureDescription($response): string |
|||
{ |
|||
return (string) $response; |
|||
} |
|||
} |
|||
@ -0,0 +1,63 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\Test\Constraint; |
|||
|
|||
use PHPUnit\Framework\Constraint\Constraint; |
|||
use Symfony\Component\HttpFoundation\Response; |
|||
|
|||
final class ResponseStatusCodeSame extends Constraint |
|||
{ |
|||
private $statusCode; |
|||
|
|||
public function __construct(int $statusCode) |
|||
{ |
|||
$this->statusCode = $statusCode; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function toString(): string |
|||
{ |
|||
return 'status code is '.$this->statusCode; |
|||
} |
|||
|
|||
/** |
|||
* @param Response $response |
|||
* |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function matches($response): bool |
|||
{ |
|||
return $this->statusCode === $response->getStatusCode(); |
|||
} |
|||
|
|||
/** |
|||
* @param Response $response |
|||
* |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function failureDescription($response): string |
|||
{ |
|||
return 'the Response '.$this->toString(); |
|||
} |
|||
|
|||
/** |
|||
* @param Response $response |
|||
* |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function additionalFailureDescription($response): string |
|||
{ |
|||
return (string) $response; |
|||
} |
|||
} |
|||
@ -0,0 +1,113 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\Tests; |
|||
|
|||
use PHPUnit\Framework\TestCase; |
|||
use Symfony\Component\HttpFoundation\AcceptHeaderItem; |
|||
|
|||
class AcceptHeaderItemTest extends TestCase |
|||
{ |
|||
/** |
|||
* @dataProvider provideFromStringData |
|||
*/ |
|||
public function testFromString($string, $value, array $attributes) |
|||
{ |
|||
$item = AcceptHeaderItem::fromString($string); |
|||
$this->assertEquals($value, $item->getValue()); |
|||
$this->assertEquals($attributes, $item->getAttributes()); |
|||
} |
|||
|
|||
public function provideFromStringData() |
|||
{ |
|||
return [ |
|||
[ |
|||
'text/html', |
|||
'text/html', [], |
|||
], |
|||
[ |
|||
'"this;should,not=matter"', |
|||
'this;should,not=matter', [], |
|||
], |
|||
[ |
|||
"text/plain; charset=utf-8;param=\"this;should,not=matter\";\tfootnotes=true", |
|||
'text/plain', ['charset' => 'utf-8', 'param' => 'this;should,not=matter', 'footnotes' => 'true'], |
|||
], |
|||
[ |
|||
'"this;should,not=matter";charset=utf-8', |
|||
'this;should,not=matter', ['charset' => 'utf-8'], |
|||
], |
|||
]; |
|||
} |
|||
|
|||
/** |
|||
* @dataProvider provideToStringData |
|||
*/ |
|||
public function testToString($value, array $attributes, $string) |
|||
{ |
|||
$item = new AcceptHeaderItem($value, $attributes); |
|||
$this->assertEquals($string, (string) $item); |
|||
} |
|||
|
|||
public function provideToStringData() |
|||
{ |
|||
return [ |
|||
[ |
|||
'text/html', [], |
|||
'text/html', |
|||
], |
|||
[ |
|||
'text/plain', ['charset' => 'utf-8', 'param' => 'this;should,not=matter', 'footnotes' => 'true'], |
|||
'text/plain; charset=utf-8; param="this;should,not=matter"; footnotes=true', |
|||
], |
|||
]; |
|||
} |
|||
|
|||
public function testValue() |
|||
{ |
|||
$item = new AcceptHeaderItem('value', []); |
|||
$this->assertEquals('value', $item->getValue()); |
|||
|
|||
$item->setValue('new value'); |
|||
$this->assertEquals('new value', $item->getValue()); |
|||
|
|||
$item->setValue(1); |
|||
$this->assertEquals('1', $item->getValue()); |
|||
} |
|||
|
|||
public function testQuality() |
|||
{ |
|||
$item = new AcceptHeaderItem('value', []); |
|||
$this->assertEquals(1.0, $item->getQuality()); |
|||
|
|||
$item->setQuality(0.5); |
|||
$this->assertEquals(0.5, $item->getQuality()); |
|||
|
|||
$item->setAttribute('q', 0.75); |
|||
$this->assertEquals(0.75, $item->getQuality()); |
|||
$this->assertFalse($item->hasAttribute('q')); |
|||
} |
|||
|
|||
public function testAttribute() |
|||
{ |
|||
$item = new AcceptHeaderItem('value', []); |
|||
$this->assertEquals([], $item->getAttributes()); |
|||
$this->assertFalse($item->hasAttribute('test')); |
|||
$this->assertNull($item->getAttribute('test')); |
|||
$this->assertEquals('default', $item->getAttribute('test', 'default')); |
|||
|
|||
$item->setAttribute('test', 'value'); |
|||
$this->assertEquals(['test' => 'value'], $item->getAttributes()); |
|||
$this->assertTrue($item->hasAttribute('test')); |
|||
$this->assertEquals('value', $item->getAttribute('test')); |
|||
$this->assertEquals('value', $item->getAttribute('test', 'default')); |
|||
} |
|||
} |
|||
@ -0,0 +1,130 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\Tests; |
|||
|
|||
use PHPUnit\Framework\TestCase; |
|||
use Symfony\Component\HttpFoundation\AcceptHeader; |
|||
use Symfony\Component\HttpFoundation\AcceptHeaderItem; |
|||
|
|||
class AcceptHeaderTest extends TestCase |
|||
{ |
|||
public function testFirst() |
|||
{ |
|||
$header = AcceptHeader::fromString('text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c'); |
|||
$this->assertSame('text/html', $header->first()->getValue()); |
|||
} |
|||
|
|||
/** |
|||
* @dataProvider provideFromStringData |
|||
*/ |
|||
public function testFromString($string, array $items) |
|||
{ |
|||
$header = AcceptHeader::fromString($string); |
|||
$parsed = array_values($header->all()); |
|||
// reset index since the fixtures don't have them set |
|||
foreach ($parsed as $item) { |
|||
$item->setIndex(0); |
|||
} |
|||
$this->assertEquals($items, $parsed); |
|||
} |
|||
|
|||
public function provideFromStringData() |
|||
{ |
|||
return [ |
|||
['', []], |
|||
['gzip', [new AcceptHeaderItem('gzip')]], |
|||
['gzip,deflate,sdch', [new AcceptHeaderItem('gzip'), new AcceptHeaderItem('deflate'), new AcceptHeaderItem('sdch')]], |
|||
["gzip, deflate\t,sdch", [new AcceptHeaderItem('gzip'), new AcceptHeaderItem('deflate'), new AcceptHeaderItem('sdch')]], |
|||
['"this;should,not=matter"', [new AcceptHeaderItem('this;should,not=matter')]], |
|||
]; |
|||
} |
|||
|
|||
/** |
|||
* @dataProvider provideToStringData |
|||
*/ |
|||
public function testToString(array $items, $string) |
|||
{ |
|||
$header = new AcceptHeader($items); |
|||
$this->assertEquals($string, (string) $header); |
|||
} |
|||
|
|||
public function provideToStringData() |
|||
{ |
|||
return [ |
|||
[[], ''], |
|||
[[new AcceptHeaderItem('gzip')], 'gzip'], |
|||
[[new AcceptHeaderItem('gzip'), new AcceptHeaderItem('deflate'), new AcceptHeaderItem('sdch')], 'gzip,deflate,sdch'], |
|||
[[new AcceptHeaderItem('this;should,not=matter')], 'this;should,not=matter'], |
|||
]; |
|||
} |
|||
|
|||
/** |
|||
* @dataProvider provideFilterData |
|||
*/ |
|||
public function testFilter($string, $filter, array $values) |
|||
{ |
|||
$header = AcceptHeader::fromString($string)->filter($filter); |
|||
$this->assertEquals($values, array_keys($header->all())); |
|||
} |
|||
|
|||
public function provideFilterData() |
|||
{ |
|||
return [ |
|||
['fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4', '/fr.*/', ['fr-FR', 'fr']], |
|||
]; |
|||
} |
|||
|
|||
/** |
|||
* @dataProvider provideSortingData |
|||
*/ |
|||
public function testSorting($string, array $values) |
|||
{ |
|||
$header = AcceptHeader::fromString($string); |
|||
$this->assertEquals($values, array_keys($header->all())); |
|||
} |
|||
|
|||
public function provideSortingData() |
|||
{ |
|||
return [ |
|||
'quality has priority' => ['*;q=0.3,ISO-8859-1,utf-8;q=0.7', ['ISO-8859-1', 'utf-8', '*']], |
|||
'order matters when q is equal' => ['*;q=0.3,ISO-8859-1;q=0.7,utf-8;q=0.7', ['ISO-8859-1', 'utf-8', '*']], |
|||
'order matters when q is equal2' => ['*;q=0.3,utf-8;q=0.7,ISO-8859-1;q=0.7', ['utf-8', 'ISO-8859-1', '*']], |
|||
]; |
|||
} |
|||
|
|||
/** |
|||
* @dataProvider provideDefaultValueData |
|||
*/ |
|||
public function testDefaultValue($acceptHeader, $value, $expectedQuality) |
|||
{ |
|||
$header = AcceptHeader::fromString($acceptHeader); |
|||
$this->assertSame($expectedQuality, $header->get($value)->getQuality()); |
|||
} |
|||
|
|||
public function provideDefaultValueData() |
|||
{ |
|||
yield ['text/plain;q=0.5, text/html, text/x-dvi;q=0.8, *;q=0.3', 'text/xml', 0.3]; |
|||
yield ['text/plain;q=0.5, text/html, text/x-dvi;q=0.8, */*;q=0.3', 'text/xml', 0.3]; |
|||
yield ['text/plain;q=0.5, text/html, text/x-dvi;q=0.8, */*;q=0.3', 'text/html', 1.0]; |
|||
yield ['text/plain;q=0.5, text/html, text/x-dvi;q=0.8, */*;q=0.3', 'text/plain', 0.5]; |
|||
yield ['text/plain;q=0.5, text/html, text/x-dvi;q=0.8, */*;q=0.3', '*', 0.3]; |
|||
yield ['text/plain;q=0.5, text/html, text/x-dvi;q=0.8, */*', '*', 1.0]; |
|||
yield ['text/plain;q=0.5, text/html, text/x-dvi;q=0.8, */*', 'text/xml', 1.0]; |
|||
yield ['text/plain;q=0.5, text/html, text/x-dvi;q=0.8, */*', 'text/*', 1.0]; |
|||
yield ['text/plain;q=0.5, text/html, text/*;q=0.8, */*', 'text/*', 0.8]; |
|||
yield ['text/plain;q=0.5, text/html, text/*;q=0.8, */*', 'text/html', 1.0]; |
|||
yield ['text/plain;q=0.5, text/html, text/*;q=0.8, */*', 'text/x-dvi', 0.8]; |
|||
yield ['*;q=0.3, ISO-8859-1;q=0.7, utf-8;q=0.7', '*', 0.3]; |
|||
yield ['*;q=0.3, ISO-8859-1;q=0.7, utf-8;q=0.7', 'utf-8', 0.7]; |
|||
yield ['*;q=0.3, ISO-8859-1;q=0.7, utf-8;q=0.7', 'SHIFT_JIS', 0.3]; |
|||
} |
|||
} |
|||
@ -0,0 +1,93 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\Tests; |
|||
|
|||
use PHPUnit\Framework\TestCase; |
|||
use Symfony\Component\HttpFoundation\ApacheRequest; |
|||
|
|||
class ApacheRequestTest extends TestCase |
|||
{ |
|||
/** |
|||
* @dataProvider provideServerVars |
|||
*/ |
|||
public function testUriMethods($server, $expectedRequestUri, $expectedBaseUrl, $expectedPathInfo) |
|||
{ |
|||
$request = new ApacheRequest(); |
|||
$request->server->replace($server); |
|||
|
|||
$this->assertEquals($expectedRequestUri, $request->getRequestUri(), '->getRequestUri() is correct'); |
|||
$this->assertEquals($expectedBaseUrl, $request->getBaseUrl(), '->getBaseUrl() is correct'); |
|||
$this->assertEquals($expectedPathInfo, $request->getPathInfo(), '->getPathInfo() is correct'); |
|||
} |
|||
|
|||
public function provideServerVars() |
|||
{ |
|||
return [ |
|||
[ |
|||
[ |
|||
'REQUEST_URI' => '/foo/app_dev.php/bar', |
|||
'SCRIPT_NAME' => '/foo/app_dev.php', |
|||
'PATH_INFO' => '/bar', |
|||
], |
|||
'/foo/app_dev.php/bar', |
|||
'/foo/app_dev.php', |
|||
'/bar', |
|||
], |
|||
[ |
|||
[ |
|||
'REQUEST_URI' => '/foo/bar', |
|||
'SCRIPT_NAME' => '/foo/app_dev.php', |
|||
], |
|||
'/foo/bar', |
|||
'/foo', |
|||
'/bar', |
|||
], |
|||
[ |
|||
[ |
|||
'REQUEST_URI' => '/app_dev.php/foo/bar', |
|||
'SCRIPT_NAME' => '/app_dev.php', |
|||
'PATH_INFO' => '/foo/bar', |
|||
], |
|||
'/app_dev.php/foo/bar', |
|||
'/app_dev.php', |
|||
'/foo/bar', |
|||
], |
|||
[ |
|||
[ |
|||
'REQUEST_URI' => '/foo/bar', |
|||
'SCRIPT_NAME' => '/app_dev.php', |
|||
], |
|||
'/foo/bar', |
|||
'', |
|||
'/foo/bar', |
|||
], |
|||
[ |
|||
[ |
|||
'REQUEST_URI' => '/app_dev.php', |
|||
'SCRIPT_NAME' => '/app_dev.php', |
|||
], |
|||
'/app_dev.php', |
|||
'/app_dev.php', |
|||
'/', |
|||
], |
|||
[ |
|||
[ |
|||
'REQUEST_URI' => '/', |
|||
'SCRIPT_NAME' => '/app_dev.php', |
|||
], |
|||
'/', |
|||
'', |
|||
'/', |
|||
], |
|||
]; |
|||
} |
|||
} |
|||
@ -0,0 +1,365 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\Tests; |
|||
|
|||
use Symfony\Component\HttpFoundation\BinaryFileResponse; |
|||
use Symfony\Component\HttpFoundation\File\Stream; |
|||
use Symfony\Component\HttpFoundation\Request; |
|||
use Symfony\Component\HttpFoundation\ResponseHeaderBag; |
|||
use Symfony\Component\HttpFoundation\Tests\File\FakeFile; |
|||
|
|||
class BinaryFileResponseTest extends ResponseTestCase |
|||
{ |
|||
public function testConstruction() |
|||
{ |
|||
$file = __DIR__.'/../README.md'; |
|||
$response = new BinaryFileResponse($file, 404, ['X-Header' => 'Foo'], true, null, true, true); |
|||
$this->assertEquals(404, $response->getStatusCode()); |
|||
$this->assertEquals('Foo', $response->headers->get('X-Header')); |
|||
$this->assertTrue($response->headers->has('ETag')); |
|||
$this->assertTrue($response->headers->has('Last-Modified')); |
|||
$this->assertFalse($response->headers->has('Content-Disposition')); |
|||
|
|||
$response = BinaryFileResponse::create($file, 404, [], true, ResponseHeaderBag::DISPOSITION_INLINE); |
|||
$this->assertEquals(404, $response->getStatusCode()); |
|||
$this->assertFalse($response->headers->has('ETag')); |
|||
$this->assertEquals('inline; filename=README.md', $response->headers->get('Content-Disposition')); |
|||
} |
|||
|
|||
public function testConstructWithNonAsciiFilename() |
|||
{ |
|||
touch(sys_get_temp_dir().'/fööö.html'); |
|||
|
|||
$response = new BinaryFileResponse(sys_get_temp_dir().'/fööö.html', 200, [], true, 'attachment'); |
|||
|
|||
@unlink(sys_get_temp_dir().'/fööö.html'); |
|||
|
|||
$this->assertSame('fööö.html', $response->getFile()->getFilename()); |
|||
} |
|||
|
|||
public function testSetContent() |
|||
{ |
|||
$this->expectException('LogicException'); |
|||
$response = new BinaryFileResponse(__FILE__); |
|||
$response->setContent('foo'); |
|||
} |
|||
|
|||
public function testGetContent() |
|||
{ |
|||
$response = new BinaryFileResponse(__FILE__); |
|||
$this->assertFalse($response->getContent()); |
|||
} |
|||
|
|||
public function testSetContentDispositionGeneratesSafeFallbackFilename() |
|||
{ |
|||
$response = new BinaryFileResponse(__FILE__); |
|||
$response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, 'föö.html'); |
|||
|
|||
$this->assertSame('attachment; filename=f__.html; filename*=utf-8\'\'f%C3%B6%C3%B6.html', $response->headers->get('Content-Disposition')); |
|||
} |
|||
|
|||
public function testSetContentDispositionGeneratesSafeFallbackFilenameForWronglyEncodedFilename() |
|||
{ |
|||
$response = new BinaryFileResponse(__FILE__); |
|||
|
|||
$iso88591EncodedFilename = utf8_decode('föö.html'); |
|||
$response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $iso88591EncodedFilename); |
|||
|
|||
// the parameter filename* is invalid in this case (rawurldecode('f%F6%F6') does not provide a UTF-8 string but an ISO-8859-1 encoded one) |
|||
$this->assertSame('attachment; filename=f__.html; filename*=utf-8\'\'f%F6%F6.html', $response->headers->get('Content-Disposition')); |
|||
} |
|||
|
|||
/** |
|||
* @dataProvider provideRanges |
|||
*/ |
|||
public function testRequests($requestRange, $offset, $length, $responseRange) |
|||
{ |
|||
$response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream'])->setAutoEtag(); |
|||
|
|||
// do a request to get the ETag |
|||
$request = Request::create('/'); |
|||
$response->prepare($request); |
|||
$etag = $response->headers->get('ETag'); |
|||
|
|||
// prepare a request for a range of the testing file |
|||
$request = Request::create('/'); |
|||
$request->headers->set('If-Range', $etag); |
|||
$request->headers->set('Range', $requestRange); |
|||
|
|||
$file = fopen(__DIR__.'/File/Fixtures/test.gif', 'r'); |
|||
fseek($file, $offset); |
|||
$data = fread($file, $length); |
|||
fclose($file); |
|||
|
|||
$this->expectOutputString($data); |
|||
$response = clone $response; |
|||
$response->prepare($request); |
|||
$response->sendContent(); |
|||
|
|||
$this->assertEquals(206, $response->getStatusCode()); |
|||
$this->assertEquals($responseRange, $response->headers->get('Content-Range')); |
|||
$this->assertSame((string) $length, $response->headers->get('Content-Length')); |
|||
} |
|||
|
|||
/** |
|||
* @dataProvider provideRanges |
|||
*/ |
|||
public function testRequestsWithoutEtag($requestRange, $offset, $length, $responseRange) |
|||
{ |
|||
$response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream']); |
|||
|
|||
// do a request to get the LastModified |
|||
$request = Request::create('/'); |
|||
$response->prepare($request); |
|||
$lastModified = $response->headers->get('Last-Modified'); |
|||
|
|||
// prepare a request for a range of the testing file |
|||
$request = Request::create('/'); |
|||
$request->headers->set('If-Range', $lastModified); |
|||
$request->headers->set('Range', $requestRange); |
|||
|
|||
$file = fopen(__DIR__.'/File/Fixtures/test.gif', 'r'); |
|||
fseek($file, $offset); |
|||
$data = fread($file, $length); |
|||
fclose($file); |
|||
|
|||
$this->expectOutputString($data); |
|||
$response = clone $response; |
|||
$response->prepare($request); |
|||
$response->sendContent(); |
|||
|
|||
$this->assertEquals(206, $response->getStatusCode()); |
|||
$this->assertEquals($responseRange, $response->headers->get('Content-Range')); |
|||
} |
|||
|
|||
public function provideRanges() |
|||
{ |
|||
return [ |
|||
['bytes=1-4', 1, 4, 'bytes 1-4/35'], |
|||
['bytes=-5', 30, 5, 'bytes 30-34/35'], |
|||
['bytes=30-', 30, 5, 'bytes 30-34/35'], |
|||
['bytes=30-30', 30, 1, 'bytes 30-30/35'], |
|||
['bytes=30-34', 30, 5, 'bytes 30-34/35'], |
|||
]; |
|||
} |
|||
|
|||
public function testRangeRequestsWithoutLastModifiedDate() |
|||
{ |
|||
// prevent auto last modified |
|||
$response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream'], true, null, false, false); |
|||
|
|||
// prepare a request for a range of the testing file |
|||
$request = Request::create('/'); |
|||
$request->headers->set('If-Range', date('D, d M Y H:i:s').' GMT'); |
|||
$request->headers->set('Range', 'bytes=1-4'); |
|||
|
|||
$this->expectOutputString(file_get_contents(__DIR__.'/File/Fixtures/test.gif')); |
|||
$response = clone $response; |
|||
$response->prepare($request); |
|||
$response->sendContent(); |
|||
|
|||
$this->assertEquals(200, $response->getStatusCode()); |
|||
$this->assertNull($response->headers->get('Content-Range')); |
|||
} |
|||
|
|||
/** |
|||
* @dataProvider provideFullFileRanges |
|||
*/ |
|||
public function testFullFileRequests($requestRange) |
|||
{ |
|||
$response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream'])->setAutoEtag(); |
|||
|
|||
// prepare a request for a range of the testing file |
|||
$request = Request::create('/'); |
|||
$request->headers->set('Range', $requestRange); |
|||
|
|||
$file = fopen(__DIR__.'/File/Fixtures/test.gif', 'r'); |
|||
$data = fread($file, 35); |
|||
fclose($file); |
|||
|
|||
$this->expectOutputString($data); |
|||
$response = clone $response; |
|||
$response->prepare($request); |
|||
$response->sendContent(); |
|||
|
|||
$this->assertEquals(200, $response->getStatusCode()); |
|||
} |
|||
|
|||
public function provideFullFileRanges() |
|||
{ |
|||
return [ |
|||
['bytes=0-'], |
|||
['bytes=0-34'], |
|||
['bytes=-35'], |
|||
// Syntactical invalid range-request should also return the full resource |
|||
['bytes=20-10'], |
|||
['bytes=50-40'], |
|||
]; |
|||
} |
|||
|
|||
public function testUnpreparedResponseSendsFullFile() |
|||
{ |
|||
$response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200); |
|||
|
|||
$data = file_get_contents(__DIR__.'/File/Fixtures/test.gif'); |
|||
|
|||
$this->expectOutputString($data); |
|||
$response = clone $response; |
|||
$response->sendContent(); |
|||
|
|||
$this->assertEquals(200, $response->getStatusCode()); |
|||
} |
|||
|
|||
/** |
|||
* @dataProvider provideInvalidRanges |
|||
*/ |
|||
public function testInvalidRequests($requestRange) |
|||
{ |
|||
$response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream'])->setAutoEtag(); |
|||
|
|||
// prepare a request for a range of the testing file |
|||
$request = Request::create('/'); |
|||
$request->headers->set('Range', $requestRange); |
|||
|
|||
$response = clone $response; |
|||
$response->prepare($request); |
|||
$response->sendContent(); |
|||
|
|||
$this->assertEquals(416, $response->getStatusCode()); |
|||
$this->assertEquals('bytes */35', $response->headers->get('Content-Range')); |
|||
} |
|||
|
|||
public function provideInvalidRanges() |
|||
{ |
|||
return [ |
|||
['bytes=-40'], |
|||
['bytes=30-40'], |
|||
]; |
|||
} |
|||
|
|||
/** |
|||
* @dataProvider provideXSendfileFiles |
|||
*/ |
|||
public function testXSendfile($file) |
|||
{ |
|||
$request = Request::create('/'); |
|||
$request->headers->set('X-Sendfile-Type', 'X-Sendfile'); |
|||
|
|||
BinaryFileResponse::trustXSendfileTypeHeader(); |
|||
$response = BinaryFileResponse::create($file, 200, ['Content-Type' => 'application/octet-stream']); |
|||
$response->prepare($request); |
|||
|
|||
$this->expectOutputString(''); |
|||
$response->sendContent(); |
|||
|
|||
$this->assertStringContainsString('README.md', $response->headers->get('X-Sendfile')); |
|||
} |
|||
|
|||
public function provideXSendfileFiles() |
|||
{ |
|||
return [ |
|||
[__DIR__.'/../README.md'], |
|||
['file://'.__DIR__.'/../README.md'], |
|||
]; |
|||
} |
|||
|
|||
/** |
|||
* @dataProvider getSampleXAccelMappings |
|||
*/ |
|||
public function testXAccelMapping($realpath, $mapping, $virtual) |
|||
{ |
|||
$request = Request::create('/'); |
|||
$request->headers->set('X-Sendfile-Type', 'X-Accel-Redirect'); |
|||
$request->headers->set('X-Accel-Mapping', $mapping); |
|||
|
|||
$file = new FakeFile($realpath, __DIR__.'/File/Fixtures/test'); |
|||
|
|||
BinaryFileResponse::trustXSendfileTypeHeader(); |
|||
$response = new BinaryFileResponse($file, 200, ['Content-Type' => 'application/octet-stream']); |
|||
$reflection = new \ReflectionObject($response); |
|||
$property = $reflection->getProperty('file'); |
|||
$property->setAccessible(true); |
|||
$property->setValue($response, $file); |
|||
|
|||
$response->prepare($request); |
|||
$this->assertEquals($virtual, $response->headers->get('X-Accel-Redirect')); |
|||
} |
|||
|
|||
public function testDeleteFileAfterSend() |
|||
{ |
|||
$request = Request::create('/'); |
|||
|
|||
$path = __DIR__.'/File/Fixtures/to_delete'; |
|||
touch($path); |
|||
$realPath = realpath($path); |
|||
$this->assertFileExists($realPath); |
|||
|
|||
$response = new BinaryFileResponse($realPath, 200, ['Content-Type' => 'application/octet-stream']); |
|||
$response->deleteFileAfterSend(true); |
|||
|
|||
$response->prepare($request); |
|||
$response->sendContent(); |
|||
|
|||
$this->assertFileNotExists($path); |
|||
} |
|||
|
|||
public function testAcceptRangeOnUnsafeMethods() |
|||
{ |
|||
$request = Request::create('/', 'POST'); |
|||
$response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream']); |
|||
$response->prepare($request); |
|||
|
|||
$this->assertEquals('none', $response->headers->get('Accept-Ranges')); |
|||
} |
|||
|
|||
public function testAcceptRangeNotOverriden() |
|||
{ |
|||
$request = Request::create('/', 'POST'); |
|||
$response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream']); |
|||
$response->headers->set('Accept-Ranges', 'foo'); |
|||
$response->prepare($request); |
|||
|
|||
$this->assertEquals('foo', $response->headers->get('Accept-Ranges')); |
|||
} |
|||
|
|||
public function getSampleXAccelMappings() |
|||
{ |
|||
return [ |
|||
['/var/www/var/www/files/foo.txt', '/var/www/=/files/', '/files/var/www/files/foo.txt'], |
|||
['/home/Foo/bar.txt', '/var/www/=/files/,/home/Foo/=/baz/', '/baz/bar.txt'], |
|||
['/home/Foo/bar.txt', '"/var/www/"="/files/", "/home/Foo/"="/baz/"', '/baz/bar.txt'], |
|||
['/tmp/bar.txt', '"/var/www/"="/files/", "/home/Foo/"="/baz/"', null], |
|||
]; |
|||
} |
|||
|
|||
public function testStream() |
|||
{ |
|||
$request = Request::create('/'); |
|||
$response = new BinaryFileResponse(new Stream(__DIR__.'/../README.md'), 200, ['Content-Type' => 'text/plain']); |
|||
$response->prepare($request); |
|||
|
|||
$this->assertNull($response->headers->get('Content-Length')); |
|||
} |
|||
|
|||
protected function provideResponse() |
|||
{ |
|||
return new BinaryFileResponse(__DIR__.'/../README.md', 200, ['Content-Type' => 'application/octet-stream']); |
|||
} |
|||
|
|||
public static function tearDownAfterClass(): void |
|||
{ |
|||
$path = __DIR__.'/../Fixtures/to_delete'; |
|||
if (file_exists($path)) { |
|||
@unlink($path); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,251 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\Tests; |
|||
|
|||
use PHPUnit\Framework\TestCase; |
|||
use Symfony\Component\HttpFoundation\Cookie; |
|||
|
|||
/** |
|||
* CookieTest. |
|||
* |
|||
* @author John Kary <john@johnkary.net> |
|||
* @author Hugo Hamon <hugo.hamon@sensio.com> |
|||
* |
|||
* @group time-sensitive |
|||
*/ |
|||
class CookieTest extends TestCase |
|||
{ |
|||
public function invalidNames() |
|||
{ |
|||
return [ |
|||
[''], |
|||
[',MyName'], |
|||
[';MyName'], |
|||
[' MyName'], |
|||
["\tMyName"], |
|||
["\rMyName"], |
|||
["\nMyName"], |
|||
["\013MyName"], |
|||
["\014MyName"], |
|||
]; |
|||
} |
|||
|
|||
/** |
|||
* @dataProvider invalidNames |
|||
*/ |
|||
public function testInstantiationThrowsExceptionIfCookieNameContainsInvalidCharacters($name) |
|||
{ |
|||
$this->expectException('InvalidArgumentException'); |
|||
Cookie::create($name); |
|||
} |
|||
|
|||
public function testInvalidExpiration() |
|||
{ |
|||
$this->expectException('InvalidArgumentException'); |
|||
Cookie::create('MyCookie', 'foo', 'bar'); |
|||
} |
|||
|
|||
public function testNegativeExpirationIsNotPossible() |
|||
{ |
|||
$cookie = Cookie::create('foo', 'bar', -100); |
|||
|
|||
$this->assertSame(0, $cookie->getExpiresTime()); |
|||
} |
|||
|
|||
public function testGetValue() |
|||
{ |
|||
$value = 'MyValue'; |
|||
$cookie = Cookie::create('MyCookie', $value); |
|||
|
|||
$this->assertSame($value, $cookie->getValue(), '->getValue() returns the proper value'); |
|||
} |
|||
|
|||
public function testGetPath() |
|||
{ |
|||
$cookie = Cookie::create('foo', 'bar'); |
|||
|
|||
$this->assertSame('/', $cookie->getPath(), '->getPath() returns / as the default path'); |
|||
} |
|||
|
|||
public function testGetExpiresTime() |
|||
{ |
|||
$cookie = Cookie::create('foo', 'bar'); |
|||
|
|||
$this->assertEquals(0, $cookie->getExpiresTime(), '->getExpiresTime() returns the default expire date'); |
|||
|
|||
$cookie = Cookie::create('foo', 'bar', $expire = time() + 3600); |
|||
|
|||
$this->assertEquals($expire, $cookie->getExpiresTime(), '->getExpiresTime() returns the expire date'); |
|||
} |
|||
|
|||
public function testGetExpiresTimeIsCastToInt() |
|||
{ |
|||
$cookie = Cookie::create('foo', 'bar', 3600.9); |
|||
|
|||
$this->assertSame(3600, $cookie->getExpiresTime(), '->getExpiresTime() returns the expire date as an integer'); |
|||
} |
|||
|
|||
public function testConstructorWithDateTime() |
|||
{ |
|||
$expire = new \DateTime(); |
|||
$cookie = Cookie::create('foo', 'bar', $expire); |
|||
|
|||
$this->assertEquals($expire->format('U'), $cookie->getExpiresTime(), '->getExpiresTime() returns the expire date'); |
|||
} |
|||
|
|||
public function testConstructorWithDateTimeImmutable() |
|||
{ |
|||
$expire = new \DateTimeImmutable(); |
|||
$cookie = Cookie::create('foo', 'bar', $expire); |
|||
|
|||
$this->assertEquals($expire->format('U'), $cookie->getExpiresTime(), '->getExpiresTime() returns the expire date'); |
|||
} |
|||
|
|||
public function testGetExpiresTimeWithStringValue() |
|||
{ |
|||
$value = '+1 day'; |
|||
$cookie = Cookie::create('foo', 'bar', $value); |
|||
$expire = strtotime($value); |
|||
|
|||
$this->assertEqualsWithDelta($expire, $cookie->getExpiresTime(), 1, '->getExpiresTime() returns the expire date'); |
|||
} |
|||
|
|||
public function testGetDomain() |
|||
{ |
|||
$cookie = Cookie::create('foo', 'bar', 0, '/', '.myfoodomain.com'); |
|||
|
|||
$this->assertEquals('.myfoodomain.com', $cookie->getDomain(), '->getDomain() returns the domain name on which the cookie is valid'); |
|||
} |
|||
|
|||
public function testIsSecure() |
|||
{ |
|||
$cookie = Cookie::create('foo', 'bar', 0, '/', '.myfoodomain.com', true); |
|||
|
|||
$this->assertTrue($cookie->isSecure(), '->isSecure() returns whether the cookie is transmitted over HTTPS'); |
|||
} |
|||
|
|||
public function testIsHttpOnly() |
|||
{ |
|||
$cookie = Cookie::create('foo', 'bar', 0, '/', '.myfoodomain.com', false, true); |
|||
|
|||
$this->assertTrue($cookie->isHttpOnly(), '->isHttpOnly() returns whether the cookie is only transmitted over HTTP'); |
|||
} |
|||
|
|||
public function testCookieIsNotCleared() |
|||
{ |
|||
$cookie = Cookie::create('foo', 'bar', time() + 3600 * 24); |
|||
|
|||
$this->assertFalse($cookie->isCleared(), '->isCleared() returns false if the cookie did not expire yet'); |
|||
} |
|||
|
|||
public function testCookieIsCleared() |
|||
{ |
|||
$cookie = Cookie::create('foo', 'bar', time() - 20); |
|||
|
|||
$this->assertTrue($cookie->isCleared(), '->isCleared() returns true if the cookie has expired'); |
|||
|
|||
$cookie = Cookie::create('foo', 'bar'); |
|||
|
|||
$this->assertFalse($cookie->isCleared()); |
|||
|
|||
$cookie = Cookie::create('foo', 'bar'); |
|||
|
|||
$this->assertFalse($cookie->isCleared()); |
|||
|
|||
$cookie = Cookie::create('foo', 'bar', -1); |
|||
|
|||
$this->assertFalse($cookie->isCleared()); |
|||
} |
|||
|
|||
public function testToString() |
|||
{ |
|||
$cookie = Cookie::create('foo', 'bar', $expire = strtotime('Fri, 20-May-2011 15:25:52 GMT'), '/', '.myfoodomain.com', true, true, false, null); |
|||
$this->assertEquals('foo=bar; expires=Fri, 20-May-2011 15:25:52 GMT; Max-Age=0; path=/; domain=.myfoodomain.com; secure; httponly', (string) $cookie, '->__toString() returns string representation of the cookie'); |
|||
|
|||
$cookie = Cookie::create('foo', 'bar with white spaces', strtotime('Fri, 20-May-2011 15:25:52 GMT'), '/', '.myfoodomain.com', true, true, false, null); |
|||
$this->assertEquals('foo=bar%20with%20white%20spaces; expires=Fri, 20-May-2011 15:25:52 GMT; Max-Age=0; path=/; domain=.myfoodomain.com; secure; httponly', (string) $cookie, '->__toString() encodes the value of the cookie according to RFC 3986 (white space = %20)'); |
|||
|
|||
$cookie = Cookie::create('foo', null, 1, '/admin/', '.myfoodomain.com', false, true, false, null); |
|||
$this->assertEquals('foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', $expire = time() - 31536001).'; Max-Age=0; path=/admin/; domain=.myfoodomain.com; httponly', (string) $cookie, '->__toString() returns string representation of a cleared cookie if value is NULL'); |
|||
|
|||
$cookie = Cookie::create('foo', 'bar'); |
|||
$this->assertEquals('foo=bar; path=/; httponly; samesite=lax', (string) $cookie); |
|||
} |
|||
|
|||
public function testRawCookie() |
|||
{ |
|||
$cookie = Cookie::create('foo', 'b a r', 0, '/', null, false, false, false, null); |
|||
$this->assertFalse($cookie->isRaw()); |
|||
$this->assertEquals('foo=b%20a%20r; path=/', (string) $cookie); |
|||
|
|||
$cookie = Cookie::create('foo', 'b+a+r', 0, '/', null, false, false, true, null); |
|||
$this->assertTrue($cookie->isRaw()); |
|||
$this->assertEquals('foo=b+a+r; path=/', (string) $cookie); |
|||
} |
|||
|
|||
public function testGetMaxAge() |
|||
{ |
|||
$cookie = Cookie::create('foo', 'bar'); |
|||
$this->assertEquals(0, $cookie->getMaxAge()); |
|||
|
|||
$cookie = Cookie::create('foo', 'bar', $expire = time() + 100); |
|||
$this->assertEquals($expire - time(), $cookie->getMaxAge()); |
|||
|
|||
$cookie = Cookie::create('foo', 'bar', $expire = time() - 100); |
|||
$this->assertEquals(0, $cookie->getMaxAge()); |
|||
} |
|||
|
|||
public function testFromString() |
|||
{ |
|||
$cookie = Cookie::fromString('foo=bar; expires=Fri, 20-May-2011 15:25:52 GMT; path=/; domain=.myfoodomain.com; secure; httponly'); |
|||
$this->assertEquals(Cookie::create('foo', 'bar', strtotime('Fri, 20-May-2011 15:25:52 GMT'), '/', '.myfoodomain.com', true, true, true, null), $cookie); |
|||
|
|||
$cookie = Cookie::fromString('foo=bar', true); |
|||
$this->assertEquals(Cookie::create('foo', 'bar', 0, '/', null, false, false, false, null), $cookie); |
|||
|
|||
$cookie = Cookie::fromString('foo', true); |
|||
$this->assertEquals(Cookie::create('foo', null, 0, '/', null, false, false, false, null), $cookie); |
|||
} |
|||
|
|||
public function testFromStringWithHttpOnly() |
|||
{ |
|||
$cookie = Cookie::fromString('foo=bar; expires=Fri, 20-May-2011 15:25:52 GMT; path=/; domain=.myfoodomain.com; secure; httponly'); |
|||
$this->assertTrue($cookie->isHttpOnly()); |
|||
|
|||
$cookie = Cookie::fromString('foo=bar; expires=Fri, 20-May-2011 15:25:52 GMT; path=/; domain=.myfoodomain.com; secure'); |
|||
$this->assertFalse($cookie->isHttpOnly()); |
|||
} |
|||
|
|||
public function testSameSiteAttribute() |
|||
{ |
|||
$cookie = new Cookie('foo', 'bar', 0, '/', null, false, true, false, 'Lax'); |
|||
$this->assertEquals('lax', $cookie->getSameSite()); |
|||
|
|||
$cookie = new Cookie('foo', 'bar', 0, '/', null, false, true, false, ''); |
|||
$this->assertNull($cookie->getSameSite()); |
|||
} |
|||
|
|||
public function testSetSecureDefault() |
|||
{ |
|||
$cookie = Cookie::create('foo', 'bar'); |
|||
|
|||
$this->assertFalse($cookie->isSecure()); |
|||
|
|||
$cookie->setSecureDefault(true); |
|||
|
|||
$this->assertTrue($cookie->isSecure()); |
|||
|
|||
$cookie->setSecureDefault(false); |
|||
|
|||
$this->assertFalse($cookie->isSecure()); |
|||
} |
|||
} |
|||
@ -0,0 +1,67 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\Tests; |
|||
|
|||
use PHPUnit\Framework\TestCase; |
|||
use Symfony\Component\ExpressionLanguage\ExpressionLanguage; |
|||
use Symfony\Component\HttpFoundation\ExpressionRequestMatcher; |
|||
use Symfony\Component\HttpFoundation\Request; |
|||
|
|||
class ExpressionRequestMatcherTest extends TestCase |
|||
{ |
|||
public function testWhenNoExpressionIsSet() |
|||
{ |
|||
$this->expectException('LogicException'); |
|||
$expressionRequestMatcher = new ExpressionRequestMatcher(); |
|||
$expressionRequestMatcher->matches(new Request()); |
|||
} |
|||
|
|||
/** |
|||
* @dataProvider provideExpressions |
|||
*/ |
|||
public function testMatchesWhenParentMatchesIsTrue($expression, $expected) |
|||
{ |
|||
$request = Request::create('/foo'); |
|||
$expressionRequestMatcher = new ExpressionRequestMatcher(); |
|||
|
|||
$expressionRequestMatcher->setExpression(new ExpressionLanguage(), $expression); |
|||
$this->assertSame($expected, $expressionRequestMatcher->matches($request)); |
|||
} |
|||
|
|||
/** |
|||
* @dataProvider provideExpressions |
|||
*/ |
|||
public function testMatchesWhenParentMatchesIsFalse($expression) |
|||
{ |
|||
$request = Request::create('/foo'); |
|||
$request->attributes->set('foo', 'foo'); |
|||
$expressionRequestMatcher = new ExpressionRequestMatcher(); |
|||
$expressionRequestMatcher->matchAttribute('foo', 'bar'); |
|||
|
|||
$expressionRequestMatcher->setExpression(new ExpressionLanguage(), $expression); |
|||
$this->assertFalse($expressionRequestMatcher->matches($request)); |
|||
} |
|||
|
|||
public function provideExpressions() |
|||
{ |
|||
return [ |
|||
['request.getMethod() == method', true], |
|||
['request.getPathInfo() == path', true], |
|||
['request.getHost() == host', true], |
|||
['request.getClientIp() == ip', true], |
|||
['request.attributes.all() == attributes', true], |
|||
['request.getMethod() == method && request.getPathInfo() == path && request.getHost() == host && request.getClientIp() == ip && request.attributes.all() == attributes', true], |
|||
['request.getMethod() != method', false], |
|||
['request.getMethod() != method && request.getPathInfo() == path && request.getHost() == host && request.getClientIp() == ip && request.attributes.all() == attributes', false], |
|||
]; |
|||
} |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\Tests\File; |
|||
|
|||
use Symfony\Component\HttpFoundation\File\File as OrigFile; |
|||
|
|||
class FakeFile extends OrigFile |
|||
{ |
|||
private $realpath; |
|||
|
|||
public function __construct($realpath, $path) |
|||
{ |
|||
$this->realpath = $realpath; |
|||
parent::__construct($path, false); |
|||
} |
|||
|
|||
public function isReadable() |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
public function getRealpath() |
|||
{ |
|||
return $this->realpath; |
|||
} |
|||
|
|||
public function getSize() |
|||
{ |
|||
return 42; |
|||
} |
|||
|
|||
public function getMTime() |
|||
{ |
|||
return time(); |
|||
} |
|||
} |
|||
@ -0,0 +1,144 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the Symfony package. |
|||
* |
|||
* (c) Fabien Potencier <fabien@symfony.com> |
|||
* |
|||
* For the full copyright and license information, please view the LICENSE |
|||
* file that was distributed with this source code. |
|||
*/ |
|||
|
|||
namespace Symfony\Component\HttpFoundation\Tests\File; |
|||
|
|||
use PHPUnit\Framework\TestCase; |
|||
use Symfony\Component\HttpFoundation\File\File; |
|||
|
|||
/** |
|||
* @requires extension fileinfo |
|||
*/ |
|||
class FileTest extends TestCase |
|||
{ |
|||
protected $file; |
|||
|
|||
public function testGetMimeTypeUsesMimeTypeGuessers() |
|||
{ |
|||
$file = new File(__DIR__.'/Fixtures/test.gif'); |
|||
$this->assertEquals('image/gif', $file->getMimeType()); |
|||
} |
|||
|
|||
public function testGuessExtensionWithoutGuesser() |
|||
{ |
|||
$file = new File(__DIR__.'/Fixtures/directory/.empty'); |
|||
$this->assertNull($file->guessExtension()); |
|||
} |
|||
|
|||
public function testGuessExtensionIsBasedOnMimeType() |
|||
{ |
|||
$file = new File(__DIR__.'/Fixtures/test'); |
|||
$this->assertEquals('gif', $file->guessExtension()); |
|||
} |
|||
|
|||
public function testConstructWhenFileNotExists() |
|||
{ |
|||
$this->expectException('Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException'); |
|||
|
|||
new File(__DIR__.'/Fixtures/not_here'); |
|||
} |
|||
|
|||
public function testMove() |
|||
{ |
|||
$path = __DIR__.'/Fixtures/test.copy.gif'; |
|||
$targetDir = __DIR__.'/Fixtures/directory'; |
|||
$targetPath = $targetDir.'/test.copy.gif'; |
|||
@unlink($path); |
|||
@unlink($targetPath); |
|||
copy(__DIR__.'/Fixtures/test.gif', $path); |
|||
|
|||
$file = new File($path); |
|||
$movedFile = $file->move($targetDir); |
|||
$this->assertInstanceOf('Symfony\Component\HttpFoundation\File\File', $movedFile); |
|||
|
|||
$this->assertFileExists($targetPath); |
|||
$this->assertFileNotExists($path); |
|||
$this->assertEquals(realpath($targetPath), $movedFile->getRealPath()); |
|||
|
|||
@unlink($targetPath); |
|||
} |
|||
|
|||
public function testMoveWithNewName() |
|||
{ |
|||
$path = __DIR__.'/Fixtures/test.copy.gif'; |
|||
$targetDir = __DIR__.'/Fixtures/directory'; |
|||
$targetPath = $targetDir.'/test.newname.gif'; |
|||
@unlink($path); |
|||
@unlink($targetPath); |
|||
copy(__DIR__.'/Fixtures/test.gif', $path); |
|||
|
|||
$file = new File($path); |
|||
$movedFile = $file->move($targetDir, 'test.newname.gif'); |
|||
|
|||
$this->assertFileExists($targetPath); |
|||
$this->assertFileNotExists($path); |
|||
$this->assertEquals(realpath($targetPath), $movedFile->getRealPath()); |
|||
|
|||
@unlink($targetPath); |
|||
} |
|||
|
|||
public function getFilenameFixtures() |
|||
{ |
|||
return [ |
|||
['original.gif', 'original.gif'], |
|||
['..\\..\\original.gif', 'original.gif'], |
|||
['../../original.gif', 'original.gif'], |
|||
['файлfile.gif', 'файлfile.gif'], |
|||
['..\\..\\файлfile.gif', 'файлfile.gif'], |
|||
['../../файлfile.gif', 'файлfile.gif'], |
|||
]; |
|||
} |
|||
|
|||
/** |
|||
* @dataProvider getFilenameFixtures |
|||
*/ |
|||
public function testMoveWithNonLatinName($filename, $sanitizedFilename) |
|||
{ |
|||
$path = __DIR__.'/Fixtures/'.$sanitizedFilename; |
|||
$targetDir = __DIR__.'/Fixtures/directory/'; |
|||
$targetPath = $targetDir.$sanitizedFilename; |
|||
@unlink($path); |
|||
@unlink($targetPath); |
|||
copy(__DIR__.'/Fixtures/test.gif', $path); |
|||
|
|||
$file = new File($path); |
|||
$movedFile = $file->move($targetDir, $filename); |
|||
$this->assertInstanceOf('Symfony\Component\HttpFoundation\File\File', $movedFile); |
|||
|
|||
$this->assertFileExists($targetPath); |
|||
$this->assertFileNotExists($path); |
|||
$this->assertEquals(realpath($targetPath), $movedFile->getRealPath()); |
|||
|
|||
@unlink($targetPath); |
|||
} |
|||
|
|||
public function testMoveToAnUnexistentDirectory() |
|||
{ |
|||
$sourcePath = __DIR__.'/Fixtures/test.copy.gif'; |
|||
$targetDir = __DIR__.'/Fixtures/directory/sub'; |
|||
$targetPath = $targetDir.'/test.copy.gif'; |
|||
@unlink($sourcePath); |
|||
@unlink($targetPath); |
|||
@rmdir($targetDir); |
|||
copy(__DIR__.'/Fixtures/test.gif', $sourcePath); |
|||
|
|||
$file = new File($sourcePath); |
|||
$movedFile = $file->move($targetDir); |
|||
|
|||
$this->assertFileExists($targetPath); |
|||
$this->assertFileNotExists($sourcePath); |
|||
$this->assertEquals(realpath($targetPath), $movedFile->getRealPath()); |
|||
|
|||
@unlink($sourcePath); |
|||
@unlink($targetPath); |
|||
@rmdir($targetDir); |
|||
} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
f |
|||
Binary file not shown.
|
After Width: | Height: | Size: 35 B |
|
After Width: | Height: | Size: 35 B |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue