├── AcceptHeader.php ├── AcceptHeaderItem.php ├── BinaryFileResponse.php ├── CHANGELOG.md ├── ChainRequestMatcher.php ├── Cookie.php ├── EventStreamResponse.php ├── Exception ├── BadRequestException.php ├── ConflictingHeadersException.php ├── ExceptionInterface.php ├── ExpiredSignedUriException.php ├── JsonException.php ├── LogicException.php ├── RequestExceptionInterface.php ├── SessionNotFoundException.php ├── SignedUriException.php ├── SuspiciousOperationException.php ├── UnexpectedValueException.php ├── UnsignedUriException.php └── UnverifiedSignedUriException.php ├── File ├── Exception │ ├── AccessDeniedException.php │ ├── CannotWriteFileException.php │ ├── ExtensionFileException.php │ ├── FileException.php │ ├── FileNotFoundException.php │ ├── FormSizeFileException.php │ ├── IniSizeFileException.php │ ├── NoFileException.php │ ├── NoTmpDirFileException.php │ ├── PartialFileException.php │ ├── UnexpectedTypeException.php │ └── UploadException.php ├── File.php ├── Stream.php └── UploadedFile.php ├── FileBag.php ├── HeaderBag.php ├── HeaderUtils.php ├── InputBag.php ├── IpUtils.php ├── JsonResponse.php ├── LICENSE ├── ParameterBag.php ├── README.md ├── RateLimiter ├── AbstractRequestRateLimiter.php ├── PeekableRequestRateLimiterInterface.php └── RequestRateLimiterInterface.php ├── RedirectResponse.php ├── Request.php ├── RequestMatcher ├── AttributesRequestMatcher.php ├── ExpressionRequestMatcher.php ├── HeaderRequestMatcher.php ├── HostRequestMatcher.php ├── IpsRequestMatcher.php ├── IsJsonRequestMatcher.php ├── MethodRequestMatcher.php ├── PathRequestMatcher.php ├── PortRequestMatcher.php ├── QueryParameterRequestMatcher.php └── SchemeRequestMatcher.php ├── RequestMatcherInterface.php ├── RequestStack.php ├── Response.php ├── ResponseHeaderBag.php ├── ServerBag.php ├── ServerEvent.php ├── Session ├── Attribute │ ├── AttributeBag.php │ └── AttributeBagInterface.php ├── Flash │ ├── AutoExpireFlashBag.php │ ├── FlashBag.php │ └── FlashBagInterface.php ├── FlashBagAwareSessionInterface.php ├── Session.php ├── SessionBagInterface.php ├── SessionBagProxy.php ├── SessionFactory.php ├── SessionFactoryInterface.php ├── SessionInterface.php ├── SessionUtils.php └── Storage │ ├── Handler │ ├── AbstractSessionHandler.php │ ├── IdentityMarshaller.php │ ├── MarshallingSessionHandler.php │ ├── MemcachedSessionHandler.php │ ├── MigratingSessionHandler.php │ ├── MongoDbSessionHandler.php │ ├── NativeFileSessionHandler.php │ ├── NullSessionHandler.php │ ├── PdoSessionHandler.php │ ├── RedisSessionHandler.php │ ├── SessionHandlerFactory.php │ └── StrictSessionHandler.php │ ├── MetadataBag.php │ ├── MockArraySessionStorage.php │ ├── MockFileSessionStorage.php │ ├── MockFileSessionStorageFactory.php │ ├── NativeSessionStorage.php │ ├── NativeSessionStorageFactory.php │ ├── PhpBridgeSessionStorage.php │ ├── PhpBridgeSessionStorageFactory.php │ ├── Proxy │ ├── AbstractProxy.php │ └── SessionHandlerProxy.php │ ├── SessionStorageFactoryInterface.php │ └── SessionStorageInterface.php ├── StreamedJsonResponse.php ├── StreamedResponse.php ├── Test └── Constraint │ ├── RequestAttributeValueSame.php │ ├── ResponseCookieValueSame.php │ ├── ResponseFormatSame.php │ ├── ResponseHasCookie.php │ ├── ResponseHasHeader.php │ ├── ResponseHeaderLocationSame.php │ ├── ResponseHeaderSame.php │ ├── ResponseIsRedirected.php │ ├── ResponseIsSuccessful.php │ ├── ResponseIsUnprocessable.php │ └── ResponseStatusCodeSame.php ├── UriSigner.php ├── UrlHelper.php └── composer.json /AcceptHeader.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation; 13 | 14 | // Help opcache.preload discover always-needed symbols 15 | class_exists(AcceptHeaderItem::class); 16 | 17 | /** 18 | * Represents an Accept-* header. 19 | * 20 | * An accept header is compound with a list of items, 21 | * sorted by descending quality. 22 | * 23 | * @author Jean-François Simon 24 | */ 25 | class AcceptHeader 26 | { 27 | /** 28 | * @var AcceptHeaderItem[] 29 | */ 30 | private array $items = []; 31 | 32 | private bool $sorted = true; 33 | 34 | /** 35 | * @param AcceptHeaderItem[] $items 36 | */ 37 | public function __construct(array $items) 38 | { 39 | foreach ($items as $item) { 40 | $this->add($item); 41 | } 42 | } 43 | 44 | /** 45 | * Builds an AcceptHeader instance from a string. 46 | */ 47 | public static function fromString(?string $headerValue): self 48 | { 49 | $parts = HeaderUtils::split($headerValue ?? '', ',;='); 50 | 51 | return new self(array_map(function ($subParts) { 52 | static $index = 0; 53 | $part = array_shift($subParts); 54 | $attributes = HeaderUtils::combine($subParts); 55 | 56 | $item = new AcceptHeaderItem($part[0], $attributes); 57 | $item->setIndex($index++); 58 | 59 | return $item; 60 | }, $parts)); 61 | } 62 | 63 | /** 64 | * Returns header value's string representation. 65 | */ 66 | public function __toString(): string 67 | { 68 | return implode(',', $this->items); 69 | } 70 | 71 | /** 72 | * Tests if header has given value. 73 | */ 74 | public function has(string $value): bool 75 | { 76 | return isset($this->items[$value]); 77 | } 78 | 79 | /** 80 | * Returns given value's item, if exists. 81 | */ 82 | public function get(string $value): ?AcceptHeaderItem 83 | { 84 | return $this->items[$value] ?? $this->items[explode('/', $value)[0].'/*'] ?? $this->items['*/*'] ?? $this->items['*'] ?? null; 85 | } 86 | 87 | /** 88 | * Adds an item. 89 | * 90 | * @return $this 91 | */ 92 | public function add(AcceptHeaderItem $item): static 93 | { 94 | $this->items[$item->getValue()] = $item; 95 | $this->sorted = false; 96 | 97 | return $this; 98 | } 99 | 100 | /** 101 | * Returns all items. 102 | * 103 | * @return AcceptHeaderItem[] 104 | */ 105 | public function all(): array 106 | { 107 | $this->sort(); 108 | 109 | return $this->items; 110 | } 111 | 112 | /** 113 | * Filters items on their value using given regex. 114 | */ 115 | public function filter(string $pattern): self 116 | { 117 | return new self(array_filter($this->items, fn (AcceptHeaderItem $item) => preg_match($pattern, $item->getValue()))); 118 | } 119 | 120 | /** 121 | * Returns first item. 122 | */ 123 | public function first(): ?AcceptHeaderItem 124 | { 125 | $this->sort(); 126 | 127 | return $this->items ? reset($this->items) : null; 128 | } 129 | 130 | /** 131 | * Sorts items by descending quality. 132 | */ 133 | private function sort(): void 134 | { 135 | if (!$this->sorted) { 136 | uasort($this->items, function (AcceptHeaderItem $a, AcceptHeaderItem $b) { 137 | $qA = $a->getQuality(); 138 | $qB = $b->getQuality(); 139 | 140 | if ($qA === $qB) { 141 | return $a->getIndex() > $b->getIndex() ? 1 : -1; 142 | } 143 | 144 | return $qA > $qB ? -1 : 1; 145 | }); 146 | 147 | $this->sorted = true; 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /AcceptHeaderItem.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation; 13 | 14 | /** 15 | * Represents an Accept-* header item. 16 | * 17 | * @author Jean-François Simon 18 | */ 19 | class AcceptHeaderItem 20 | { 21 | private float $quality = 1.0; 22 | private int $index = 0; 23 | private array $attributes = []; 24 | 25 | public function __construct( 26 | private string $value, 27 | array $attributes = [], 28 | ) { 29 | foreach ($attributes as $name => $value) { 30 | $this->setAttribute($name, $value); 31 | } 32 | } 33 | 34 | /** 35 | * Builds an AcceptHeaderInstance instance from a string. 36 | */ 37 | public static function fromString(?string $itemValue): self 38 | { 39 | $parts = HeaderUtils::split($itemValue ?? '', ';='); 40 | 41 | $part = array_shift($parts); 42 | $attributes = HeaderUtils::combine($parts); 43 | 44 | return new self($part[0], $attributes); 45 | } 46 | 47 | /** 48 | * Returns header value's string representation. 49 | */ 50 | public function __toString(): string 51 | { 52 | $string = $this->value.($this->quality < 1 ? ';q='.$this->quality : ''); 53 | if (\count($this->attributes) > 0) { 54 | $string .= '; '.HeaderUtils::toString($this->attributes, ';'); 55 | } 56 | 57 | return $string; 58 | } 59 | 60 | /** 61 | * Set the item value. 62 | * 63 | * @return $this 64 | */ 65 | public function setValue(string $value): static 66 | { 67 | $this->value = $value; 68 | 69 | return $this; 70 | } 71 | 72 | /** 73 | * Returns the item value. 74 | */ 75 | public function getValue(): string 76 | { 77 | return $this->value; 78 | } 79 | 80 | /** 81 | * Set the item quality. 82 | * 83 | * @return $this 84 | */ 85 | public function setQuality(float $quality): static 86 | { 87 | $this->quality = $quality; 88 | 89 | return $this; 90 | } 91 | 92 | /** 93 | * Returns the item quality. 94 | */ 95 | public function getQuality(): float 96 | { 97 | return $this->quality; 98 | } 99 | 100 | /** 101 | * Set the item index. 102 | * 103 | * @return $this 104 | */ 105 | public function setIndex(int $index): static 106 | { 107 | $this->index = $index; 108 | 109 | return $this; 110 | } 111 | 112 | /** 113 | * Returns the item index. 114 | */ 115 | public function getIndex(): int 116 | { 117 | return $this->index; 118 | } 119 | 120 | /** 121 | * Tests if an attribute exists. 122 | */ 123 | public function hasAttribute(string $name): bool 124 | { 125 | return isset($this->attributes[$name]); 126 | } 127 | 128 | /** 129 | * Returns an attribute by its name. 130 | */ 131 | public function getAttribute(string $name, mixed $default = null): mixed 132 | { 133 | return $this->attributes[$name] ?? $default; 134 | } 135 | 136 | /** 137 | * Returns all attributes. 138 | */ 139 | public function getAttributes(): array 140 | { 141 | return $this->attributes; 142 | } 143 | 144 | /** 145 | * Set an attribute. 146 | * 147 | * @return $this 148 | */ 149 | public function setAttribute(string $name, string $value): static 150 | { 151 | if ('q' === $name) { 152 | $this->quality = (float) $value; 153 | } else { 154 | $this->attributes[$name] = $value; 155 | } 156 | 157 | return $this; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /ChainRequestMatcher.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation; 13 | 14 | /** 15 | * ChainRequestMatcher verifies that all checks match against a Request instance. 16 | * 17 | * @author Fabien Potencier 18 | */ 19 | class ChainRequestMatcher implements RequestMatcherInterface 20 | { 21 | /** 22 | * @param iterable $matchers 23 | */ 24 | public function __construct(private iterable $matchers) 25 | { 26 | } 27 | 28 | public function matches(Request $request): bool 29 | { 30 | foreach ($this->matchers as $matcher) { 31 | if (!$matcher->matches($request)) { 32 | return false; 33 | } 34 | } 35 | 36 | return true; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /EventStreamResponse.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation; 13 | 14 | /** 15 | * Represents a streaming HTTP response for sending server events 16 | * as part of the Server-Sent Events (SSE) streaming technique. 17 | * 18 | * To broadcast events to multiple users at once, for long-running 19 | * connections and for high-traffic websites, prefer using the Mercure 20 | * Symfony Component, which relies on Software designed for these use 21 | * cases: https://symfony.com/doc/current/mercure.html 22 | * 23 | * @see ServerEvent 24 | * 25 | * @author Yonel Ceruto 26 | * 27 | * Example usage: 28 | * 29 | * return new EventStreamResponse(function () { 30 | * yield new ServerEvent(time()); 31 | * 32 | * sleep(1); 33 | * 34 | * yield new ServerEvent(time()); 35 | * }); 36 | */ 37 | class EventStreamResponse extends StreamedResponse 38 | { 39 | /** 40 | * @param int|null $retry The number of milliseconds the client should wait 41 | * before reconnecting in case of network failure 42 | */ 43 | public function __construct(?callable $callback = null, int $status = 200, array $headers = [], private ?int $retry = null) 44 | { 45 | $headers += [ 46 | 'Connection' => 'keep-alive', 47 | 'Content-Type' => 'text/event-stream', 48 | 'Cache-Control' => 'private, no-cache, no-store, must-revalidate, max-age=0', 49 | 'X-Accel-Buffering' => 'no', 50 | 'Pragma' => 'no-cache', 51 | 'Expire' => '0', 52 | ]; 53 | 54 | parent::__construct($callback, $status, $headers); 55 | } 56 | 57 | public function setCallback(callable $callback): static 58 | { 59 | if ($this->callback) { 60 | return parent::setCallback($callback); 61 | } 62 | 63 | $this->callback = function () use ($callback) { 64 | if (is_iterable($events = $callback($this))) { 65 | foreach ($events as $event) { 66 | $this->sendEvent($event); 67 | 68 | if (connection_aborted()) { 69 | break; 70 | } 71 | } 72 | } 73 | }; 74 | 75 | return $this; 76 | } 77 | 78 | /** 79 | * Sends a server event to the client. 80 | * 81 | * @return $this 82 | */ 83 | public function sendEvent(ServerEvent $event): static 84 | { 85 | if ($this->retry > 0 && !$event->getRetry()) { 86 | $event->setRetry($this->retry); 87 | } 88 | 89 | foreach ($event as $part) { 90 | echo $part; 91 | 92 | if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) { 93 | static::closeOutputBuffers(0, true); 94 | flush(); 95 | } 96 | } 97 | 98 | return $this; 99 | } 100 | 101 | public function getRetry(): ?int 102 | { 103 | return $this->retry; 104 | } 105 | 106 | public function setRetry(int $retry): void 107 | { 108 | $this->retry = $retry; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Exception/BadRequestException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Exception; 13 | 14 | /** 15 | * Raised when a user sends a malformed request. 16 | */ 17 | class BadRequestException extends UnexpectedValueException implements RequestExceptionInterface 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /Exception/ConflictingHeadersException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Exception; 13 | 14 | /** 15 | * The HTTP request contains headers with conflicting information. 16 | * 17 | * @author Magnus Nordlander 18 | */ 19 | class ConflictingHeadersException extends UnexpectedValueException implements RequestExceptionInterface 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Exception; 13 | 14 | interface ExceptionInterface extends \Throwable 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /Exception/ExpiredSignedUriException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Exception; 13 | 14 | /** 15 | * @author Kevin Bond 16 | */ 17 | final class ExpiredSignedUriException extends SignedUriException 18 | { 19 | /** 20 | * @internal 21 | */ 22 | public function __construct() 23 | { 24 | parent::__construct('The URI has expired.'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Exception/JsonException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Exception; 13 | 14 | /** 15 | * Thrown by Request::toArray() when the content cannot be JSON-decoded. 16 | * 17 | * @author Tobias Nyholm 18 | */ 19 | final class JsonException extends UnexpectedValueException implements RequestExceptionInterface 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Exception/LogicException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Exception; 13 | 14 | /** 15 | * Base LogicException for Http Foundation component. 16 | */ 17 | class LogicException extends \LogicException implements ExceptionInterface 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /Exception/RequestExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Exception; 13 | 14 | /** 15 | * Interface for Request exceptions. 16 | * 17 | * Exceptions implementing this interface should trigger an HTTP 400 response in the application code. 18 | */ 19 | interface RequestExceptionInterface 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Exception/SessionNotFoundException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Exception; 13 | 14 | /** 15 | * Raised when a session does not exist. This happens in the following cases: 16 | * - the session is not enabled 17 | * - attempt to read a session outside a request context (ie. cli script). 18 | * 19 | * @author Jérémy Derussé 20 | */ 21 | class SessionNotFoundException extends \LogicException implements RequestExceptionInterface 22 | { 23 | public function __construct(string $message = 'There is currently no session available.', int $code = 0, ?\Throwable $previous = null) 24 | { 25 | parent::__construct($message, $code, $previous); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Exception/SignedUriException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Exception; 13 | 14 | /** 15 | * @author Kevin Bond 16 | */ 17 | abstract class SignedUriException extends \RuntimeException implements ExceptionInterface 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /Exception/SuspiciousOperationException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Exception; 13 | 14 | /** 15 | * Raised when a user has performed an operation that should be considered 16 | * suspicious from a security perspective. 17 | */ 18 | class SuspiciousOperationException extends UnexpectedValueException implements RequestExceptionInterface 19 | { 20 | } 21 | -------------------------------------------------------------------------------- /Exception/UnexpectedValueException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Exception; 13 | 14 | class UnexpectedValueException extends \UnexpectedValueException 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /Exception/UnsignedUriException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Exception; 13 | 14 | /** 15 | * @author Kevin Bond 16 | */ 17 | final class UnsignedUriException extends SignedUriException 18 | { 19 | /** 20 | * @internal 21 | */ 22 | public function __construct() 23 | { 24 | parent::__construct('The URI is not signed.'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Exception/UnverifiedSignedUriException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Exception; 13 | 14 | /** 15 | * @author Kevin Bond 16 | */ 17 | final class UnverifiedSignedUriException extends SignedUriException 18 | { 19 | /** 20 | * @internal 21 | */ 22 | public function __construct() 23 | { 24 | parent::__construct('The URI signature is invalid.'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /File/Exception/AccessDeniedException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\File\Exception; 13 | 14 | /** 15 | * Thrown when the access on a file was denied. 16 | * 17 | * @author Bernhard Schussek 18 | */ 19 | class AccessDeniedException extends FileException 20 | { 21 | public function __construct(string $path) 22 | { 23 | parent::__construct(\sprintf('The file %s could not be accessed', $path)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /File/Exception/CannotWriteFileException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\File\Exception; 13 | 14 | /** 15 | * Thrown when an UPLOAD_ERR_CANT_WRITE error occurred with UploadedFile. 16 | * 17 | * @author Florent Mata 18 | */ 19 | class CannotWriteFileException extends FileException 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /File/Exception/ExtensionFileException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\File\Exception; 13 | 14 | /** 15 | * Thrown when an UPLOAD_ERR_EXTENSION error occurred with UploadedFile. 16 | * 17 | * @author Florent Mata 18 | */ 19 | class ExtensionFileException extends FileException 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /File/Exception/FileException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\File\Exception; 13 | 14 | /** 15 | * Thrown when an error occurred in the component File. 16 | * 17 | * @author Bernhard Schussek 18 | */ 19 | class FileException extends \RuntimeException 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /File/Exception/FileNotFoundException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\File\Exception; 13 | 14 | /** 15 | * Thrown when a file was not found. 16 | * 17 | * @author Bernhard Schussek 18 | */ 19 | class FileNotFoundException extends FileException 20 | { 21 | public function __construct(string $path) 22 | { 23 | parent::__construct(\sprintf('The file "%s" does not exist', $path)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /File/Exception/FormSizeFileException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\File\Exception; 13 | 14 | /** 15 | * Thrown when an UPLOAD_ERR_FORM_SIZE error occurred with UploadedFile. 16 | * 17 | * @author Florent Mata 18 | */ 19 | class FormSizeFileException extends FileException 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /File/Exception/IniSizeFileException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\File\Exception; 13 | 14 | /** 15 | * Thrown when an UPLOAD_ERR_INI_SIZE error occurred with UploadedFile. 16 | * 17 | * @author Florent Mata 18 | */ 19 | class IniSizeFileException extends FileException 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /File/Exception/NoFileException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\File\Exception; 13 | 14 | /** 15 | * Thrown when an UPLOAD_ERR_NO_FILE error occurred with UploadedFile. 16 | * 17 | * @author Florent Mata 18 | */ 19 | class NoFileException extends FileException 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /File/Exception/NoTmpDirFileException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\File\Exception; 13 | 14 | /** 15 | * Thrown when an UPLOAD_ERR_NO_TMP_DIR error occurred with UploadedFile. 16 | * 17 | * @author Florent Mata 18 | */ 19 | class NoTmpDirFileException extends FileException 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /File/Exception/PartialFileException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\File\Exception; 13 | 14 | /** 15 | * Thrown when an UPLOAD_ERR_PARTIAL error occurred with UploadedFile. 16 | * 17 | * @author Florent Mata 18 | */ 19 | class PartialFileException extends FileException 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /File/Exception/UnexpectedTypeException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\File\Exception; 13 | 14 | class UnexpectedTypeException extends FileException 15 | { 16 | public function __construct(mixed $value, string $expectedType) 17 | { 18 | parent::__construct(\sprintf('Expected argument of type %s, %s given', $expectedType, get_debug_type($value))); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /File/Exception/UploadException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\File\Exception; 13 | 14 | /** 15 | * Thrown when an error occurred during file upload. 16 | * 17 | * @author Bernhard Schussek 18 | */ 19 | class UploadException extends FileException 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /File/File.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\File; 13 | 14 | use Symfony\Component\HttpFoundation\File\Exception\FileException; 15 | use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; 16 | use Symfony\Component\Mime\MimeTypes; 17 | 18 | /** 19 | * A file in the file system. 20 | * 21 | * @author Bernhard Schussek 22 | */ 23 | class File extends \SplFileInfo 24 | { 25 | /** 26 | * Constructs a new file from the given path. 27 | * 28 | * @param string $path The path to the file 29 | * @param bool $checkPath Whether to check the path or not 30 | * 31 | * @throws FileNotFoundException If the given path is not a file 32 | */ 33 | public function __construct(string $path, bool $checkPath = true) 34 | { 35 | if ($checkPath && !is_file($path)) { 36 | throw new FileNotFoundException($path); 37 | } 38 | 39 | parent::__construct($path); 40 | } 41 | 42 | /** 43 | * Returns the extension based on the mime type. 44 | * 45 | * If the mime type is unknown, returns null. 46 | * 47 | * This method uses the mime type as guessed by getMimeType() 48 | * to guess the file extension. 49 | * 50 | * @see MimeTypes 51 | * @see getMimeType() 52 | */ 53 | public function guessExtension(): ?string 54 | { 55 | if (!class_exists(MimeTypes::class)) { 56 | throw new \LogicException('You cannot guess the extension as the Mime component is not installed. Try running "composer require symfony/mime".'); 57 | } 58 | 59 | return MimeTypes::getDefault()->getExtensions($this->getMimeType())[0] ?? null; 60 | } 61 | 62 | /** 63 | * Returns the mime type of the file. 64 | * 65 | * The mime type is guessed using a MimeTypeGuesserInterface instance, 66 | * which uses finfo_file() then the "file" system binary, 67 | * depending on which of those are available. 68 | * 69 | * @see MimeTypes 70 | */ 71 | public function getMimeType(): ?string 72 | { 73 | if (!class_exists(MimeTypes::class)) { 74 | throw new \LogicException('You cannot guess the mime type as the Mime component is not installed. Try running "composer require symfony/mime".'); 75 | } 76 | 77 | return MimeTypes::getDefault()->guessMimeType($this->getPathname()); 78 | } 79 | 80 | /** 81 | * Moves the file to a new location. 82 | * 83 | * @throws FileException if the target file could not be created 84 | */ 85 | public function move(string $directory, ?string $name = null): self 86 | { 87 | $target = $this->getTargetFile($directory, $name); 88 | 89 | set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; }); 90 | try { 91 | $renamed = rename($this->getPathname(), $target); 92 | } finally { 93 | restore_error_handler(); 94 | } 95 | if (!$renamed) { 96 | throw new FileException(\sprintf('Could not move the file "%s" to "%s" (%s).', $this->getPathname(), $target, strip_tags($error))); 97 | } 98 | 99 | @chmod($target, 0666 & ~umask()); 100 | 101 | return $target; 102 | } 103 | 104 | public function getContent(): string 105 | { 106 | $content = file_get_contents($this->getPathname()); 107 | 108 | if (false === $content) { 109 | throw new FileException(\sprintf('Could not get the content of the file "%s".', $this->getPathname())); 110 | } 111 | 112 | return $content; 113 | } 114 | 115 | protected function getTargetFile(string $directory, ?string $name = null): self 116 | { 117 | if (!is_dir($directory)) { 118 | if (false === @mkdir($directory, 0777, true) && !is_dir($directory)) { 119 | throw new FileException(\sprintf('Unable to create the "%s" directory.', $directory)); 120 | } 121 | } elseif (!is_writable($directory)) { 122 | throw new FileException(\sprintf('Unable to write in the "%s" directory.', $directory)); 123 | } 124 | 125 | $target = rtrim($directory, '/\\').\DIRECTORY_SEPARATOR.(null === $name ? $this->getBasename() : $this->getName($name)); 126 | 127 | return new self($target, false); 128 | } 129 | 130 | /** 131 | * Returns locale independent base name of the given path. 132 | */ 133 | protected function getName(string $name): string 134 | { 135 | $originalName = str_replace('\\', '/', $name); 136 | $pos = strrpos($originalName, '/'); 137 | 138 | return false === $pos ? $originalName : substr($originalName, $pos + 1); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /File/Stream.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\File; 13 | 14 | /** 15 | * A PHP stream of unknown size. 16 | * 17 | * @author Nicolas Grekas 18 | */ 19 | class Stream extends File 20 | { 21 | public function getSize(): int|false 22 | { 23 | return false; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /FileBag.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation; 13 | 14 | use Symfony\Component\HttpFoundation\File\UploadedFile; 15 | 16 | /** 17 | * FileBag is a container for uploaded files. 18 | * 19 | * @author Fabien Potencier 20 | * @author Bulat Shakirzyanov 21 | */ 22 | class FileBag extends ParameterBag 23 | { 24 | private const FILE_KEYS = ['error', 'full_path', 'name', 'size', 'tmp_name', 'type']; 25 | 26 | /** 27 | * @param array|UploadedFile[] $parameters An array of HTTP files 28 | */ 29 | public function __construct(array $parameters = []) 30 | { 31 | $this->replace($parameters); 32 | } 33 | 34 | public function replace(array $files = []): void 35 | { 36 | $this->parameters = []; 37 | $this->add($files); 38 | } 39 | 40 | public function set(string $key, mixed $value): void 41 | { 42 | if (!\is_array($value) && !$value instanceof UploadedFile) { 43 | throw new \InvalidArgumentException('An uploaded file must be an array or an instance of UploadedFile.'); 44 | } 45 | 46 | parent::set($key, $this->convertFileInformation($value)); 47 | } 48 | 49 | public function add(array $files = []): void 50 | { 51 | foreach ($files as $key => $file) { 52 | $this->set($key, $file); 53 | } 54 | } 55 | 56 | /** 57 | * Converts uploaded files to UploadedFile instances. 58 | * 59 | * @return UploadedFile[]|UploadedFile|null 60 | */ 61 | protected function convertFileInformation(array|UploadedFile $file): array|UploadedFile|null 62 | { 63 | if ($file instanceof UploadedFile) { 64 | return $file; 65 | } 66 | 67 | $file = $this->fixPhpFilesArray($file); 68 | $keys = array_keys($file + ['full_path' => null]); 69 | sort($keys); 70 | 71 | if (self::FILE_KEYS === $keys) { 72 | if (\UPLOAD_ERR_NO_FILE === $file['error']) { 73 | $file = null; 74 | } else { 75 | $file = new UploadedFile($file['tmp_name'], $file['full_path'] ?? $file['name'], $file['type'], $file['error'], false); 76 | } 77 | } else { 78 | $file = array_map(fn ($v) => $v instanceof UploadedFile || \is_array($v) ? $this->convertFileInformation($v) : $v, $file); 79 | if (array_is_list($file)) { 80 | $file = array_filter($file); 81 | } 82 | } 83 | 84 | return $file; 85 | } 86 | 87 | /** 88 | * Fixes a malformed PHP $_FILES array. 89 | * 90 | * PHP has a bug that the format of the $_FILES array differs, depending on 91 | * whether the uploaded file fields had normal field names or array-like 92 | * field names ("normal" vs. "parent[child]"). 93 | * 94 | * This method fixes the array to look like the "normal" $_FILES array. 95 | * 96 | * It's safe to pass an already converted array, in which case this method 97 | * just returns the original array unmodified. 98 | */ 99 | protected function fixPhpFilesArray(array $data): array 100 | { 101 | $keys = array_keys($data + ['full_path' => null]); 102 | sort($keys); 103 | 104 | if (self::FILE_KEYS !== $keys || !isset($data['name']) || !\is_array($data['name'])) { 105 | return $data; 106 | } 107 | 108 | $files = $data; 109 | foreach (self::FILE_KEYS as $k) { 110 | unset($files[$k]); 111 | } 112 | 113 | foreach ($data['name'] as $key => $name) { 114 | $files[$key] = $this->fixPhpFilesArray([ 115 | 'error' => $data['error'][$key], 116 | 'name' => $name, 117 | 'type' => $data['type'][$key], 118 | 'tmp_name' => $data['tmp_name'][$key], 119 | 'size' => $data['size'][$key], 120 | ] + (isset($data['full_path'][$key]) ? [ 121 | 'full_path' => $data['full_path'][$key], 122 | ] : [])); 123 | } 124 | 125 | return $files; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /InputBag.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation; 13 | 14 | use Symfony\Component\HttpFoundation\Exception\BadRequestException; 15 | use Symfony\Component\HttpFoundation\Exception\UnexpectedValueException; 16 | 17 | /** 18 | * InputBag is a container for user input values such as $_GET, $_POST, $_REQUEST, and $_COOKIE. 19 | * 20 | * @author Saif Eddin Gmati 21 | */ 22 | final class InputBag extends ParameterBag 23 | { 24 | /** 25 | * Returns a scalar input value by name. 26 | * 27 | * @param string|int|float|bool|null $default The default value if the input key does not exist 28 | * 29 | * @throws BadRequestException if the input contains a non-scalar value 30 | */ 31 | public function get(string $key, mixed $default = null): string|int|float|bool|null 32 | { 33 | if (null !== $default && !\is_scalar($default) && !$default instanceof \Stringable) { 34 | throw new \InvalidArgumentException(\sprintf('Expected a scalar value as a 2nd argument to "%s()", "%s" given.', __METHOD__, get_debug_type($default))); 35 | } 36 | 37 | $value = parent::get($key, $this); 38 | 39 | if (null !== $value && $this !== $value && !\is_scalar($value) && !$value instanceof \Stringable) { 40 | throw new BadRequestException(\sprintf('Input value "%s" contains a non-scalar value.', $key)); 41 | } 42 | 43 | return $this === $value ? $default : $value; 44 | } 45 | 46 | /** 47 | * Replaces the current input values by a new set. 48 | */ 49 | public function replace(array $inputs = []): void 50 | { 51 | $this->parameters = []; 52 | $this->add($inputs); 53 | } 54 | 55 | /** 56 | * Adds input values. 57 | */ 58 | public function add(array $inputs = []): void 59 | { 60 | foreach ($inputs as $input => $value) { 61 | $this->set($input, $value); 62 | } 63 | } 64 | 65 | /** 66 | * Sets an input by name. 67 | * 68 | * @param string|int|float|bool|array|null $value 69 | */ 70 | public function set(string $key, mixed $value): void 71 | { 72 | if (null !== $value && !\is_scalar($value) && !\is_array($value) && !$value instanceof \Stringable) { 73 | throw new \InvalidArgumentException(\sprintf('Expected a scalar, or an array as a 2nd argument to "%s()", "%s" given.', __METHOD__, get_debug_type($value))); 74 | } 75 | 76 | $this->parameters[$key] = $value; 77 | } 78 | 79 | /** 80 | * Returns the parameter value converted to an enum. 81 | * 82 | * @template T of \BackedEnum 83 | * 84 | * @param class-string $class 85 | * @param ?T $default 86 | * 87 | * @return ?T 88 | * 89 | * @psalm-return ($default is null ? T|null : T) 90 | * 91 | * @throws BadRequestException if the input cannot be converted to an enum 92 | */ 93 | public function getEnum(string $key, string $class, ?\BackedEnum $default = null): ?\BackedEnum 94 | { 95 | try { 96 | return parent::getEnum($key, $class, $default); 97 | } catch (UnexpectedValueException $e) { 98 | throw new BadRequestException($e->getMessage(), $e->getCode(), $e); 99 | } 100 | } 101 | 102 | /** 103 | * Returns the parameter value converted to string. 104 | * 105 | * @throws BadRequestException if the input contains a non-scalar value 106 | */ 107 | public function getString(string $key, string $default = ''): string 108 | { 109 | // Shortcuts the parent method because the validation on scalar is already done in get(). 110 | return (string) $this->get($key, $default); 111 | } 112 | 113 | /** 114 | * @throws BadRequestException if the input value is an array and \FILTER_REQUIRE_ARRAY or \FILTER_FORCE_ARRAY is not set 115 | * @throws BadRequestException if the input value is invalid and \FILTER_NULL_ON_FAILURE is not set 116 | */ 117 | public function filter(string $key, mixed $default = null, int $filter = \FILTER_DEFAULT, mixed $options = []): mixed 118 | { 119 | $value = $this->has($key) ? $this->all()[$key] : $default; 120 | 121 | // Always turn $options into an array - this allows filter_var option shortcuts. 122 | if (!\is_array($options) && $options) { 123 | $options = ['flags' => $options]; 124 | } 125 | 126 | if (\is_array($value) && !(($options['flags'] ?? 0) & (\FILTER_REQUIRE_ARRAY | \FILTER_FORCE_ARRAY))) { 127 | throw new BadRequestException(\sprintf('Input value "%s" contains an array, but "FILTER_REQUIRE_ARRAY" or "FILTER_FORCE_ARRAY" flags were not set.', $key)); 128 | } 129 | 130 | if ((\FILTER_CALLBACK & $filter) && !(($options['options'] ?? null) instanceof \Closure)) { 131 | throw new \InvalidArgumentException(\sprintf('A Closure must be passed to "%s()" when FILTER_CALLBACK is used, "%s" given.', __METHOD__, get_debug_type($options['options'] ?? null))); 132 | } 133 | 134 | $options['flags'] ??= 0; 135 | $nullOnFailure = $options['flags'] & \FILTER_NULL_ON_FAILURE; 136 | $options['flags'] |= \FILTER_NULL_ON_FAILURE; 137 | 138 | $value = filter_var($value, $filter, $options); 139 | 140 | if (null !== $value || $nullOnFailure) { 141 | return $value; 142 | } 143 | 144 | throw new BadRequestException(\sprintf('Input value "%s" is invalid and flag "FILTER_NULL_ON_FAILURE" was not set.', $key)); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /JsonResponse.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation; 13 | 14 | /** 15 | * Response represents an HTTP response in JSON format. 16 | * 17 | * Note that this class does not force the returned JSON content to be an 18 | * object. It is however recommended that you do return an object as it 19 | * protects yourself against XSSI and JSON-JavaScript Hijacking. 20 | * 21 | * @see https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/AJAX_Security_Cheat_Sheet.md#always-return-json-with-an-object-on-the-outside 22 | * 23 | * @author Igor Wiedler 24 | */ 25 | class JsonResponse extends Response 26 | { 27 | protected mixed $data; 28 | protected ?string $callback = null; 29 | 30 | // Encode <, >, ', &, and " characters in the JSON, making it also safe to be embedded into HTML. 31 | // 15 === JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT 32 | public const DEFAULT_ENCODING_OPTIONS = 15; 33 | 34 | protected int $encodingOptions = self::DEFAULT_ENCODING_OPTIONS; 35 | 36 | /** 37 | * @param bool $json If the data is already a JSON string 38 | */ 39 | public function __construct(mixed $data = null, int $status = 200, array $headers = [], bool $json = false) 40 | { 41 | parent::__construct('', $status, $headers); 42 | 43 | if ($json && !\is_string($data) && !is_numeric($data) && !$data instanceof \Stringable) { 44 | throw new \TypeError(\sprintf('"%s": If $json is set to true, argument $data must be a string or object implementing __toString(), "%s" given.', __METHOD__, get_debug_type($data))); 45 | } 46 | 47 | $data ??= new \ArrayObject(); 48 | 49 | $json ? $this->setJson($data) : $this->setData($data); 50 | } 51 | 52 | /** 53 | * Factory method for chainability. 54 | * 55 | * Example: 56 | * 57 | * return JsonResponse::fromJsonString('{"key": "value"}') 58 | * ->setSharedMaxAge(300); 59 | * 60 | * @param string $data The JSON response string 61 | * @param int $status The response status code (200 "OK" by default) 62 | * @param array $headers An array of response headers 63 | */ 64 | public static function fromJsonString(string $data, int $status = 200, array $headers = []): static 65 | { 66 | return new static($data, $status, $headers, true); 67 | } 68 | 69 | /** 70 | * Sets the JSONP callback. 71 | * 72 | * @param string|null $callback The JSONP callback or null to use none 73 | * 74 | * @return $this 75 | * 76 | * @throws \InvalidArgumentException When the callback name is not valid 77 | */ 78 | public function setCallback(?string $callback): static 79 | { 80 | if (null !== $callback) { 81 | // partially taken from https://geekality.net/2011/08/03/valid-javascript-identifier/ 82 | // partially taken from https://github.com/willdurand/JsonpCallbackValidator 83 | // JsonpCallbackValidator is released under the MIT License. See https://github.com/willdurand/JsonpCallbackValidator/blob/v1.1.0/LICENSE for details. 84 | // (c) William Durand 85 | $pattern = '/^[$_\p{L}][$_\p{L}\p{Mn}\p{Mc}\p{Nd}\p{Pc}\x{200C}\x{200D}]*(?:\[(?:"(?:\\\.|[^"\\\])*"|\'(?:\\\.|[^\'\\\])*\'|\d+)\])*?$/u'; 86 | $reserved = [ 87 | 'break', 'do', 'instanceof', 'typeof', 'case', 'else', 'new', 'var', 'catch', 'finally', 'return', 'void', 'continue', 'for', 'switch', 'while', 88 | 'debugger', 'function', 'this', 'with', 'default', 'if', 'throw', 'delete', 'in', 'try', 'class', 'enum', 'extends', 'super', 'const', 'export', 89 | 'import', 'implements', 'let', 'private', 'public', 'yield', 'interface', 'package', 'protected', 'static', 'null', 'true', 'false', 90 | ]; 91 | $parts = explode('.', $callback); 92 | foreach ($parts as $part) { 93 | if (!preg_match($pattern, $part) || \in_array($part, $reserved, true)) { 94 | throw new \InvalidArgumentException('The callback name is not valid.'); 95 | } 96 | } 97 | } 98 | 99 | $this->callback = $callback; 100 | 101 | return $this->update(); 102 | } 103 | 104 | /** 105 | * Sets a raw string containing a JSON document to be sent. 106 | * 107 | * @return $this 108 | */ 109 | public function setJson(string $json): static 110 | { 111 | $this->data = $json; 112 | 113 | return $this->update(); 114 | } 115 | 116 | /** 117 | * Sets the data to be sent as JSON. 118 | * 119 | * @return $this 120 | * 121 | * @throws \InvalidArgumentException 122 | */ 123 | public function setData(mixed $data = []): static 124 | { 125 | try { 126 | $data = json_encode($data, $this->encodingOptions); 127 | } catch (\Exception $e) { 128 | if ('Exception' === $e::class && str_starts_with($e->getMessage(), 'Failed calling ')) { 129 | throw $e->getPrevious() ?: $e; 130 | } 131 | throw $e; 132 | } 133 | 134 | if (\JSON_THROW_ON_ERROR & $this->encodingOptions) { 135 | return $this->setJson($data); 136 | } 137 | 138 | if (\JSON_ERROR_NONE !== json_last_error()) { 139 | throw new \InvalidArgumentException(json_last_error_msg()); 140 | } 141 | 142 | return $this->setJson($data); 143 | } 144 | 145 | /** 146 | * Returns options used while encoding data to JSON. 147 | */ 148 | public function getEncodingOptions(): int 149 | { 150 | return $this->encodingOptions; 151 | } 152 | 153 | /** 154 | * Sets options used while encoding data to JSON. 155 | * 156 | * @return $this 157 | */ 158 | public function setEncodingOptions(int $encodingOptions): static 159 | { 160 | $this->encodingOptions = $encodingOptions; 161 | 162 | return $this->setData(json_decode($this->data)); 163 | } 164 | 165 | /** 166 | * Updates the content and headers according to the JSON data and callback. 167 | * 168 | * @return $this 169 | */ 170 | protected function update(): static 171 | { 172 | if (null !== $this->callback) { 173 | // Not using application/javascript for compatibility reasons with older browsers. 174 | $this->headers->set('Content-Type', 'text/javascript'); 175 | 176 | return $this->setContent(\sprintf('/**/%s(%s);', $this->callback, $this->data)); 177 | } 178 | 179 | // Only set the header when there is none or when it equals 'text/javascript' (from a previous update with callback) 180 | // in order to not overwrite a custom definition. 181 | if (!$this->headers->has('Content-Type') || 'text/javascript' === $this->headers->get('Content-Type')) { 182 | $this->headers->set('Content-Type', 'application/json'); 183 | } 184 | 185 | return $this->setContent($this->data); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2004-present Fabien Potencier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | HttpFoundation Component 2 | ======================== 3 | 4 | The HttpFoundation component defines an object-oriented layer for the HTTP 5 | specification. 6 | 7 | Resources 8 | --------- 9 | 10 | * [Documentation](https://symfony.com/doc/current/components/http_foundation.html) 11 | * [Contributing](https://symfony.com/doc/current/contributing/index.html) 12 | * [Report issues](https://github.com/symfony/symfony/issues) and 13 | [send Pull Requests](https://github.com/symfony/symfony/pulls) 14 | in the [main Symfony repository](https://github.com/symfony/symfony) 15 | -------------------------------------------------------------------------------- /RateLimiter/AbstractRequestRateLimiter.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\RateLimiter; 13 | 14 | use Symfony\Component\HttpFoundation\Request; 15 | use Symfony\Component\RateLimiter\LimiterInterface; 16 | use Symfony\Component\RateLimiter\Policy\NoLimiter; 17 | use Symfony\Component\RateLimiter\RateLimit; 18 | 19 | /** 20 | * An implementation of PeekableRequestRateLimiterInterface that 21 | * fits most use-cases. 22 | * 23 | * @author Wouter de Jong 24 | */ 25 | abstract class AbstractRequestRateLimiter implements PeekableRequestRateLimiterInterface 26 | { 27 | public function consume(Request $request): RateLimit 28 | { 29 | return $this->doConsume($request, 1); 30 | } 31 | 32 | public function peek(Request $request): RateLimit 33 | { 34 | return $this->doConsume($request, 0); 35 | } 36 | 37 | private function doConsume(Request $request, int $tokens): RateLimit 38 | { 39 | $limiters = $this->getLimiters($request); 40 | if (0 === \count($limiters)) { 41 | $limiters = [new NoLimiter()]; 42 | } 43 | 44 | $minimalRateLimit = null; 45 | foreach ($limiters as $limiter) { 46 | $rateLimit = $limiter->consume($tokens); 47 | 48 | $minimalRateLimit = $minimalRateLimit ? self::getMinimalRateLimit($minimalRateLimit, $rateLimit) : $rateLimit; 49 | } 50 | 51 | return $minimalRateLimit; 52 | } 53 | 54 | public function reset(Request $request): void 55 | { 56 | foreach ($this->getLimiters($request) as $limiter) { 57 | $limiter->reset(); 58 | } 59 | } 60 | 61 | /** 62 | * @return LimiterInterface[] a set of limiters using keys extracted from the request 63 | */ 64 | abstract protected function getLimiters(Request $request): array; 65 | 66 | private static function getMinimalRateLimit(RateLimit $first, RateLimit $second): RateLimit 67 | { 68 | if ($first->isAccepted() !== $second->isAccepted()) { 69 | return $first->isAccepted() ? $second : $first; 70 | } 71 | 72 | $firstRemainingTokens = $first->getRemainingTokens(); 73 | $secondRemainingTokens = $second->getRemainingTokens(); 74 | 75 | if ($firstRemainingTokens === $secondRemainingTokens) { 76 | return $first->getRetryAfter() < $second->getRetryAfter() ? $second : $first; 77 | } 78 | 79 | return $firstRemainingTokens > $secondRemainingTokens ? $second : $first; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /RateLimiter/PeekableRequestRateLimiterInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\RateLimiter; 13 | 14 | use Symfony\Component\HttpFoundation\Request; 15 | use Symfony\Component\RateLimiter\RateLimit; 16 | 17 | /** 18 | * A request limiter which allows peeking ahead. 19 | * 20 | * This is valuable to reduce the cache backend load in scenarios 21 | * like a login when we only want to consume a token on login failure, 22 | * and where the majority of requests will be successful and thus not 23 | * need to consume a token. 24 | * 25 | * This way we can peek ahead before allowing the request through, and 26 | * only consume if the request failed (1 backend op). This is compared 27 | * to always consuming and then resetting the limit if the request 28 | * is successful (2 backend ops). 29 | * 30 | * @author Jordi Boggiano 31 | */ 32 | interface PeekableRequestRateLimiterInterface extends RequestRateLimiterInterface 33 | { 34 | public function peek(Request $request): RateLimit; 35 | } 36 | -------------------------------------------------------------------------------- /RateLimiter/RequestRateLimiterInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\RateLimiter; 13 | 14 | use Symfony\Component\HttpFoundation\Request; 15 | use Symfony\Component\RateLimiter\RateLimit; 16 | 17 | /** 18 | * A special type of limiter that deals with requests. 19 | * 20 | * This allows to limit on different types of information 21 | * from the requests. 22 | * 23 | * @author Wouter de Jong 24 | */ 25 | interface RequestRateLimiterInterface 26 | { 27 | public function consume(Request $request): RateLimit; 28 | 29 | public function reset(Request $request): void; 30 | } 31 | -------------------------------------------------------------------------------- /RedirectResponse.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation; 13 | 14 | /** 15 | * RedirectResponse represents an HTTP response doing a redirect. 16 | * 17 | * @author Fabien Potencier 18 | */ 19 | class RedirectResponse extends Response 20 | { 21 | protected string $targetUrl; 22 | 23 | /** 24 | * Creates a redirect response so that it conforms to the rules defined for a redirect status code. 25 | * 26 | * @param string $url The URL to redirect to. The URL should be a full URL, with schema etc., 27 | * but practically every browser redirects on paths only as well 28 | * @param int $status The HTTP status code (302 "Found" by default) 29 | * @param array $headers The headers (Location is always set to the given URL) 30 | * 31 | * @throws \InvalidArgumentException 32 | * 33 | * @see https://tools.ietf.org/html/rfc2616#section-10.3 34 | */ 35 | public function __construct(string $url, int $status = 302, array $headers = []) 36 | { 37 | parent::__construct('', $status, $headers); 38 | 39 | $this->setTargetUrl($url); 40 | 41 | if (!$this->isRedirect()) { 42 | throw new \InvalidArgumentException(\sprintf('The HTTP status code is not a redirect ("%s" given).', $status)); 43 | } 44 | 45 | if (301 == $status && !\array_key_exists('cache-control', array_change_key_case($headers, \CASE_LOWER))) { 46 | $this->headers->remove('cache-control'); 47 | } 48 | } 49 | 50 | /** 51 | * Returns the target URL. 52 | */ 53 | public function getTargetUrl(): string 54 | { 55 | return $this->targetUrl; 56 | } 57 | 58 | /** 59 | * Sets the redirect target of this response. 60 | * 61 | * @return $this 62 | * 63 | * @throws \InvalidArgumentException 64 | */ 65 | public function setTargetUrl(string $url): static 66 | { 67 | if ('' === $url) { 68 | throw new \InvalidArgumentException('Cannot redirect to an empty URL.'); 69 | } 70 | 71 | $this->targetUrl = $url; 72 | 73 | $this->setContent( 74 | \sprintf(' 75 | 76 | 77 | 78 | 79 | 80 | Redirecting to %1$s 81 | 82 | 83 | Redirecting to %1$s. 84 | 85 | ', htmlspecialchars($url, \ENT_QUOTES, 'UTF-8'))); 86 | 87 | $this->headers->set('Location', $url); 88 | $this->headers->set('Content-Type', 'text/html; charset=utf-8'); 89 | 90 | return $this; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /RequestMatcher/AttributesRequestMatcher.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\RequestMatcher; 13 | 14 | use Symfony\Component\HttpFoundation\Request; 15 | use Symfony\Component\HttpFoundation\RequestMatcherInterface; 16 | 17 | /** 18 | * Checks the Request attributes matches all regular expressions. 19 | * 20 | * @author Fabien Potencier 21 | */ 22 | class AttributesRequestMatcher implements RequestMatcherInterface 23 | { 24 | /** 25 | * @param array $regexps 26 | */ 27 | public function __construct(private array $regexps) 28 | { 29 | } 30 | 31 | public function matches(Request $request): bool 32 | { 33 | foreach ($this->regexps as $key => $regexp) { 34 | $attribute = $request->attributes->get($key); 35 | if (!\is_string($attribute)) { 36 | return false; 37 | } 38 | if (!preg_match('{'.$regexp.'}', $attribute)) { 39 | return false; 40 | } 41 | } 42 | 43 | return true; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /RequestMatcher/ExpressionRequestMatcher.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\RequestMatcher; 13 | 14 | use Symfony\Component\ExpressionLanguage\Expression; 15 | use Symfony\Component\ExpressionLanguage\ExpressionLanguage; 16 | use Symfony\Component\HttpFoundation\Request; 17 | use Symfony\Component\HttpFoundation\RequestMatcherInterface; 18 | 19 | /** 20 | * ExpressionRequestMatcher uses an expression to match a Request. 21 | * 22 | * @author Fabien Potencier 23 | */ 24 | class ExpressionRequestMatcher implements RequestMatcherInterface 25 | { 26 | public function __construct( 27 | private ExpressionLanguage $language, 28 | private Expression|string $expression, 29 | ) { 30 | } 31 | 32 | public function matches(Request $request): bool 33 | { 34 | return $this->language->evaluate($this->expression, [ 35 | 'request' => $request, 36 | 'method' => $request->getMethod(), 37 | 'path' => rawurldecode($request->getPathInfo()), 38 | 'host' => $request->getHost(), 39 | 'ip' => $request->getClientIp(), 40 | 'attributes' => $request->attributes->all(), 41 | ]); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /RequestMatcher/HeaderRequestMatcher.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\RequestMatcher; 13 | 14 | use Symfony\Component\HttpFoundation\Request; 15 | use Symfony\Component\HttpFoundation\RequestMatcherInterface; 16 | 17 | /** 18 | * Checks the presence of HTTP headers in a Request. 19 | * 20 | * @author Alexandre Daubois 21 | */ 22 | class HeaderRequestMatcher implements RequestMatcherInterface 23 | { 24 | /** 25 | * @var string[] 26 | */ 27 | private array $headers; 28 | 29 | /** 30 | * @param string[]|string $headers A header or a list of headers 31 | * Strings can contain a comma-delimited list of headers 32 | */ 33 | public function __construct(array|string $headers) 34 | { 35 | $this->headers = array_reduce((array) $headers, static fn (array $headers, string $header) => array_merge($headers, preg_split('/\s*,\s*/', $header)), []); 36 | } 37 | 38 | public function matches(Request $request): bool 39 | { 40 | if (!$this->headers) { 41 | return true; 42 | } 43 | 44 | foreach ($this->headers as $header) { 45 | if (!$request->headers->has($header)) { 46 | return false; 47 | } 48 | } 49 | 50 | return true; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /RequestMatcher/HostRequestMatcher.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\RequestMatcher; 13 | 14 | use Symfony\Component\HttpFoundation\Request; 15 | use Symfony\Component\HttpFoundation\RequestMatcherInterface; 16 | 17 | /** 18 | * Checks the Request URL host name matches a regular expression. 19 | * 20 | * @author Fabien Potencier 21 | */ 22 | class HostRequestMatcher implements RequestMatcherInterface 23 | { 24 | public function __construct(private string $regexp) 25 | { 26 | } 27 | 28 | public function matches(Request $request): bool 29 | { 30 | return preg_match('{'.$this->regexp.'}i', $request->getHost()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /RequestMatcher/IpsRequestMatcher.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\RequestMatcher; 13 | 14 | use Symfony\Component\HttpFoundation\IpUtils; 15 | use Symfony\Component\HttpFoundation\Request; 16 | use Symfony\Component\HttpFoundation\RequestMatcherInterface; 17 | 18 | /** 19 | * Checks the client IP of a Request. 20 | * 21 | * @author Fabien Potencier 22 | */ 23 | class IpsRequestMatcher implements RequestMatcherInterface 24 | { 25 | private array $ips; 26 | 27 | /** 28 | * @param string[]|string $ips A specific IP address or a range specified using IP/netmask like 192.168.1.0/24 29 | * Strings can contain a comma-delimited list of IPs/ranges 30 | */ 31 | public function __construct(array|string $ips) 32 | { 33 | $this->ips = array_reduce((array) $ips, static fn (array $ips, string $ip) => array_merge($ips, preg_split('/\s*,\s*/', $ip)), []); 34 | } 35 | 36 | public function matches(Request $request): bool 37 | { 38 | if (!$this->ips) { 39 | return true; 40 | } 41 | 42 | return IpUtils::checkIp($request->getClientIp() ?? '', $this->ips); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /RequestMatcher/IsJsonRequestMatcher.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\RequestMatcher; 13 | 14 | use Symfony\Component\HttpFoundation\Request; 15 | use Symfony\Component\HttpFoundation\RequestMatcherInterface; 16 | 17 | /** 18 | * Checks the Request content is valid JSON. 19 | * 20 | * @author Fabien Potencier 21 | */ 22 | class IsJsonRequestMatcher implements RequestMatcherInterface 23 | { 24 | public function matches(Request $request): bool 25 | { 26 | return json_validate($request->getContent()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /RequestMatcher/MethodRequestMatcher.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\RequestMatcher; 13 | 14 | use Symfony\Component\HttpFoundation\Request; 15 | use Symfony\Component\HttpFoundation\RequestMatcherInterface; 16 | 17 | /** 18 | * Checks the HTTP method of a Request. 19 | * 20 | * @author Fabien Potencier 21 | */ 22 | class MethodRequestMatcher implements RequestMatcherInterface 23 | { 24 | /** 25 | * @var string[] 26 | */ 27 | private array $methods = []; 28 | 29 | /** 30 | * @param string[]|string $methods An HTTP method or an array of HTTP methods 31 | * Strings can contain a comma-delimited list of methods 32 | */ 33 | public function __construct(array|string $methods) 34 | { 35 | $this->methods = array_reduce(array_map('strtoupper', (array) $methods), static fn (array $methods, string $method) => array_merge($methods, preg_split('/\s*,\s*/', $method)), []); 36 | } 37 | 38 | public function matches(Request $request): bool 39 | { 40 | if (!$this->methods) { 41 | return true; 42 | } 43 | 44 | return \in_array($request->getMethod(), $this->methods, true); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /RequestMatcher/PathRequestMatcher.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\RequestMatcher; 13 | 14 | use Symfony\Component\HttpFoundation\Request; 15 | use Symfony\Component\HttpFoundation\RequestMatcherInterface; 16 | 17 | /** 18 | * Checks the Request URL path info matches a regular expression. 19 | * 20 | * @author Fabien Potencier 21 | */ 22 | class PathRequestMatcher implements RequestMatcherInterface 23 | { 24 | public function __construct(private string $regexp) 25 | { 26 | } 27 | 28 | public function matches(Request $request): bool 29 | { 30 | return preg_match('{'.$this->regexp.'}', rawurldecode($request->getPathInfo())); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /RequestMatcher/PortRequestMatcher.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\RequestMatcher; 13 | 14 | use Symfony\Component\HttpFoundation\Request; 15 | use Symfony\Component\HttpFoundation\RequestMatcherInterface; 16 | 17 | /** 18 | * Checks the HTTP port of a Request. 19 | * 20 | * @author Fabien Potencier 21 | */ 22 | class PortRequestMatcher implements RequestMatcherInterface 23 | { 24 | public function __construct(private int $port) 25 | { 26 | } 27 | 28 | public function matches(Request $request): bool 29 | { 30 | return $request->getPort() === $this->port; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /RequestMatcher/QueryParameterRequestMatcher.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\RequestMatcher; 13 | 14 | use Symfony\Component\HttpFoundation\Request; 15 | use Symfony\Component\HttpFoundation\RequestMatcherInterface; 16 | 17 | /** 18 | * Checks the presence of HTTP query parameters of a Request. 19 | * 20 | * @author Alexandre Daubois 21 | */ 22 | class QueryParameterRequestMatcher implements RequestMatcherInterface 23 | { 24 | /** 25 | * @var string[] 26 | */ 27 | private array $parameters; 28 | 29 | /** 30 | * @param string[]|string $parameters A parameter or a list of parameters 31 | * Strings can contain a comma-delimited list of query parameters 32 | */ 33 | public function __construct(array|string $parameters) 34 | { 35 | $this->parameters = array_reduce(array_map(strtolower(...), (array) $parameters), static fn (array $parameters, string $parameter) => array_merge($parameters, preg_split('/\s*,\s*/', $parameter)), []); 36 | } 37 | 38 | public function matches(Request $request): bool 39 | { 40 | if (!$this->parameters) { 41 | return true; 42 | } 43 | 44 | return 0 === \count(array_diff_assoc($this->parameters, $request->query->keys())); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /RequestMatcher/SchemeRequestMatcher.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\RequestMatcher; 13 | 14 | use Symfony\Component\HttpFoundation\Request; 15 | use Symfony\Component\HttpFoundation\RequestMatcherInterface; 16 | 17 | /** 18 | * Checks the HTTP scheme of a Request. 19 | * 20 | * @author Fabien Potencier 21 | */ 22 | class SchemeRequestMatcher implements RequestMatcherInterface 23 | { 24 | /** 25 | * @var string[] 26 | */ 27 | private array $schemes; 28 | 29 | /** 30 | * @param string[]|string $schemes A scheme or a list of schemes 31 | * Strings can contain a comma-delimited list of schemes 32 | */ 33 | public function __construct(array|string $schemes) 34 | { 35 | $this->schemes = array_reduce(array_map('strtolower', (array) $schemes), static fn (array $schemes, string $scheme) => array_merge($schemes, preg_split('/\s*,\s*/', $scheme)), []); 36 | } 37 | 38 | public function matches(Request $request): bool 39 | { 40 | if (!$this->schemes) { 41 | return true; 42 | } 43 | 44 | return \in_array($request->getScheme(), $this->schemes, true); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /RequestMatcherInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation; 13 | 14 | /** 15 | * RequestMatcherInterface is an interface for strategies to match a Request. 16 | * 17 | * @author Fabien Potencier 18 | */ 19 | interface RequestMatcherInterface 20 | { 21 | /** 22 | * Decides whether the rule(s) implemented by the strategy matches the supplied request. 23 | */ 24 | public function matches(Request $request): bool; 25 | } 26 | -------------------------------------------------------------------------------- /RequestStack.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation; 13 | 14 | use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException; 15 | use Symfony\Component\HttpFoundation\Session\SessionInterface; 16 | 17 | /** 18 | * Request stack that controls the lifecycle of requests. 19 | * 20 | * @author Benjamin Eberlei 21 | */ 22 | class RequestStack 23 | { 24 | /** 25 | * @var Request[] 26 | */ 27 | private array $requests = []; 28 | 29 | /** 30 | * @param Request[] $requests 31 | */ 32 | public function __construct(array $requests = []) 33 | { 34 | foreach ($requests as $request) { 35 | $this->push($request); 36 | } 37 | } 38 | 39 | /** 40 | * Pushes a Request on the stack. 41 | * 42 | * This method should generally not be called directly as the stack 43 | * management should be taken care of by the application itself. 44 | */ 45 | public function push(Request $request): void 46 | { 47 | $this->requests[] = $request; 48 | } 49 | 50 | /** 51 | * Pops the current request from the stack. 52 | * 53 | * This operation lets the current request go out of scope. 54 | * 55 | * This method should generally not be called directly as the stack 56 | * management should be taken care of by the application itself. 57 | */ 58 | public function pop(): ?Request 59 | { 60 | if (!$this->requests) { 61 | return null; 62 | } 63 | 64 | return array_pop($this->requests); 65 | } 66 | 67 | public function getCurrentRequest(): ?Request 68 | { 69 | return end($this->requests) ?: null; 70 | } 71 | 72 | /** 73 | * Gets the main request. 74 | * 75 | * Be warned that making your code aware of the main request 76 | * might make it un-compatible with other features of your framework 77 | * like ESI support. 78 | */ 79 | public function getMainRequest(): ?Request 80 | { 81 | if (!$this->requests) { 82 | return null; 83 | } 84 | 85 | return $this->requests[0]; 86 | } 87 | 88 | /** 89 | * Returns the parent request of the current. 90 | * 91 | * Be warned that making your code aware of the parent request 92 | * might make it un-compatible with other features of your framework 93 | * like ESI support. 94 | * 95 | * If current Request is the main request, it returns null. 96 | */ 97 | public function getParentRequest(): ?Request 98 | { 99 | $pos = \count($this->requests) - 2; 100 | 101 | return $this->requests[$pos] ?? null; 102 | } 103 | 104 | /** 105 | * Gets the current session. 106 | * 107 | * @throws SessionNotFoundException 108 | */ 109 | public function getSession(): SessionInterface 110 | { 111 | if ((null !== $request = end($this->requests) ?: null) && $request->hasSession()) { 112 | return $request->getSession(); 113 | } 114 | 115 | throw new SessionNotFoundException(); 116 | } 117 | 118 | public function resetRequestFormats(): void 119 | { 120 | static $resetRequestFormats; 121 | $resetRequestFormats ??= \Closure::bind(static fn () => self::$formats = null, null, Request::class); 122 | $resetRequestFormats(); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /ServerBag.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation; 13 | 14 | /** 15 | * ServerBag is a container for HTTP headers from the $_SERVER variable. 16 | * 17 | * @author Fabien Potencier 18 | * @author Bulat Shakirzyanov 19 | * @author Robert Kiss 20 | */ 21 | class ServerBag extends ParameterBag 22 | { 23 | /** 24 | * Gets the HTTP headers. 25 | */ 26 | public function getHeaders(): array 27 | { 28 | $headers = []; 29 | foreach ($this->parameters as $key => $value) { 30 | if (str_starts_with($key, 'HTTP_')) { 31 | $headers[substr($key, 5)] = $value; 32 | } elseif (\in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH', 'CONTENT_MD5'], true) && '' !== $value) { 33 | $headers[$key] = $value; 34 | } 35 | } 36 | 37 | if (isset($this->parameters['PHP_AUTH_USER'])) { 38 | $headers['PHP_AUTH_USER'] = $this->parameters['PHP_AUTH_USER']; 39 | $headers['PHP_AUTH_PW'] = $this->parameters['PHP_AUTH_PW'] ?? ''; 40 | } else { 41 | /* 42 | * php-cgi under Apache does not pass HTTP Basic user/pass to PHP by default 43 | * For this workaround to work, add these lines to your .htaccess file: 44 | * RewriteCond %{HTTP:Authorization} .+ 45 | * RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0] 46 | * 47 | * A sample .htaccess file: 48 | * RewriteEngine On 49 | * RewriteCond %{HTTP:Authorization} .+ 50 | * RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0] 51 | * RewriteCond %{REQUEST_FILENAME} !-f 52 | * RewriteRule ^(.*)$ index.php [QSA,L] 53 | */ 54 | 55 | $authorizationHeader = null; 56 | if (isset($this->parameters['HTTP_AUTHORIZATION'])) { 57 | $authorizationHeader = $this->parameters['HTTP_AUTHORIZATION']; 58 | } elseif (isset($this->parameters['REDIRECT_HTTP_AUTHORIZATION'])) { 59 | $authorizationHeader = $this->parameters['REDIRECT_HTTP_AUTHORIZATION']; 60 | } 61 | 62 | if (null !== $authorizationHeader) { 63 | if (0 === stripos($authorizationHeader, 'basic ')) { 64 | // Decode AUTHORIZATION header into PHP_AUTH_USER and PHP_AUTH_PW when authorization header is basic 65 | $exploded = explode(':', base64_decode(substr($authorizationHeader, 6)), 2); 66 | if (2 == \count($exploded)) { 67 | [$headers['PHP_AUTH_USER'], $headers['PHP_AUTH_PW']] = $exploded; 68 | } 69 | } elseif (empty($this->parameters['PHP_AUTH_DIGEST']) && (0 === stripos($authorizationHeader, 'digest '))) { 70 | // In some circumstances PHP_AUTH_DIGEST needs to be set 71 | $headers['PHP_AUTH_DIGEST'] = $authorizationHeader; 72 | $this->parameters['PHP_AUTH_DIGEST'] = $authorizationHeader; 73 | } elseif (0 === stripos($authorizationHeader, 'bearer ')) { 74 | /* 75 | * XXX: Since there is no PHP_AUTH_BEARER in PHP predefined variables, 76 | * I'll just set $headers['AUTHORIZATION'] here. 77 | * https://php.net/reserved.variables.server 78 | */ 79 | $headers['AUTHORIZATION'] = $authorizationHeader; 80 | } 81 | } 82 | } 83 | 84 | if (isset($headers['AUTHORIZATION'])) { 85 | return $headers; 86 | } 87 | 88 | // PHP_AUTH_USER/PHP_AUTH_PW 89 | if (isset($headers['PHP_AUTH_USER'])) { 90 | $headers['AUTHORIZATION'] = 'Basic '.base64_encode($headers['PHP_AUTH_USER'].':'.($headers['PHP_AUTH_PW'] ?? '')); 91 | } elseif (isset($headers['PHP_AUTH_DIGEST'])) { 92 | $headers['AUTHORIZATION'] = $headers['PHP_AUTH_DIGEST']; 93 | } 94 | 95 | return $headers; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /ServerEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation; 13 | 14 | /** 15 | * An event generated on the server intended for streaming to the client 16 | * as part of the SSE streaming technique. 17 | * 18 | * @implements \IteratorAggregate 19 | * 20 | * @author Yonel Ceruto 21 | */ 22 | class ServerEvent implements \IteratorAggregate 23 | { 24 | /** 25 | * @param string|iterable $data The event data field for the message 26 | * @param string|null $type The event type 27 | * @param int|null $retry The number of milliseconds the client should wait 28 | * before reconnecting in case of network failure 29 | * @param string|null $id The event ID to set the EventSource object's last event ID value 30 | * @param string|null $comment The event comment 31 | */ 32 | public function __construct( 33 | private string|iterable $data, 34 | private ?string $type = null, 35 | private ?int $retry = null, 36 | private ?string $id = null, 37 | private ?string $comment = null, 38 | ) { 39 | } 40 | 41 | public function getData(): iterable|string 42 | { 43 | return $this->data; 44 | } 45 | 46 | /** 47 | * @return $this 48 | */ 49 | public function setData(iterable|string $data): static 50 | { 51 | $this->data = $data; 52 | 53 | return $this; 54 | } 55 | 56 | public function getType(): ?string 57 | { 58 | return $this->type; 59 | } 60 | 61 | /** 62 | * @return $this 63 | */ 64 | public function setType(string $type): static 65 | { 66 | $this->type = $type; 67 | 68 | return $this; 69 | } 70 | 71 | public function getRetry(): ?int 72 | { 73 | return $this->retry; 74 | } 75 | 76 | /** 77 | * @return $this 78 | */ 79 | public function setRetry(?int $retry): static 80 | { 81 | $this->retry = $retry; 82 | 83 | return $this; 84 | } 85 | 86 | public function getId(): ?string 87 | { 88 | return $this->id; 89 | } 90 | 91 | /** 92 | * @return $this 93 | */ 94 | public function setId(string $id): static 95 | { 96 | $this->id = $id; 97 | 98 | return $this; 99 | } 100 | 101 | public function getComment(): ?string 102 | { 103 | return $this->comment; 104 | } 105 | 106 | public function setComment(string $comment): static 107 | { 108 | $this->comment = $comment; 109 | 110 | return $this; 111 | } 112 | 113 | /** 114 | * @return \Traversable 115 | */ 116 | public function getIterator(): \Traversable 117 | { 118 | static $lastRetry = null; 119 | 120 | $head = ''; 121 | if ($this->comment) { 122 | $head .= \sprintf(': %s', $this->comment)."\n"; 123 | } 124 | if ($this->id) { 125 | $head .= \sprintf('id: %s', $this->id)."\n"; 126 | } 127 | if ($this->retry > 0 && $this->retry !== $lastRetry) { 128 | $head .= \sprintf('retry: %s', $lastRetry = $this->retry)."\n"; 129 | } 130 | if ($this->type) { 131 | $head .= \sprintf('event: %s', $this->type)."\n"; 132 | } 133 | yield $head; 134 | 135 | if ($this->data) { 136 | if (is_iterable($this->data)) { 137 | foreach ($this->data as $data) { 138 | yield \sprintf('data: %s', $data)."\n"; 139 | } 140 | } else { 141 | yield \sprintf('data: %s', $this->data)."\n"; 142 | } 143 | } 144 | 145 | yield "\n"; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /Session/Attribute/AttributeBag.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session\Attribute; 13 | 14 | /** 15 | * This class relates to session attribute storage. 16 | * 17 | * @implements \IteratorAggregate 18 | */ 19 | class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Countable 20 | { 21 | protected array $attributes = []; 22 | 23 | private string $name = 'attributes'; 24 | 25 | /** 26 | * @param string $storageKey The key used to store attributes in the session 27 | */ 28 | public function __construct( 29 | private string $storageKey = '_sf2_attributes', 30 | ) { 31 | } 32 | 33 | public function getName(): string 34 | { 35 | return $this->name; 36 | } 37 | 38 | public function setName(string $name): void 39 | { 40 | $this->name = $name; 41 | } 42 | 43 | public function initialize(array &$attributes): void 44 | { 45 | $this->attributes = &$attributes; 46 | } 47 | 48 | public function getStorageKey(): string 49 | { 50 | return $this->storageKey; 51 | } 52 | 53 | public function has(string $name): bool 54 | { 55 | return \array_key_exists($name, $this->attributes); 56 | } 57 | 58 | public function get(string $name, mixed $default = null): mixed 59 | { 60 | return \array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default; 61 | } 62 | 63 | public function set(string $name, mixed $value): void 64 | { 65 | $this->attributes[$name] = $value; 66 | } 67 | 68 | public function all(): array 69 | { 70 | return $this->attributes; 71 | } 72 | 73 | public function replace(array $attributes): void 74 | { 75 | $this->attributes = []; 76 | foreach ($attributes as $key => $value) { 77 | $this->set($key, $value); 78 | } 79 | } 80 | 81 | public function remove(string $name): mixed 82 | { 83 | $retval = null; 84 | if (\array_key_exists($name, $this->attributes)) { 85 | $retval = $this->attributes[$name]; 86 | unset($this->attributes[$name]); 87 | } 88 | 89 | return $retval; 90 | } 91 | 92 | public function clear(): mixed 93 | { 94 | $return = $this->attributes; 95 | $this->attributes = []; 96 | 97 | return $return; 98 | } 99 | 100 | /** 101 | * Returns an iterator for attributes. 102 | * 103 | * @return \ArrayIterator 104 | */ 105 | public function getIterator(): \ArrayIterator 106 | { 107 | return new \ArrayIterator($this->attributes); 108 | } 109 | 110 | /** 111 | * Returns the number of attributes. 112 | */ 113 | public function count(): int 114 | { 115 | return \count($this->attributes); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Session/Attribute/AttributeBagInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session\Attribute; 13 | 14 | use Symfony\Component\HttpFoundation\Session\SessionBagInterface; 15 | 16 | /** 17 | * Attributes store. 18 | * 19 | * @author Drak 20 | */ 21 | interface AttributeBagInterface extends SessionBagInterface 22 | { 23 | /** 24 | * Checks if an attribute is defined. 25 | */ 26 | public function has(string $name): bool; 27 | 28 | /** 29 | * Returns an attribute. 30 | */ 31 | public function get(string $name, mixed $default = null): mixed; 32 | 33 | /** 34 | * Sets an attribute. 35 | */ 36 | public function set(string $name, mixed $value): void; 37 | 38 | /** 39 | * Returns attributes. 40 | * 41 | * @return array 42 | */ 43 | public function all(): array; 44 | 45 | public function replace(array $attributes): void; 46 | 47 | /** 48 | * Removes an attribute. 49 | * 50 | * @return mixed The removed value or null when it does not exist 51 | */ 52 | public function remove(string $name): mixed; 53 | } 54 | -------------------------------------------------------------------------------- /Session/Flash/AutoExpireFlashBag.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session\Flash; 13 | 14 | /** 15 | * AutoExpireFlashBag flash message container. 16 | * 17 | * @author Drak 18 | */ 19 | class AutoExpireFlashBag implements FlashBagInterface 20 | { 21 | private string $name = 'flashes'; 22 | private array $flashes = ['display' => [], 'new' => []]; 23 | 24 | /** 25 | * @param string $storageKey The key used to store flashes in the session 26 | */ 27 | public function __construct( 28 | private string $storageKey = '_symfony_flashes', 29 | ) { 30 | } 31 | 32 | public function getName(): string 33 | { 34 | return $this->name; 35 | } 36 | 37 | public function setName(string $name): void 38 | { 39 | $this->name = $name; 40 | } 41 | 42 | public function initialize(array &$flashes): void 43 | { 44 | $this->flashes = &$flashes; 45 | 46 | // The logic: messages from the last request will be stored in new, so we move them to previous 47 | // This request we will show what is in 'display'. What is placed into 'new' this time round will 48 | // be moved to display next time round. 49 | $this->flashes['display'] = \array_key_exists('new', $this->flashes) ? $this->flashes['new'] : []; 50 | $this->flashes['new'] = []; 51 | } 52 | 53 | public function add(string $type, mixed $message): void 54 | { 55 | $this->flashes['new'][$type][] = $message; 56 | } 57 | 58 | public function peek(string $type, array $default = []): array 59 | { 60 | return $this->has($type) ? $this->flashes['display'][$type] : $default; 61 | } 62 | 63 | public function peekAll(): array 64 | { 65 | return \array_key_exists('display', $this->flashes) ? $this->flashes['display'] : []; 66 | } 67 | 68 | public function get(string $type, array $default = []): array 69 | { 70 | $return = $default; 71 | 72 | if (!$this->has($type)) { 73 | return $return; 74 | } 75 | 76 | if (isset($this->flashes['display'][$type])) { 77 | $return = $this->flashes['display'][$type]; 78 | unset($this->flashes['display'][$type]); 79 | } 80 | 81 | return $return; 82 | } 83 | 84 | public function all(): array 85 | { 86 | $return = $this->flashes['display']; 87 | $this->flashes['display'] = []; 88 | 89 | return $return; 90 | } 91 | 92 | public function setAll(array $messages): void 93 | { 94 | $this->flashes['new'] = $messages; 95 | } 96 | 97 | public function set(string $type, string|array $messages): void 98 | { 99 | $this->flashes['new'][$type] = (array) $messages; 100 | } 101 | 102 | public function has(string $type): bool 103 | { 104 | return \array_key_exists($type, $this->flashes['display']) && $this->flashes['display'][$type]; 105 | } 106 | 107 | public function keys(): array 108 | { 109 | return array_keys($this->flashes['display']); 110 | } 111 | 112 | public function getStorageKey(): string 113 | { 114 | return $this->storageKey; 115 | } 116 | 117 | public function clear(): mixed 118 | { 119 | return $this->all(); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Session/Flash/FlashBag.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session\Flash; 13 | 14 | /** 15 | * FlashBag flash message container. 16 | * 17 | * @author Drak 18 | */ 19 | class FlashBag implements FlashBagInterface 20 | { 21 | private string $name = 'flashes'; 22 | private array $flashes = []; 23 | 24 | /** 25 | * @param string $storageKey The key used to store flashes in the session 26 | */ 27 | public function __construct( 28 | private string $storageKey = '_symfony_flashes', 29 | ) { 30 | } 31 | 32 | public function getName(): string 33 | { 34 | return $this->name; 35 | } 36 | 37 | public function setName(string $name): void 38 | { 39 | $this->name = $name; 40 | } 41 | 42 | public function initialize(array &$flashes): void 43 | { 44 | $this->flashes = &$flashes; 45 | } 46 | 47 | public function add(string $type, mixed $message): void 48 | { 49 | $this->flashes[$type][] = $message; 50 | } 51 | 52 | public function peek(string $type, array $default = []): array 53 | { 54 | return $this->has($type) ? $this->flashes[$type] : $default; 55 | } 56 | 57 | public function peekAll(): array 58 | { 59 | return $this->flashes; 60 | } 61 | 62 | public function get(string $type, array $default = []): array 63 | { 64 | if (!$this->has($type)) { 65 | return $default; 66 | } 67 | 68 | $return = $this->flashes[$type]; 69 | 70 | unset($this->flashes[$type]); 71 | 72 | return $return; 73 | } 74 | 75 | public function all(): array 76 | { 77 | $return = $this->peekAll(); 78 | $this->flashes = []; 79 | 80 | return $return; 81 | } 82 | 83 | public function set(string $type, string|array $messages): void 84 | { 85 | $this->flashes[$type] = (array) $messages; 86 | } 87 | 88 | public function setAll(array $messages): void 89 | { 90 | $this->flashes = $messages; 91 | } 92 | 93 | public function has(string $type): bool 94 | { 95 | return \array_key_exists($type, $this->flashes) && $this->flashes[$type]; 96 | } 97 | 98 | public function keys(): array 99 | { 100 | return array_keys($this->flashes); 101 | } 102 | 103 | public function getStorageKey(): string 104 | { 105 | return $this->storageKey; 106 | } 107 | 108 | public function clear(): mixed 109 | { 110 | return $this->all(); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Session/Flash/FlashBagInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session\Flash; 13 | 14 | use Symfony\Component\HttpFoundation\Session\SessionBagInterface; 15 | 16 | /** 17 | * FlashBagInterface. 18 | * 19 | * @author Drak 20 | */ 21 | interface FlashBagInterface extends SessionBagInterface 22 | { 23 | /** 24 | * Adds a flash message for the given type. 25 | */ 26 | public function add(string $type, mixed $message): void; 27 | 28 | /** 29 | * Registers one or more messages for a given type. 30 | */ 31 | public function set(string $type, string|array $messages): void; 32 | 33 | /** 34 | * Gets flash messages for a given type. 35 | * 36 | * @param string $type Message category type 37 | * @param array $default Default value if $type does not exist 38 | */ 39 | public function peek(string $type, array $default = []): array; 40 | 41 | /** 42 | * Gets all flash messages. 43 | */ 44 | public function peekAll(): array; 45 | 46 | /** 47 | * Gets and clears flash from the stack. 48 | * 49 | * @param array $default Default value if $type does not exist 50 | */ 51 | public function get(string $type, array $default = []): array; 52 | 53 | /** 54 | * Gets and clears flashes from the stack. 55 | */ 56 | public function all(): array; 57 | 58 | /** 59 | * Sets all flash messages. 60 | */ 61 | public function setAll(array $messages): void; 62 | 63 | /** 64 | * Has flash messages for a given type? 65 | */ 66 | public function has(string $type): bool; 67 | 68 | /** 69 | * Returns a list of all defined types. 70 | */ 71 | public function keys(): array; 72 | } 73 | -------------------------------------------------------------------------------- /Session/FlashBagAwareSessionInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session; 13 | 14 | use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface; 15 | 16 | /** 17 | * Interface for session with a flashbag. 18 | */ 19 | interface FlashBagAwareSessionInterface extends SessionInterface 20 | { 21 | public function getFlashBag(): FlashBagInterface; 22 | } 23 | -------------------------------------------------------------------------------- /Session/Session.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session; 13 | 14 | use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag; 15 | use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface; 16 | use Symfony\Component\HttpFoundation\Session\Flash\FlashBag; 17 | use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface; 18 | use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag; 19 | use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; 20 | use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface; 21 | 22 | // Help opcache.preload discover always-needed symbols 23 | class_exists(AttributeBag::class); 24 | class_exists(FlashBag::class); 25 | class_exists(SessionBagProxy::class); 26 | 27 | /** 28 | * @author Fabien Potencier 29 | * @author Drak 30 | * 31 | * @implements \IteratorAggregate 32 | */ 33 | class Session implements FlashBagAwareSessionInterface, \IteratorAggregate, \Countable 34 | { 35 | protected SessionStorageInterface $storage; 36 | 37 | private string $flashName; 38 | private string $attributeName; 39 | private array $data = []; 40 | private int $usageIndex = 0; 41 | private ?\Closure $usageReporter; 42 | 43 | public function __construct(?SessionStorageInterface $storage = null, ?AttributeBagInterface $attributes = null, ?FlashBagInterface $flashes = null, ?callable $usageReporter = null) 44 | { 45 | $this->storage = $storage ?? new NativeSessionStorage(); 46 | $this->usageReporter = null === $usageReporter ? null : $usageReporter(...); 47 | 48 | $attributes ??= new AttributeBag(); 49 | $this->attributeName = $attributes->getName(); 50 | $this->registerBag($attributes); 51 | 52 | $flashes ??= new FlashBag(); 53 | $this->flashName = $flashes->getName(); 54 | $this->registerBag($flashes); 55 | } 56 | 57 | public function start(): bool 58 | { 59 | return $this->storage->start(); 60 | } 61 | 62 | public function has(string $name): bool 63 | { 64 | return $this->getAttributeBag()->has($name); 65 | } 66 | 67 | public function get(string $name, mixed $default = null): mixed 68 | { 69 | return $this->getAttributeBag()->get($name, $default); 70 | } 71 | 72 | public function set(string $name, mixed $value): void 73 | { 74 | $this->getAttributeBag()->set($name, $value); 75 | } 76 | 77 | public function all(): array 78 | { 79 | return $this->getAttributeBag()->all(); 80 | } 81 | 82 | public function replace(array $attributes): void 83 | { 84 | $this->getAttributeBag()->replace($attributes); 85 | } 86 | 87 | public function remove(string $name): mixed 88 | { 89 | return $this->getAttributeBag()->remove($name); 90 | } 91 | 92 | public function clear(): void 93 | { 94 | $this->getAttributeBag()->clear(); 95 | } 96 | 97 | public function isStarted(): bool 98 | { 99 | return $this->storage->isStarted(); 100 | } 101 | 102 | /** 103 | * Returns an iterator for attributes. 104 | * 105 | * @return \ArrayIterator 106 | */ 107 | public function getIterator(): \ArrayIterator 108 | { 109 | return new \ArrayIterator($this->getAttributeBag()->all()); 110 | } 111 | 112 | /** 113 | * Returns the number of attributes. 114 | */ 115 | public function count(): int 116 | { 117 | return \count($this->getAttributeBag()->all()); 118 | } 119 | 120 | public function &getUsageIndex(): int 121 | { 122 | return $this->usageIndex; 123 | } 124 | 125 | /** 126 | * @internal 127 | */ 128 | public function isEmpty(): bool 129 | { 130 | if ($this->isStarted()) { 131 | ++$this->usageIndex; 132 | if ($this->usageReporter && 0 <= $this->usageIndex) { 133 | ($this->usageReporter)(); 134 | } 135 | } 136 | foreach ($this->data as &$data) { 137 | if ($data) { 138 | return false; 139 | } 140 | } 141 | 142 | return true; 143 | } 144 | 145 | public function invalidate(?int $lifetime = null): bool 146 | { 147 | $this->storage->clear(); 148 | 149 | return $this->migrate(true, $lifetime); 150 | } 151 | 152 | public function migrate(bool $destroy = false, ?int $lifetime = null): bool 153 | { 154 | return $this->storage->regenerate($destroy, $lifetime); 155 | } 156 | 157 | public function save(): void 158 | { 159 | $this->storage->save(); 160 | } 161 | 162 | public function getId(): string 163 | { 164 | return $this->storage->getId(); 165 | } 166 | 167 | public function setId(string $id): void 168 | { 169 | if ($this->storage->getId() !== $id) { 170 | $this->storage->setId($id); 171 | } 172 | } 173 | 174 | public function getName(): string 175 | { 176 | return $this->storage->getName(); 177 | } 178 | 179 | public function setName(string $name): void 180 | { 181 | $this->storage->setName($name); 182 | } 183 | 184 | public function getMetadataBag(): MetadataBag 185 | { 186 | ++$this->usageIndex; 187 | if ($this->usageReporter && 0 <= $this->usageIndex) { 188 | ($this->usageReporter)(); 189 | } 190 | 191 | return $this->storage->getMetadataBag(); 192 | } 193 | 194 | public function registerBag(SessionBagInterface $bag): void 195 | { 196 | $this->storage->registerBag(new SessionBagProxy($bag, $this->data, $this->usageIndex, $this->usageReporter)); 197 | } 198 | 199 | public function getBag(string $name): SessionBagInterface 200 | { 201 | $bag = $this->storage->getBag($name); 202 | 203 | return method_exists($bag, 'getBag') ? $bag->getBag() : $bag; 204 | } 205 | 206 | /** 207 | * Gets the flashbag interface. 208 | */ 209 | public function getFlashBag(): FlashBagInterface 210 | { 211 | return $this->getBag($this->flashName); 212 | } 213 | 214 | /** 215 | * Gets the attributebag interface. 216 | * 217 | * Note that this method was added to help with IDE autocompletion. 218 | */ 219 | private function getAttributeBag(): AttributeBagInterface 220 | { 221 | return $this->getBag($this->attributeName); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /Session/SessionBagInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session; 13 | 14 | /** 15 | * Session Bag store. 16 | * 17 | * @author Drak 18 | */ 19 | interface SessionBagInterface 20 | { 21 | /** 22 | * Gets this bag's name. 23 | */ 24 | public function getName(): string; 25 | 26 | /** 27 | * Initializes the Bag. 28 | */ 29 | public function initialize(array &$array): void; 30 | 31 | /** 32 | * Gets the storage key for this bag. 33 | */ 34 | public function getStorageKey(): string; 35 | 36 | /** 37 | * Clears out data from bag. 38 | * 39 | * @return mixed Whatever data was contained 40 | */ 41 | public function clear(): mixed; 42 | } 43 | -------------------------------------------------------------------------------- /Session/SessionBagProxy.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session; 13 | 14 | /** 15 | * @author Nicolas Grekas 16 | * 17 | * @internal 18 | */ 19 | final class SessionBagProxy implements SessionBagInterface 20 | { 21 | private array $data; 22 | private ?int $usageIndex; 23 | private ?\Closure $usageReporter; 24 | 25 | public function __construct( 26 | private SessionBagInterface $bag, 27 | array &$data, 28 | ?int &$usageIndex, 29 | ?callable $usageReporter, 30 | ) { 31 | $this->bag = $bag; 32 | $this->data = &$data; 33 | $this->usageIndex = &$usageIndex; 34 | $this->usageReporter = null === $usageReporter ? null : $usageReporter(...); 35 | } 36 | 37 | public function getBag(): SessionBagInterface 38 | { 39 | ++$this->usageIndex; 40 | if ($this->usageReporter && 0 <= $this->usageIndex) { 41 | ($this->usageReporter)(); 42 | } 43 | 44 | return $this->bag; 45 | } 46 | 47 | public function isEmpty(): bool 48 | { 49 | if (!isset($this->data[$this->bag->getStorageKey()])) { 50 | return true; 51 | } 52 | ++$this->usageIndex; 53 | if ($this->usageReporter && 0 <= $this->usageIndex) { 54 | ($this->usageReporter)(); 55 | } 56 | 57 | return empty($this->data[$this->bag->getStorageKey()]); 58 | } 59 | 60 | public function getName(): string 61 | { 62 | return $this->bag->getName(); 63 | } 64 | 65 | public function initialize(array &$array): void 66 | { 67 | ++$this->usageIndex; 68 | if ($this->usageReporter && 0 <= $this->usageIndex) { 69 | ($this->usageReporter)(); 70 | } 71 | 72 | $this->data[$this->bag->getStorageKey()] = &$array; 73 | 74 | $this->bag->initialize($array); 75 | } 76 | 77 | public function getStorageKey(): string 78 | { 79 | return $this->bag->getStorageKey(); 80 | } 81 | 82 | public function clear(): mixed 83 | { 84 | return $this->bag->clear(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Session/SessionFactory.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session; 13 | 14 | use Symfony\Component\HttpFoundation\RequestStack; 15 | use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageFactoryInterface; 16 | 17 | // Help opcache.preload discover always-needed symbols 18 | class_exists(Session::class); 19 | 20 | /** 21 | * @author Jérémy Derussé 22 | */ 23 | class SessionFactory implements SessionFactoryInterface 24 | { 25 | private ?\Closure $usageReporter; 26 | 27 | public function __construct( 28 | private RequestStack $requestStack, 29 | private SessionStorageFactoryInterface $storageFactory, 30 | ?callable $usageReporter = null, 31 | ) { 32 | $this->usageReporter = null === $usageReporter ? null : $usageReporter(...); 33 | } 34 | 35 | public function createSession(): SessionInterface 36 | { 37 | return new Session($this->storageFactory->createStorage($this->requestStack->getMainRequest()), null, null, $this->usageReporter); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Session/SessionFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session; 13 | 14 | /** 15 | * @author Kevin Bond 16 | */ 17 | interface SessionFactoryInterface 18 | { 19 | public function createSession(): SessionInterface; 20 | } 21 | -------------------------------------------------------------------------------- /Session/SessionInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session; 13 | 14 | use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag; 15 | 16 | /** 17 | * Interface for the session. 18 | * 19 | * @author Drak 20 | */ 21 | interface SessionInterface 22 | { 23 | /** 24 | * Starts the session storage. 25 | * 26 | * @throws \RuntimeException if session fails to start 27 | */ 28 | public function start(): bool; 29 | 30 | /** 31 | * Returns the session ID. 32 | */ 33 | public function getId(): string; 34 | 35 | /** 36 | * Sets the session ID. 37 | */ 38 | public function setId(string $id): void; 39 | 40 | /** 41 | * Returns the session name. 42 | */ 43 | public function getName(): string; 44 | 45 | /** 46 | * Sets the session name. 47 | */ 48 | public function setName(string $name): void; 49 | 50 | /** 51 | * Invalidates the current session. 52 | * 53 | * Clears all session attributes and flashes and regenerates the 54 | * session and deletes the old session from persistence. 55 | * 56 | * @param int|null $lifetime Sets the cookie lifetime for the session cookie. A null value 57 | * will leave the system settings unchanged, 0 sets the cookie 58 | * to expire with browser session. Time is in seconds, and is 59 | * not a Unix timestamp. 60 | */ 61 | public function invalidate(?int $lifetime = null): bool; 62 | 63 | /** 64 | * Migrates the current session to a new session id while maintaining all 65 | * session attributes. 66 | * 67 | * @param bool $destroy Whether to delete the old session or leave it to garbage collection 68 | * @param int|null $lifetime Sets the cookie lifetime for the session cookie. A null value 69 | * will leave the system settings unchanged, 0 sets the cookie 70 | * to expire with browser session. Time is in seconds, and is 71 | * not a Unix timestamp. 72 | */ 73 | public function migrate(bool $destroy = false, ?int $lifetime = null): bool; 74 | 75 | /** 76 | * Force the session to be saved and closed. 77 | * 78 | * This method is generally not required for real sessions as 79 | * the session will be automatically saved at the end of 80 | * code execution. 81 | */ 82 | public function save(): void; 83 | 84 | /** 85 | * Checks if an attribute is defined. 86 | */ 87 | public function has(string $name): bool; 88 | 89 | /** 90 | * Returns an attribute. 91 | */ 92 | public function get(string $name, mixed $default = null): mixed; 93 | 94 | /** 95 | * Sets an attribute. 96 | */ 97 | public function set(string $name, mixed $value): void; 98 | 99 | /** 100 | * Returns attributes. 101 | */ 102 | public function all(): array; 103 | 104 | /** 105 | * Sets attributes. 106 | */ 107 | public function replace(array $attributes): void; 108 | 109 | /** 110 | * Removes an attribute. 111 | * 112 | * @return mixed The removed value or null when it does not exist 113 | */ 114 | public function remove(string $name): mixed; 115 | 116 | /** 117 | * Clears all attributes. 118 | */ 119 | public function clear(): void; 120 | 121 | /** 122 | * Checks if the session was started. 123 | */ 124 | public function isStarted(): bool; 125 | 126 | /** 127 | * Registers a SessionBagInterface with the session. 128 | */ 129 | public function registerBag(SessionBagInterface $bag): void; 130 | 131 | /** 132 | * Gets a bag instance by name. 133 | */ 134 | public function getBag(string $name): SessionBagInterface; 135 | 136 | /** 137 | * Gets session meta. 138 | */ 139 | public function getMetadataBag(): MetadataBag; 140 | } 141 | -------------------------------------------------------------------------------- /Session/SessionUtils.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session; 13 | 14 | /** 15 | * Session utility functions. 16 | * 17 | * @author Nicolas Grekas 18 | * @author Rémon van de Kamp 19 | * 20 | * @internal 21 | */ 22 | final class SessionUtils 23 | { 24 | /** 25 | * Finds the session header amongst the headers that are to be sent, removes it, and returns 26 | * it so the caller can process it further. 27 | */ 28 | public static function popSessionCookie(string $sessionName, #[\SensitiveParameter] string $sessionId): ?string 29 | { 30 | $sessionCookie = null; 31 | $sessionCookiePrefix = \sprintf(' %s=', urlencode($sessionName)); 32 | $sessionCookieWithId = \sprintf('%s%s;', $sessionCookiePrefix, urlencode($sessionId)); 33 | $otherCookies = []; 34 | foreach (headers_list() as $h) { 35 | if (0 !== stripos($h, 'Set-Cookie:')) { 36 | continue; 37 | } 38 | if (11 === strpos($h, $sessionCookiePrefix, 11)) { 39 | $sessionCookie = $h; 40 | 41 | if (11 !== strpos($h, $sessionCookieWithId, 11)) { 42 | $otherCookies[] = $h; 43 | } 44 | } else { 45 | $otherCookies[] = $h; 46 | } 47 | } 48 | if (null === $sessionCookie) { 49 | return null; 50 | } 51 | 52 | header_remove('Set-Cookie'); 53 | foreach ($otherCookies as $h) { 54 | header($h, false); 55 | } 56 | 57 | return $sessionCookie; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Session/Storage/Handler/AbstractSessionHandler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; 13 | 14 | use Symfony\Component\HttpFoundation\Session\SessionUtils; 15 | 16 | /** 17 | * This abstract session handler provides a generic implementation 18 | * of the PHP 7.0 SessionUpdateTimestampHandlerInterface, 19 | * enabling strict and lazy session handling. 20 | * 21 | * @author Nicolas Grekas 22 | */ 23 | abstract class AbstractSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface 24 | { 25 | private string $sessionName; 26 | private string $prefetchId; 27 | private string $prefetchData; 28 | private ?string $newSessionId = null; 29 | private string $igbinaryEmptyData; 30 | 31 | public function open(string $savePath, string $sessionName): bool 32 | { 33 | $this->sessionName = $sessionName; 34 | if (!headers_sent() && !\ini_get('session.cache_limiter') && '0' !== \ini_get('session.cache_limiter')) { 35 | header(\sprintf('Cache-Control: max-age=%d, private, must-revalidate', 60 * (int) \ini_get('session.cache_expire'))); 36 | } 37 | 38 | return true; 39 | } 40 | 41 | abstract protected function doRead(#[\SensitiveParameter] string $sessionId): string; 42 | 43 | abstract protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool; 44 | 45 | abstract protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool; 46 | 47 | public function validateId(#[\SensitiveParameter] string $sessionId): bool 48 | { 49 | $this->prefetchData = $this->read($sessionId); 50 | $this->prefetchId = $sessionId; 51 | 52 | return '' !== $this->prefetchData; 53 | } 54 | 55 | public function read(#[\SensitiveParameter] string $sessionId): string 56 | { 57 | if (isset($this->prefetchId)) { 58 | $prefetchId = $this->prefetchId; 59 | $prefetchData = $this->prefetchData; 60 | unset($this->prefetchId, $this->prefetchData); 61 | 62 | if ($prefetchId === $sessionId || '' === $prefetchData) { 63 | $this->newSessionId = '' === $prefetchData ? $sessionId : null; 64 | 65 | return $prefetchData; 66 | } 67 | } 68 | 69 | $data = $this->doRead($sessionId); 70 | $this->newSessionId = '' === $data ? $sessionId : null; 71 | 72 | return $data; 73 | } 74 | 75 | public function write(#[\SensitiveParameter] string $sessionId, string $data): bool 76 | { 77 | // see https://github.com/igbinary/igbinary/issues/146 78 | $this->igbinaryEmptyData ??= \function_exists('igbinary_serialize') ? igbinary_serialize([]) : ''; 79 | if ('' === $data || $this->igbinaryEmptyData === $data) { 80 | return $this->destroy($sessionId); 81 | } 82 | $this->newSessionId = null; 83 | 84 | return $this->doWrite($sessionId, $data); 85 | } 86 | 87 | public function destroy(#[\SensitiveParameter] string $sessionId): bool 88 | { 89 | if (!headers_sent() && filter_var(\ini_get('session.use_cookies'), \FILTER_VALIDATE_BOOL)) { 90 | if (!isset($this->sessionName)) { 91 | throw new \LogicException(\sprintf('Session name cannot be empty, did you forget to call "parent::open()" in "%s"?.', static::class)); 92 | } 93 | $cookie = SessionUtils::popSessionCookie($this->sessionName, $sessionId); 94 | 95 | /* 96 | * We send an invalidation Set-Cookie header (zero lifetime) 97 | * when either the session was started or a cookie with 98 | * the session name was sent by the client (in which case 99 | * we know it's invalid as a valid session cookie would've 100 | * started the session). 101 | */ 102 | if (null === $cookie || isset($_COOKIE[$this->sessionName])) { 103 | $params = session_get_cookie_params(); 104 | unset($params['lifetime']); 105 | setcookie($this->sessionName, '', $params); 106 | } 107 | } 108 | 109 | return $this->newSessionId === $sessionId || $this->doDestroy($sessionId); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Session/Storage/Handler/IdentityMarshaller.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; 13 | 14 | use Symfony\Component\Cache\Marshaller\MarshallerInterface; 15 | 16 | /** 17 | * @author Ahmed TAILOULOUTE 18 | */ 19 | class IdentityMarshaller implements MarshallerInterface 20 | { 21 | public function marshall(array $values, ?array &$failed): array 22 | { 23 | foreach ($values as $key => $value) { 24 | if (!\is_string($value)) { 25 | throw new \LogicException(\sprintf('%s accepts only string as data.', __METHOD__)); 26 | } 27 | } 28 | 29 | return $values; 30 | } 31 | 32 | public function unmarshall(string $value): string 33 | { 34 | return $value; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Session/Storage/Handler/MarshallingSessionHandler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; 13 | 14 | use Symfony\Component\Cache\Marshaller\MarshallerInterface; 15 | 16 | /** 17 | * @author Ahmed TAILOULOUTE 18 | */ 19 | class MarshallingSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface 20 | { 21 | public function __construct( 22 | private AbstractSessionHandler $handler, 23 | private MarshallerInterface $marshaller, 24 | ) { 25 | } 26 | 27 | public function open(string $savePath, string $name): bool 28 | { 29 | return $this->handler->open($savePath, $name); 30 | } 31 | 32 | public function close(): bool 33 | { 34 | return $this->handler->close(); 35 | } 36 | 37 | public function destroy(#[\SensitiveParameter] string $sessionId): bool 38 | { 39 | return $this->handler->destroy($sessionId); 40 | } 41 | 42 | public function gc(int $maxlifetime): int|false 43 | { 44 | return $this->handler->gc($maxlifetime); 45 | } 46 | 47 | public function read(#[\SensitiveParameter] string $sessionId): string 48 | { 49 | return $this->marshaller->unmarshall($this->handler->read($sessionId)); 50 | } 51 | 52 | public function write(#[\SensitiveParameter] string $sessionId, string $data): bool 53 | { 54 | $failed = []; 55 | $marshalledData = $this->marshaller->marshall(['data' => $data], $failed); 56 | 57 | if (isset($failed['data'])) { 58 | return false; 59 | } 60 | 61 | return $this->handler->write($sessionId, $marshalledData['data']); 62 | } 63 | 64 | public function validateId(#[\SensitiveParameter] string $sessionId): bool 65 | { 66 | return $this->handler->validateId($sessionId); 67 | } 68 | 69 | public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool 70 | { 71 | return $this->handler->updateTimestamp($sessionId, $data); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Session/Storage/Handler/MemcachedSessionHandler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; 13 | 14 | /** 15 | * Memcached based session storage handler based on the Memcached class 16 | * provided by the PHP memcached extension. 17 | * 18 | * @see https://php.net/memcached 19 | * 20 | * @author Drak 21 | */ 22 | class MemcachedSessionHandler extends AbstractSessionHandler 23 | { 24 | /** 25 | * Time to live in seconds. 26 | */ 27 | private int|\Closure|null $ttl; 28 | 29 | /** 30 | * Key prefix for shared environments. 31 | */ 32 | private string $prefix; 33 | 34 | /** 35 | * Constructor. 36 | * 37 | * List of available options: 38 | * * prefix: The prefix to use for the memcached keys in order to avoid collision 39 | * * ttl: The time to live in seconds. 40 | * 41 | * @throws \InvalidArgumentException When unsupported options are passed 42 | */ 43 | public function __construct( 44 | private \Memcached $memcached, 45 | array $options = [], 46 | ) { 47 | if ($diff = array_diff(array_keys($options), ['prefix', 'expiretime', 'ttl'])) { 48 | throw new \InvalidArgumentException(\sprintf('The following options are not supported "%s".', implode(', ', $diff))); 49 | } 50 | 51 | $this->ttl = $options['expiretime'] ?? $options['ttl'] ?? null; 52 | $this->prefix = $options['prefix'] ?? 'sf2s'; 53 | } 54 | 55 | public function close(): bool 56 | { 57 | return $this->memcached->quit(); 58 | } 59 | 60 | protected function doRead(#[\SensitiveParameter] string $sessionId): string 61 | { 62 | return $this->memcached->get($this->prefix.$sessionId) ?: ''; 63 | } 64 | 65 | public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool 66 | { 67 | $this->memcached->touch($this->prefix.$sessionId, $this->getCompatibleTtl()); 68 | 69 | return true; 70 | } 71 | 72 | protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool 73 | { 74 | return $this->memcached->set($this->prefix.$sessionId, $data, $this->getCompatibleTtl()); 75 | } 76 | 77 | private function getCompatibleTtl(): int 78 | { 79 | $ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime'); 80 | 81 | // If the relative TTL that is used exceeds 30 days, memcached will treat the value as Unix time. 82 | // We have to convert it to an absolute Unix time at this point, to make sure the TTL is correct. 83 | if ($ttl > 60 * 60 * 24 * 30) { 84 | $ttl += time(); 85 | } 86 | 87 | return $ttl; 88 | } 89 | 90 | protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool 91 | { 92 | $result = $this->memcached->delete($this->prefix.$sessionId); 93 | 94 | return $result || \Memcached::RES_NOTFOUND == $this->memcached->getResultCode(); 95 | } 96 | 97 | public function gc(int $maxlifetime): int|false 98 | { 99 | // not required here because memcached will auto expire the records anyhow. 100 | return 0; 101 | } 102 | 103 | /** 104 | * Return a Memcached instance. 105 | */ 106 | protected function getMemcached(): \Memcached 107 | { 108 | return $this->memcached; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Session/Storage/Handler/MigratingSessionHandler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; 13 | 14 | /** 15 | * Migrating session handler for migrating from one handler to another. It reads 16 | * from the current handler and writes both the current and new ones. 17 | * 18 | * It ignores errors from the new handler. 19 | * 20 | * @author Ross Motley 21 | * @author Oliver Radwell 22 | */ 23 | class MigratingSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface 24 | { 25 | private \SessionHandlerInterface&\SessionUpdateTimestampHandlerInterface $currentHandler; 26 | private \SessionHandlerInterface&\SessionUpdateTimestampHandlerInterface $writeOnlyHandler; 27 | 28 | public function __construct(\SessionHandlerInterface $currentHandler, \SessionHandlerInterface $writeOnlyHandler) 29 | { 30 | if (!$currentHandler instanceof \SessionUpdateTimestampHandlerInterface) { 31 | $currentHandler = new StrictSessionHandler($currentHandler); 32 | } 33 | if (!$writeOnlyHandler instanceof \SessionUpdateTimestampHandlerInterface) { 34 | $writeOnlyHandler = new StrictSessionHandler($writeOnlyHandler); 35 | } 36 | 37 | $this->currentHandler = $currentHandler; 38 | $this->writeOnlyHandler = $writeOnlyHandler; 39 | } 40 | 41 | public function close(): bool 42 | { 43 | $result = $this->currentHandler->close(); 44 | $this->writeOnlyHandler->close(); 45 | 46 | return $result; 47 | } 48 | 49 | public function destroy(#[\SensitiveParameter] string $sessionId): bool 50 | { 51 | $result = $this->currentHandler->destroy($sessionId); 52 | $this->writeOnlyHandler->destroy($sessionId); 53 | 54 | return $result; 55 | } 56 | 57 | public function gc(int $maxlifetime): int|false 58 | { 59 | $result = $this->currentHandler->gc($maxlifetime); 60 | $this->writeOnlyHandler->gc($maxlifetime); 61 | 62 | return $result; 63 | } 64 | 65 | public function open(string $savePath, string $sessionName): bool 66 | { 67 | $result = $this->currentHandler->open($savePath, $sessionName); 68 | $this->writeOnlyHandler->open($savePath, $sessionName); 69 | 70 | return $result; 71 | } 72 | 73 | public function read(#[\SensitiveParameter] string $sessionId): string 74 | { 75 | // No reading from new handler until switch-over 76 | return $this->currentHandler->read($sessionId); 77 | } 78 | 79 | public function write(#[\SensitiveParameter] string $sessionId, string $sessionData): bool 80 | { 81 | $result = $this->currentHandler->write($sessionId, $sessionData); 82 | $this->writeOnlyHandler->write($sessionId, $sessionData); 83 | 84 | return $result; 85 | } 86 | 87 | public function validateId(#[\SensitiveParameter] string $sessionId): bool 88 | { 89 | // No reading from new handler until switch-over 90 | return $this->currentHandler->validateId($sessionId); 91 | } 92 | 93 | public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $sessionData): bool 94 | { 95 | $result = $this->currentHandler->updateTimestamp($sessionId, $sessionData); 96 | $this->writeOnlyHandler->updateTimestamp($sessionId, $sessionData); 97 | 98 | return $result; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Session/Storage/Handler/MongoDbSessionHandler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; 13 | 14 | use MongoDB\BSON\Binary; 15 | use MongoDB\BSON\UTCDateTime; 16 | use MongoDB\Client; 17 | use MongoDB\Driver\BulkWrite; 18 | use MongoDB\Driver\Manager; 19 | use MongoDB\Driver\Query; 20 | 21 | /** 22 | * Session handler using the MongoDB driver extension. 23 | * 24 | * @author Markus Bachmann 25 | * @author Jérôme Tamarelle 26 | * 27 | * @see https://php.net/mongodb 28 | */ 29 | class MongoDbSessionHandler extends AbstractSessionHandler 30 | { 31 | private Manager $manager; 32 | private string $namespace; 33 | private array $options; 34 | private int|\Closure|null $ttl; 35 | 36 | /** 37 | * Constructor. 38 | * 39 | * List of available options: 40 | * * database: The name of the database [required] 41 | * * collection: The name of the collection [required] 42 | * * id_field: The field name for storing the session id [default: _id] 43 | * * data_field: The field name for storing the session data [default: data] 44 | * * time_field: The field name for storing the timestamp [default: time] 45 | * * expiry_field: The field name for storing the expiry-timestamp [default: expires_at] 46 | * * ttl: The time to live in seconds. 47 | * 48 | * It is strongly recommended to put an index on the `expiry_field` for 49 | * garbage-collection. Alternatively it's possible to automatically expire 50 | * the sessions in the database as described below: 51 | * 52 | * A TTL collections can be used on MongoDB 2.2+ to cleanup expired sessions 53 | * automatically. Such an index can for example look like this: 54 | * 55 | * db..createIndex( 56 | * { "": 1 }, 57 | * { "expireAfterSeconds": 0 } 58 | * ) 59 | * 60 | * More details on: https://docs.mongodb.org/manual/tutorial/expire-data/ 61 | * 62 | * If you use such an index, you can drop `gc_probability` to 0 since 63 | * no garbage-collection is required. 64 | * 65 | * @throws \InvalidArgumentException When "database" or "collection" not provided 66 | */ 67 | public function __construct(Client|Manager $mongo, array $options) 68 | { 69 | if (!isset($options['database']) || !isset($options['collection'])) { 70 | throw new \InvalidArgumentException('You must provide the "database" and "collection" option for MongoDBSessionHandler.'); 71 | } 72 | 73 | if ($mongo instanceof Client) { 74 | $mongo = $mongo->getManager(); 75 | } 76 | 77 | $this->manager = $mongo; 78 | $this->namespace = $options['database'].'.'.$options['collection']; 79 | 80 | $this->options = array_merge([ 81 | 'id_field' => '_id', 82 | 'data_field' => 'data', 83 | 'time_field' => 'time', 84 | 'expiry_field' => 'expires_at', 85 | ], $options); 86 | $this->ttl = $this->options['ttl'] ?? null; 87 | } 88 | 89 | public function close(): bool 90 | { 91 | return true; 92 | } 93 | 94 | protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool 95 | { 96 | $write = new BulkWrite(); 97 | $write->delete( 98 | [$this->options['id_field'] => $sessionId], 99 | ['limit' => 1] 100 | ); 101 | 102 | $this->manager->executeBulkWrite($this->namespace, $write); 103 | 104 | return true; 105 | } 106 | 107 | public function gc(int $maxlifetime): int|false 108 | { 109 | $write = new BulkWrite(); 110 | $write->delete( 111 | [$this->options['expiry_field'] => ['$lt' => $this->getUTCDateTime()]], 112 | ); 113 | $result = $this->manager->executeBulkWrite($this->namespace, $write); 114 | 115 | return $result->getDeletedCount() ?? false; 116 | } 117 | 118 | protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool 119 | { 120 | $ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime'); 121 | $expiry = $this->getUTCDateTime($ttl); 122 | 123 | $fields = [ 124 | $this->options['time_field'] => $this->getUTCDateTime(), 125 | $this->options['expiry_field'] => $expiry, 126 | $this->options['data_field'] => new Binary($data, Binary::TYPE_GENERIC), 127 | ]; 128 | 129 | $write = new BulkWrite(); 130 | $write->update( 131 | [$this->options['id_field'] => $sessionId], 132 | ['$set' => $fields], 133 | ['upsert' => true] 134 | ); 135 | 136 | $this->manager->executeBulkWrite($this->namespace, $write); 137 | 138 | return true; 139 | } 140 | 141 | public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool 142 | { 143 | $ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime'); 144 | $expiry = $this->getUTCDateTime($ttl); 145 | 146 | $write = new BulkWrite(); 147 | $write->update( 148 | [$this->options['id_field'] => $sessionId], 149 | ['$set' => [ 150 | $this->options['time_field'] => $this->getUTCDateTime(), 151 | $this->options['expiry_field'] => $expiry, 152 | ]], 153 | ['multi' => false], 154 | ); 155 | 156 | $this->manager->executeBulkWrite($this->namespace, $write); 157 | 158 | return true; 159 | } 160 | 161 | protected function doRead(#[\SensitiveParameter] string $sessionId): string 162 | { 163 | $cursor = $this->manager->executeQuery($this->namespace, new Query([ 164 | $this->options['id_field'] => $sessionId, 165 | $this->options['expiry_field'] => ['$gte' => $this->getUTCDateTime()], 166 | ], [ 167 | 'projection' => [ 168 | '_id' => false, 169 | $this->options['data_field'] => true, 170 | ], 171 | 'limit' => 1, 172 | ])); 173 | 174 | foreach ($cursor as $document) { 175 | return (string) $document->{$this->options['data_field']} ?? ''; 176 | } 177 | 178 | // Not found 179 | return ''; 180 | } 181 | 182 | private function getUTCDateTime(int $additionalSeconds = 0): UTCDateTime 183 | { 184 | return new UTCDateTime((time() + $additionalSeconds) * 1000); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /Session/Storage/Handler/NativeFileSessionHandler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; 13 | 14 | /** 15 | * Native session handler using PHP's built in file storage. 16 | * 17 | * @author Drak 18 | */ 19 | class NativeFileSessionHandler extends \SessionHandler 20 | { 21 | /** 22 | * @param string|null $savePath Path of directory to save session files 23 | * Default null will leave setting as defined by PHP. 24 | * '/path', 'N;/path', or 'N;octal-mode;/path 25 | * 26 | * @see https://php.net/session.configuration#ini.session.save-path for further details. 27 | * 28 | * @throws \InvalidArgumentException On invalid $savePath 29 | * @throws \RuntimeException When failing to create the save directory 30 | */ 31 | public function __construct(?string $savePath = null) 32 | { 33 | $baseDir = $savePath ??= \ini_get('session.save_path'); 34 | 35 | if ($count = substr_count($savePath, ';')) { 36 | if ($count > 2) { 37 | throw new \InvalidArgumentException(\sprintf('Invalid argument $savePath \'%s\'.', $savePath)); 38 | } 39 | 40 | // characters after last ';' are the path 41 | $baseDir = ltrim(strrchr($savePath, ';'), ';'); 42 | } 43 | 44 | if ($baseDir && !is_dir($baseDir) && !@mkdir($baseDir, 0777, true) && !is_dir($baseDir)) { 45 | throw new \RuntimeException(\sprintf('Session Storage was not able to create directory "%s".', $baseDir)); 46 | } 47 | 48 | if ($savePath !== \ini_get('session.save_path')) { 49 | ini_set('session.save_path', $savePath); 50 | } 51 | if ('files' !== \ini_get('session.save_handler')) { 52 | ini_set('session.save_handler', 'files'); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Session/Storage/Handler/NullSessionHandler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; 13 | 14 | /** 15 | * Can be used in unit testing or in a situations where persisted sessions are not desired. 16 | * 17 | * @author Drak 18 | */ 19 | class NullSessionHandler extends AbstractSessionHandler 20 | { 21 | public function close(): bool 22 | { 23 | return true; 24 | } 25 | 26 | public function validateId(#[\SensitiveParameter] string $sessionId): bool 27 | { 28 | return true; 29 | } 30 | 31 | protected function doRead(#[\SensitiveParameter] string $sessionId): string 32 | { 33 | return ''; 34 | } 35 | 36 | public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool 37 | { 38 | return true; 39 | } 40 | 41 | protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool 42 | { 43 | return true; 44 | } 45 | 46 | protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool 47 | { 48 | return true; 49 | } 50 | 51 | public function gc(int $maxlifetime): int|false 52 | { 53 | return 0; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Session/Storage/Handler/RedisSessionHandler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; 13 | 14 | use Predis\Response\ErrorInterface; 15 | use Relay\Relay; 16 | 17 | /** 18 | * Redis based session storage handler based on the Redis class 19 | * provided by the PHP redis extension. 20 | * 21 | * @author Dalibor Karlović 22 | */ 23 | class RedisSessionHandler extends AbstractSessionHandler 24 | { 25 | /** 26 | * Key prefix for shared environments. 27 | */ 28 | private string $prefix; 29 | 30 | /** 31 | * Time to live in seconds. 32 | */ 33 | private int|\Closure|null $ttl; 34 | 35 | /** 36 | * List of available options: 37 | * * prefix: The prefix to use for the keys in order to avoid collision on the Redis server 38 | * * ttl: The time to live in seconds. 39 | * 40 | * @throws \InvalidArgumentException When unsupported client or options are passed 41 | */ 42 | public function __construct( 43 | private \Redis|Relay|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis, 44 | array $options = [], 45 | ) { 46 | if ($diff = array_diff(array_keys($options), ['prefix', 'ttl'])) { 47 | throw new \InvalidArgumentException(\sprintf('The following options are not supported "%s".', implode(', ', $diff))); 48 | } 49 | 50 | $this->prefix = $options['prefix'] ?? 'sf_s'; 51 | $this->ttl = $options['ttl'] ?? null; 52 | } 53 | 54 | protected function doRead(#[\SensitiveParameter] string $sessionId): string 55 | { 56 | return $this->redis->get($this->prefix.$sessionId) ?: ''; 57 | } 58 | 59 | protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool 60 | { 61 | $ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime'); 62 | $result = $this->redis->setEx($this->prefix.$sessionId, (int) $ttl, $data); 63 | 64 | return $result && !$result instanceof ErrorInterface; 65 | } 66 | 67 | protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool 68 | { 69 | static $unlink = true; 70 | 71 | if ($unlink) { 72 | try { 73 | $unlink = false !== $this->redis->unlink($this->prefix.$sessionId); 74 | } catch (\Throwable) { 75 | $unlink = false; 76 | } 77 | } 78 | 79 | if (!$unlink) { 80 | $this->redis->del($this->prefix.$sessionId); 81 | } 82 | 83 | return true; 84 | } 85 | 86 | #[\ReturnTypeWillChange] 87 | public function close(): bool 88 | { 89 | return true; 90 | } 91 | 92 | public function gc(int $maxlifetime): int|false 93 | { 94 | return 0; 95 | } 96 | 97 | public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool 98 | { 99 | $ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime'); 100 | 101 | return $this->redis->expire($this->prefix.$sessionId, (int) $ttl); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Session/Storage/Handler/SessionHandlerFactory.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; 13 | 14 | use Doctrine\DBAL\Configuration; 15 | use Doctrine\DBAL\DriverManager; 16 | use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory; 17 | use Doctrine\DBAL\Tools\DsnParser; 18 | use Relay\Relay; 19 | use Symfony\Component\Cache\Adapter\AbstractAdapter; 20 | 21 | /** 22 | * @author Nicolas Grekas 23 | */ 24 | class SessionHandlerFactory 25 | { 26 | public static function createHandler(object|string $connection, array $options = []): AbstractSessionHandler 27 | { 28 | if ($query = \is_string($connection) ? parse_url($connection) : false) { 29 | parse_str($query['query'] ?? '', $query); 30 | 31 | if (($options['ttl'] ?? null) instanceof \Closure) { 32 | $query['ttl'] = $options['ttl']; 33 | } 34 | } 35 | $options = ($query ?: []) + $options; 36 | 37 | switch (true) { 38 | case $connection instanceof \Redis: 39 | case $connection instanceof Relay: 40 | case $connection instanceof \RedisArray: 41 | case $connection instanceof \RedisCluster: 42 | case $connection instanceof \Predis\ClientInterface: 43 | return new RedisSessionHandler($connection); 44 | 45 | case $connection instanceof \Memcached: 46 | return new MemcachedSessionHandler($connection); 47 | 48 | case $connection instanceof \PDO: 49 | return new PdoSessionHandler($connection); 50 | 51 | case !\is_string($connection): 52 | throw new \InvalidArgumentException(\sprintf('Unsupported Connection: "%s".', get_debug_type($connection))); 53 | case str_starts_with($connection, 'file://'): 54 | $savePath = substr($connection, 7); 55 | 56 | return new StrictSessionHandler(new NativeFileSessionHandler('' === $savePath ? null : $savePath)); 57 | 58 | case str_starts_with($connection, 'redis:'): 59 | case str_starts_with($connection, 'rediss:'): 60 | case str_starts_with($connection, 'valkey:'): 61 | case str_starts_with($connection, 'valkeys:'): 62 | case str_starts_with($connection, 'memcached:'): 63 | if (!class_exists(AbstractAdapter::class)) { 64 | throw new \InvalidArgumentException('Unsupported Redis or Memcached DSN. Try running "composer require symfony/cache".'); 65 | } 66 | $handlerClass = str_starts_with($connection, 'memcached:') ? MemcachedSessionHandler::class : RedisSessionHandler::class; 67 | $connection = AbstractAdapter::createConnection($connection, ['lazy' => true]); 68 | 69 | return new $handlerClass($connection, array_intersect_key($options, ['prefix' => 1, 'ttl' => 1])); 70 | 71 | case str_starts_with($connection, 'pdo_oci://'): 72 | if (!class_exists(DriverManager::class)) { 73 | throw new \InvalidArgumentException('Unsupported PDO OCI DSN. Try running "composer require doctrine/dbal".'); 74 | } 75 | $connection[3] = '-'; 76 | $params = (new DsnParser())->parse($connection); 77 | $config = new Configuration(); 78 | $config->setSchemaManagerFactory(new DefaultSchemaManagerFactory()); 79 | 80 | $connection = DriverManager::getConnection($params, $config)->getNativeConnection(); 81 | // no break; 82 | 83 | case str_starts_with($connection, 'mssql://'): 84 | case str_starts_with($connection, 'mysql://'): 85 | case str_starts_with($connection, 'mysql2://'): 86 | case str_starts_with($connection, 'pgsql://'): 87 | case str_starts_with($connection, 'postgres://'): 88 | case str_starts_with($connection, 'postgresql://'): 89 | case str_starts_with($connection, 'sqlsrv://'): 90 | case str_starts_with($connection, 'sqlite://'): 91 | case str_starts_with($connection, 'sqlite3://'): 92 | return new PdoSessionHandler($connection, $options); 93 | } 94 | 95 | throw new \InvalidArgumentException(\sprintf('Unsupported Connection: "%s".', $connection)); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Session/Storage/Handler/StrictSessionHandler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; 13 | 14 | /** 15 | * Adds basic `SessionUpdateTimestampHandlerInterface` behaviors to another `SessionHandlerInterface`. 16 | * 17 | * @author Nicolas Grekas 18 | */ 19 | class StrictSessionHandler extends AbstractSessionHandler 20 | { 21 | private bool $doDestroy; 22 | 23 | public function __construct( 24 | private \SessionHandlerInterface $handler, 25 | ) { 26 | if ($handler instanceof \SessionUpdateTimestampHandlerInterface) { 27 | throw new \LogicException(\sprintf('"%s" is already an instance of "SessionUpdateTimestampHandlerInterface", you cannot wrap it with "%s".', get_debug_type($handler), self::class)); 28 | } 29 | } 30 | 31 | /** 32 | * Returns true if this handler wraps an internal PHP session save handler using \SessionHandler. 33 | * 34 | * @internal 35 | */ 36 | public function isWrapper(): bool 37 | { 38 | return $this->handler instanceof \SessionHandler; 39 | } 40 | 41 | public function open(string $savePath, string $sessionName): bool 42 | { 43 | parent::open($savePath, $sessionName); 44 | 45 | return $this->handler->open($savePath, $sessionName); 46 | } 47 | 48 | protected function doRead(#[\SensitiveParameter] string $sessionId): string 49 | { 50 | return $this->handler->read($sessionId); 51 | } 52 | 53 | public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool 54 | { 55 | return $this->write($sessionId, $data); 56 | } 57 | 58 | protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool 59 | { 60 | return $this->handler->write($sessionId, $data); 61 | } 62 | 63 | public function destroy(#[\SensitiveParameter] string $sessionId): bool 64 | { 65 | $this->doDestroy = true; 66 | $destroyed = parent::destroy($sessionId); 67 | 68 | return $this->doDestroy ? $this->doDestroy($sessionId) : $destroyed; 69 | } 70 | 71 | protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool 72 | { 73 | $this->doDestroy = false; 74 | 75 | return $this->handler->destroy($sessionId); 76 | } 77 | 78 | public function close(): bool 79 | { 80 | return $this->handler->close(); 81 | } 82 | 83 | public function gc(int $maxlifetime): int|false 84 | { 85 | return $this->handler->gc($maxlifetime); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Session/Storage/MetadataBag.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session\Storage; 13 | 14 | use Symfony\Component\HttpFoundation\Session\SessionBagInterface; 15 | 16 | /** 17 | * Metadata container. 18 | * 19 | * Adds metadata to the session. 20 | * 21 | * @author Drak 22 | */ 23 | class MetadataBag implements SessionBagInterface 24 | { 25 | public const CREATED = 'c'; 26 | public const UPDATED = 'u'; 27 | public const LIFETIME = 'l'; 28 | 29 | protected array $meta = [self::CREATED => 0, self::UPDATED => 0, self::LIFETIME => 0]; 30 | 31 | private string $name = '__metadata'; 32 | private int $lastUsed; 33 | 34 | /** 35 | * @param string $storageKey The key used to store bag in the session 36 | * @param int $updateThreshold The time to wait between two UPDATED updates 37 | */ 38 | public function __construct( 39 | private string $storageKey = '_sf2_meta', 40 | private int $updateThreshold = 0, 41 | ) { 42 | } 43 | 44 | public function initialize(array &$array): void 45 | { 46 | $this->meta = &$array; 47 | 48 | if (isset($array[self::CREATED])) { 49 | $this->lastUsed = $this->meta[self::UPDATED]; 50 | 51 | $timeStamp = time(); 52 | if ($timeStamp - $array[self::UPDATED] >= $this->updateThreshold) { 53 | $this->meta[self::UPDATED] = $timeStamp; 54 | } 55 | } else { 56 | $this->stampCreated(); 57 | } 58 | } 59 | 60 | /** 61 | * Gets the lifetime that the session cookie was set with. 62 | */ 63 | public function getLifetime(): int 64 | { 65 | return $this->meta[self::LIFETIME]; 66 | } 67 | 68 | /** 69 | * Stamps a new session's metadata. 70 | * 71 | * @param int|null $lifetime Sets the cookie lifetime for the session cookie. A null value 72 | * will leave the system settings unchanged, 0 sets the cookie 73 | * to expire with browser session. Time is in seconds, and is 74 | * not a Unix timestamp. 75 | */ 76 | public function stampNew(?int $lifetime = null): void 77 | { 78 | $this->stampCreated($lifetime); 79 | } 80 | 81 | public function getStorageKey(): string 82 | { 83 | return $this->storageKey; 84 | } 85 | 86 | /** 87 | * Gets the created timestamp metadata. 88 | * 89 | * @return int Unix timestamp 90 | */ 91 | public function getCreated(): int 92 | { 93 | return $this->meta[self::CREATED]; 94 | } 95 | 96 | /** 97 | * Gets the last used metadata. 98 | * 99 | * @return int Unix timestamp 100 | */ 101 | public function getLastUsed(): int 102 | { 103 | return $this->lastUsed; 104 | } 105 | 106 | public function clear(): mixed 107 | { 108 | // nothing to do 109 | return null; 110 | } 111 | 112 | public function getName(): string 113 | { 114 | return $this->name; 115 | } 116 | 117 | /** 118 | * Sets name. 119 | */ 120 | public function setName(string $name): void 121 | { 122 | $this->name = $name; 123 | } 124 | 125 | private function stampCreated(?int $lifetime = null): void 126 | { 127 | $timeStamp = time(); 128 | $this->meta[self::CREATED] = $this->meta[self::UPDATED] = $this->lastUsed = $timeStamp; 129 | $this->meta[self::LIFETIME] = $lifetime ?? (int) \ini_get('session.cookie_lifetime'); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Session/Storage/MockArraySessionStorage.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session\Storage; 13 | 14 | use Symfony\Component\HttpFoundation\Session\SessionBagInterface; 15 | 16 | /** 17 | * MockArraySessionStorage mocks the session for unit tests. 18 | * 19 | * No PHP session is actually started since a session can be initialized 20 | * and shutdown only once per PHP execution cycle. 21 | * 22 | * When doing functional testing, you should use MockFileSessionStorage instead. 23 | * 24 | * @author Fabien Potencier 25 | * @author Bulat Shakirzyanov 26 | * @author Drak 27 | */ 28 | class MockArraySessionStorage implements SessionStorageInterface 29 | { 30 | protected string $id = ''; 31 | protected bool $started = false; 32 | protected bool $closed = false; 33 | protected array $data = []; 34 | protected MetadataBag $metadataBag; 35 | 36 | /** 37 | * @var SessionBagInterface[] 38 | */ 39 | protected array $bags = []; 40 | 41 | public function __construct( 42 | protected string $name = 'MOCKSESSID', 43 | ?MetadataBag $metaBag = null, 44 | ) { 45 | $this->setMetadataBag($metaBag); 46 | } 47 | 48 | public function setSessionData(array $array): void 49 | { 50 | $this->data = $array; 51 | } 52 | 53 | public function start(): bool 54 | { 55 | if ($this->started) { 56 | return true; 57 | } 58 | 59 | if (!$this->id) { 60 | $this->id = $this->generateId(); 61 | } 62 | 63 | $this->loadSession(); 64 | 65 | return true; 66 | } 67 | 68 | public function regenerate(bool $destroy = false, ?int $lifetime = null): bool 69 | { 70 | if (!$this->started) { 71 | $this->start(); 72 | } 73 | 74 | $this->metadataBag->stampNew($lifetime); 75 | $this->id = $this->generateId(); 76 | 77 | return true; 78 | } 79 | 80 | public function getId(): string 81 | { 82 | return $this->id; 83 | } 84 | 85 | public function setId(string $id): void 86 | { 87 | if ($this->started) { 88 | throw new \LogicException('Cannot set session ID after the session has started.'); 89 | } 90 | 91 | $this->id = $id; 92 | } 93 | 94 | public function getName(): string 95 | { 96 | return $this->name; 97 | } 98 | 99 | public function setName(string $name): void 100 | { 101 | $this->name = $name; 102 | } 103 | 104 | public function save(): void 105 | { 106 | if (!$this->started || $this->closed) { 107 | throw new \RuntimeException('Trying to save a session that was not started yet or was already closed.'); 108 | } 109 | // nothing to do since we don't persist the session data 110 | $this->closed = false; 111 | $this->started = false; 112 | } 113 | 114 | public function clear(): void 115 | { 116 | // clear out the bags 117 | foreach ($this->bags as $bag) { 118 | $bag->clear(); 119 | } 120 | 121 | // clear out the session 122 | $this->data = []; 123 | 124 | // reconnect the bags to the session 125 | $this->loadSession(); 126 | } 127 | 128 | public function registerBag(SessionBagInterface $bag): void 129 | { 130 | $this->bags[$bag->getName()] = $bag; 131 | } 132 | 133 | public function getBag(string $name): SessionBagInterface 134 | { 135 | if (!isset($this->bags[$name])) { 136 | throw new \InvalidArgumentException(\sprintf('The SessionBagInterface "%s" is not registered.', $name)); 137 | } 138 | 139 | if (!$this->started) { 140 | $this->start(); 141 | } 142 | 143 | return $this->bags[$name]; 144 | } 145 | 146 | public function isStarted(): bool 147 | { 148 | return $this->started; 149 | } 150 | 151 | public function setMetadataBag(?MetadataBag $bag): void 152 | { 153 | $this->metadataBag = $bag ?? new MetadataBag(); 154 | } 155 | 156 | /** 157 | * Gets the MetadataBag. 158 | */ 159 | public function getMetadataBag(): MetadataBag 160 | { 161 | return $this->metadataBag; 162 | } 163 | 164 | /** 165 | * Generates a session ID. 166 | * 167 | * This doesn't need to be particularly cryptographically secure since this is just 168 | * a mock. 169 | */ 170 | protected function generateId(): string 171 | { 172 | return bin2hex(random_bytes(16)); 173 | } 174 | 175 | protected function loadSession(): void 176 | { 177 | $bags = array_merge($this->bags, [$this->metadataBag]); 178 | 179 | foreach ($bags as $bag) { 180 | $key = $bag->getStorageKey(); 181 | $this->data[$key] ??= []; 182 | $bag->initialize($this->data[$key]); 183 | } 184 | 185 | $this->started = true; 186 | $this->closed = false; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /Session/Storage/MockFileSessionStorage.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session\Storage; 13 | 14 | /** 15 | * MockFileSessionStorage is used to mock sessions for 16 | * functional testing where you may need to persist session data 17 | * across separate PHP processes. 18 | * 19 | * No PHP session is actually started since a session can be initialized 20 | * and shutdown only once per PHP execution cycle and this class does 21 | * not pollute any session related globals, including session_*() functions 22 | * or session.* PHP ini directives. 23 | * 24 | * @author Drak 25 | */ 26 | class MockFileSessionStorage extends MockArraySessionStorage 27 | { 28 | private string $savePath; 29 | 30 | /** 31 | * @param string|null $savePath Path of directory to save session files 32 | */ 33 | public function __construct(?string $savePath = null, string $name = 'MOCKSESSID', ?MetadataBag $metaBag = null) 34 | { 35 | $savePath ??= sys_get_temp_dir(); 36 | 37 | if (!is_dir($savePath) && !@mkdir($savePath, 0777, true) && !is_dir($savePath)) { 38 | throw new \RuntimeException(\sprintf('Session Storage was not able to create directory "%s".', $savePath)); 39 | } 40 | 41 | $this->savePath = $savePath; 42 | 43 | parent::__construct($name, $metaBag); 44 | } 45 | 46 | public function start(): bool 47 | { 48 | if ($this->started) { 49 | return true; 50 | } 51 | 52 | if (!$this->id) { 53 | $this->id = $this->generateId(); 54 | } 55 | 56 | $this->read(); 57 | 58 | $this->started = true; 59 | 60 | return true; 61 | } 62 | 63 | public function regenerate(bool $destroy = false, ?int $lifetime = null): bool 64 | { 65 | if (!$this->started) { 66 | $this->start(); 67 | } 68 | 69 | if ($destroy) { 70 | $this->destroy(); 71 | } 72 | 73 | return parent::regenerate($destroy, $lifetime); 74 | } 75 | 76 | public function save(): void 77 | { 78 | if (!$this->started) { 79 | throw new \RuntimeException('Trying to save a session that was not started yet or was already closed.'); 80 | } 81 | 82 | $data = $this->data; 83 | 84 | foreach ($this->bags as $bag) { 85 | if (empty($data[$key = $bag->getStorageKey()])) { 86 | unset($data[$key]); 87 | } 88 | } 89 | if ([$key = $this->metadataBag->getStorageKey()] === array_keys($data)) { 90 | unset($data[$key]); 91 | } 92 | 93 | try { 94 | if ($data) { 95 | $path = $this->getFilePath(); 96 | $tmp = $path.bin2hex(random_bytes(6)); 97 | file_put_contents($tmp, serialize($data)); 98 | rename($tmp, $path); 99 | } else { 100 | $this->destroy(); 101 | } 102 | } finally { 103 | $this->data = $data; 104 | } 105 | 106 | // this is needed when the session object is re-used across multiple requests 107 | // in functional tests. 108 | $this->started = false; 109 | } 110 | 111 | /** 112 | * Deletes a session from persistent storage. 113 | * Deliberately leaves session data in memory intact. 114 | */ 115 | private function destroy(): void 116 | { 117 | set_error_handler(static function () {}); 118 | try { 119 | unlink($this->getFilePath()); 120 | } finally { 121 | restore_error_handler(); 122 | } 123 | } 124 | 125 | /** 126 | * Calculate path to file. 127 | */ 128 | private function getFilePath(): string 129 | { 130 | return $this->savePath.'/'.$this->id.'.mocksess'; 131 | } 132 | 133 | /** 134 | * Reads session from storage and loads session. 135 | */ 136 | private function read(): void 137 | { 138 | set_error_handler(static function () {}); 139 | try { 140 | $data = file_get_contents($this->getFilePath()); 141 | } finally { 142 | restore_error_handler(); 143 | } 144 | 145 | $this->data = $data ? unserialize($data) : []; 146 | 147 | $this->loadSession(); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /Session/Storage/MockFileSessionStorageFactory.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session\Storage; 13 | 14 | use Symfony\Component\HttpFoundation\Request; 15 | 16 | // Help opcache.preload discover always-needed symbols 17 | class_exists(MockFileSessionStorage::class); 18 | 19 | /** 20 | * @author Jérémy Derussé 21 | */ 22 | class MockFileSessionStorageFactory implements SessionStorageFactoryInterface 23 | { 24 | /** 25 | * @see MockFileSessionStorage constructor. 26 | */ 27 | public function __construct( 28 | private ?string $savePath = null, 29 | private string $name = 'MOCKSESSID', 30 | private ?MetadataBag $metaBag = null, 31 | ) { 32 | } 33 | 34 | public function createStorage(?Request $request): SessionStorageInterface 35 | { 36 | return new MockFileSessionStorage($this->savePath, $this->name, $this->metaBag); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Session/Storage/NativeSessionStorageFactory.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session\Storage; 13 | 14 | use Symfony\Component\HttpFoundation\Request; 15 | use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy; 16 | 17 | // Help opcache.preload discover always-needed symbols 18 | class_exists(NativeSessionStorage::class); 19 | 20 | /** 21 | * @author Jérémy Derussé 22 | */ 23 | class NativeSessionStorageFactory implements SessionStorageFactoryInterface 24 | { 25 | /** 26 | * @see NativeSessionStorage constructor. 27 | */ 28 | public function __construct( 29 | private array $options = [], 30 | private AbstractProxy|\SessionHandlerInterface|null $handler = null, 31 | private ?MetadataBag $metaBag = null, 32 | private bool $secure = false, 33 | ) { 34 | } 35 | 36 | public function createStorage(?Request $request): SessionStorageInterface 37 | { 38 | $storage = new NativeSessionStorage($this->options, $this->handler, $this->metaBag); 39 | if ($this->secure && $request?->isSecure()) { 40 | $storage->setOptions(['cookie_secure' => true]); 41 | } 42 | 43 | return $storage; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Session/Storage/PhpBridgeSessionStorage.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session\Storage; 13 | 14 | use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy; 15 | 16 | /** 17 | * Allows session to be started by PHP and managed by Symfony. 18 | * 19 | * @author Drak 20 | */ 21 | class PhpBridgeSessionStorage extends NativeSessionStorage 22 | { 23 | public function __construct(AbstractProxy|\SessionHandlerInterface|null $handler = null, ?MetadataBag $metaBag = null) 24 | { 25 | if (!\extension_loaded('session')) { 26 | throw new \LogicException('PHP extension "session" is required.'); 27 | } 28 | 29 | $this->setMetadataBag($metaBag); 30 | $this->setSaveHandler($handler); 31 | } 32 | 33 | public function start(): bool 34 | { 35 | if ($this->started) { 36 | return true; 37 | } 38 | 39 | $this->loadSession(); 40 | 41 | return true; 42 | } 43 | 44 | public function clear(): void 45 | { 46 | // clear out the bags and nothing else that may be set 47 | // since the purpose of this driver is to share a handler 48 | foreach ($this->bags as $bag) { 49 | $bag->clear(); 50 | } 51 | 52 | // reconnect the bags to the session 53 | $this->loadSession(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Session/Storage/PhpBridgeSessionStorageFactory.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session\Storage; 13 | 14 | use Symfony\Component\HttpFoundation\Request; 15 | use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy; 16 | 17 | // Help opcache.preload discover always-needed symbols 18 | class_exists(PhpBridgeSessionStorage::class); 19 | 20 | /** 21 | * @author Jérémy Derussé 22 | */ 23 | class PhpBridgeSessionStorageFactory implements SessionStorageFactoryInterface 24 | { 25 | public function __construct( 26 | private AbstractProxy|\SessionHandlerInterface|null $handler = null, 27 | private ?MetadataBag $metaBag = null, 28 | private bool $secure = false, 29 | ) { 30 | } 31 | 32 | public function createStorage(?Request $request): SessionStorageInterface 33 | { 34 | $storage = new PhpBridgeSessionStorage($this->handler, $this->metaBag); 35 | if ($this->secure && $request?->isSecure()) { 36 | $storage->setOptions(['cookie_secure' => true]); 37 | } 38 | 39 | return $storage; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Session/Storage/Proxy/AbstractProxy.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session\Storage\Proxy; 13 | 14 | /** 15 | * @author Drak 16 | */ 17 | abstract class AbstractProxy 18 | { 19 | protected bool $wrapper = false; 20 | 21 | protected ?string $saveHandlerName = null; 22 | 23 | /** 24 | * Gets the session.save_handler name. 25 | */ 26 | public function getSaveHandlerName(): ?string 27 | { 28 | return $this->saveHandlerName; 29 | } 30 | 31 | /** 32 | * Is this proxy handler and instance of \SessionHandlerInterface. 33 | */ 34 | public function isSessionHandlerInterface(): bool 35 | { 36 | return $this instanceof \SessionHandlerInterface; 37 | } 38 | 39 | /** 40 | * Returns true if this handler wraps an internal PHP session save handler using \SessionHandler. 41 | */ 42 | public function isWrapper(): bool 43 | { 44 | return $this->wrapper; 45 | } 46 | 47 | /** 48 | * Has a session started? 49 | */ 50 | public function isActive(): bool 51 | { 52 | return \PHP_SESSION_ACTIVE === session_status(); 53 | } 54 | 55 | /** 56 | * Gets the session ID. 57 | */ 58 | public function getId(): string 59 | { 60 | return session_id(); 61 | } 62 | 63 | /** 64 | * Sets the session ID. 65 | * 66 | * @throws \LogicException 67 | */ 68 | public function setId(string $id): void 69 | { 70 | if ($this->isActive()) { 71 | throw new \LogicException('Cannot change the ID of an active session.'); 72 | } 73 | 74 | session_id($id); 75 | } 76 | 77 | /** 78 | * Gets the session name. 79 | */ 80 | public function getName(): string 81 | { 82 | return session_name(); 83 | } 84 | 85 | /** 86 | * Sets the session name. 87 | * 88 | * @throws \LogicException 89 | */ 90 | public function setName(string $name): void 91 | { 92 | if ($this->isActive()) { 93 | throw new \LogicException('Cannot change the name of an active session.'); 94 | } 95 | 96 | session_name($name); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Session/Storage/Proxy/SessionHandlerProxy.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session\Storage\Proxy; 13 | 14 | use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler; 15 | 16 | /** 17 | * @author Drak 18 | */ 19 | class SessionHandlerProxy extends AbstractProxy implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface 20 | { 21 | public function __construct( 22 | protected \SessionHandlerInterface $handler, 23 | ) { 24 | $this->wrapper = $handler instanceof \SessionHandler; 25 | $this->saveHandlerName = $this->wrapper || ($handler instanceof StrictSessionHandler && $handler->isWrapper()) ? \ini_get('session.save_handler') : 'user'; 26 | } 27 | 28 | public function getHandler(): \SessionHandlerInterface 29 | { 30 | return $this->handler; 31 | } 32 | 33 | // \SessionHandlerInterface 34 | 35 | public function open(string $savePath, string $sessionName): bool 36 | { 37 | return $this->handler->open($savePath, $sessionName); 38 | } 39 | 40 | public function close(): bool 41 | { 42 | return $this->handler->close(); 43 | } 44 | 45 | public function read(#[\SensitiveParameter] string $sessionId): string|false 46 | { 47 | return $this->handler->read($sessionId); 48 | } 49 | 50 | public function write(#[\SensitiveParameter] string $sessionId, string $data): bool 51 | { 52 | return $this->handler->write($sessionId, $data); 53 | } 54 | 55 | public function destroy(#[\SensitiveParameter] string $sessionId): bool 56 | { 57 | return $this->handler->destroy($sessionId); 58 | } 59 | 60 | public function gc(int $maxlifetime): int|false 61 | { 62 | return $this->handler->gc($maxlifetime); 63 | } 64 | 65 | public function validateId(#[\SensitiveParameter] string $sessionId): bool 66 | { 67 | return !$this->handler instanceof \SessionUpdateTimestampHandlerInterface || $this->handler->validateId($sessionId); 68 | } 69 | 70 | public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool 71 | { 72 | return $this->handler instanceof \SessionUpdateTimestampHandlerInterface ? $this->handler->updateTimestamp($sessionId, $data) : $this->write($sessionId, $data); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Session/Storage/SessionStorageFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session\Storage; 13 | 14 | use Symfony\Component\HttpFoundation\Request; 15 | 16 | /** 17 | * @author Jérémy Derussé 18 | */ 19 | interface SessionStorageFactoryInterface 20 | { 21 | /** 22 | * Creates a new instance of SessionStorageInterface. 23 | */ 24 | public function createStorage(?Request $request): SessionStorageInterface; 25 | } 26 | -------------------------------------------------------------------------------- /Session/Storage/SessionStorageInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Session\Storage; 13 | 14 | use Symfony\Component\HttpFoundation\Session\SessionBagInterface; 15 | 16 | /** 17 | * StorageInterface. 18 | * 19 | * @author Fabien Potencier 20 | * @author Drak 21 | */ 22 | interface SessionStorageInterface 23 | { 24 | /** 25 | * Starts the session. 26 | * 27 | * @throws \RuntimeException if something goes wrong starting the session 28 | */ 29 | public function start(): bool; 30 | 31 | /** 32 | * Checks if the session is started. 33 | */ 34 | public function isStarted(): bool; 35 | 36 | /** 37 | * Returns the session ID. 38 | */ 39 | public function getId(): string; 40 | 41 | /** 42 | * Sets the session ID. 43 | */ 44 | public function setId(string $id): void; 45 | 46 | /** 47 | * Returns the session name. 48 | */ 49 | public function getName(): string; 50 | 51 | /** 52 | * Sets the session name. 53 | */ 54 | public function setName(string $name): void; 55 | 56 | /** 57 | * Regenerates id that represents this storage. 58 | * 59 | * This method must invoke session_regenerate_id($destroy) unless 60 | * this interface is used for a storage object designed for unit 61 | * or functional testing where a real PHP session would interfere 62 | * with testing. 63 | * 64 | * Note regenerate+destroy should not clear the session data in memory 65 | * only delete the session data from persistent storage. 66 | * 67 | * Care: When regenerating the session ID no locking is involved in PHP's 68 | * session design. See https://bugs.php.net/61470 for a discussion. 69 | * So you must make sure the regenerated session is saved BEFORE sending the 70 | * headers with the new ID. Symfony's HttpKernel offers a listener for this. 71 | * See Symfony\Component\HttpKernel\EventListener\SaveSessionListener. 72 | * Otherwise session data could get lost again for concurrent requests with the 73 | * new ID. One result could be that you get logged out after just logging in. 74 | * 75 | * @param bool $destroy Destroy session when regenerating? 76 | * @param int|null $lifetime Sets the cookie lifetime for the session cookie. A null value 77 | * will leave the system settings unchanged, 0 sets the cookie 78 | * to expire with browser session. Time is in seconds, and is 79 | * not a Unix timestamp. 80 | * 81 | * @throws \RuntimeException If an error occurs while regenerating this storage 82 | */ 83 | public function regenerate(bool $destroy = false, ?int $lifetime = null): bool; 84 | 85 | /** 86 | * Force the session to be saved and closed. 87 | * 88 | * This method must invoke session_write_close() unless this interface is 89 | * used for a storage object design for unit or functional testing where 90 | * a real PHP session would interfere with testing, in which case 91 | * it should actually persist the session data if required. 92 | * 93 | * @throws \RuntimeException if the session is saved without being started, or if the session 94 | * is already closed 95 | */ 96 | public function save(): void; 97 | 98 | /** 99 | * Clear all session data in memory. 100 | */ 101 | public function clear(): void; 102 | 103 | /** 104 | * Gets a SessionBagInterface by name. 105 | * 106 | * @throws \InvalidArgumentException If the bag does not exist 107 | */ 108 | public function getBag(string $name): SessionBagInterface; 109 | 110 | /** 111 | * Registers a SessionBagInterface for use. 112 | */ 113 | public function registerBag(SessionBagInterface $bag): void; 114 | 115 | public function getMetadataBag(): MetadataBag; 116 | } 117 | -------------------------------------------------------------------------------- /StreamedJsonResponse.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation; 13 | 14 | /** 15 | * StreamedJsonResponse represents a streamed HTTP response for JSON. 16 | * 17 | * A StreamedJsonResponse uses a structure and generics to create an 18 | * efficient resource-saving JSON response. 19 | * 20 | * It is recommended to use flush() function after a specific number of items to directly stream the data. 21 | * 22 | * @see flush() 23 | * 24 | * @author Alexander Schranz 25 | * 26 | * Example usage: 27 | * 28 | * function loadArticles(): \Generator 29 | * // some streamed loading 30 | * yield ['title' => 'Article 1']; 31 | * yield ['title' => 'Article 2']; 32 | * yield ['title' => 'Article 3']; 33 | * // recommended to use flush() after every specific number of items 34 | * }), 35 | * 36 | * $response = new StreamedJsonResponse( 37 | * // json structure with generators in which will be streamed 38 | * [ 39 | * '_embedded' => [ 40 | * 'articles' => loadArticles(), // any generator which you want to stream as list of data 41 | * ], 42 | * ], 43 | * ); 44 | */ 45 | class StreamedJsonResponse extends StreamedResponse 46 | { 47 | private const PLACEHOLDER = '__symfony_json__'; 48 | 49 | /** 50 | * @param mixed[] $data JSON Data containing PHP generators which will be streamed as list of data or a Generator 51 | * @param int $status The HTTP status code (200 "OK" by default) 52 | * @param array $headers An array of HTTP headers 53 | * @param int $encodingOptions Flags for the json_encode() function 54 | */ 55 | public function __construct( 56 | private readonly iterable $data, 57 | int $status = 200, 58 | array $headers = [], 59 | private int $encodingOptions = JsonResponse::DEFAULT_ENCODING_OPTIONS, 60 | ) { 61 | parent::__construct($this->stream(...), $status, $headers); 62 | 63 | if (!$this->headers->get('Content-Type')) { 64 | $this->headers->set('Content-Type', 'application/json'); 65 | } 66 | } 67 | 68 | private function stream(): void 69 | { 70 | $jsonEncodingOptions = \JSON_THROW_ON_ERROR | $this->encodingOptions; 71 | $keyEncodingOptions = $jsonEncodingOptions & ~\JSON_NUMERIC_CHECK; 72 | 73 | $this->streamData($this->data, $jsonEncodingOptions, $keyEncodingOptions); 74 | } 75 | 76 | private function streamData(mixed $data, int $jsonEncodingOptions, int $keyEncodingOptions): void 77 | { 78 | if (\is_array($data)) { 79 | $this->streamArray($data, $jsonEncodingOptions, $keyEncodingOptions); 80 | 81 | return; 82 | } 83 | 84 | if (is_iterable($data) && !$data instanceof \JsonSerializable) { 85 | $this->streamIterable($data, $jsonEncodingOptions, $keyEncodingOptions); 86 | 87 | return; 88 | } 89 | 90 | echo json_encode($data, $jsonEncodingOptions); 91 | } 92 | 93 | private function streamArray(array $data, int $jsonEncodingOptions, int $keyEncodingOptions): void 94 | { 95 | $generators = []; 96 | 97 | array_walk_recursive($data, function (&$item, $key) use (&$generators) { 98 | if (self::PLACEHOLDER === $key) { 99 | // if the placeholder is already in the structure it should be replaced with a new one that explode 100 | // works like expected for the structure 101 | $generators[] = $key; 102 | } 103 | 104 | // generators should be used but for better DX all kind of Traversable and objects are supported 105 | if (\is_object($item)) { 106 | $generators[] = $item; 107 | $item = self::PLACEHOLDER; 108 | } elseif (self::PLACEHOLDER === $item) { 109 | // if the placeholder is already in the structure it should be replaced with a new one that explode 110 | // works like expected for the structure 111 | $generators[] = $item; 112 | } 113 | }); 114 | 115 | $jsonParts = explode('"'.self::PLACEHOLDER.'"', json_encode($data, $jsonEncodingOptions)); 116 | 117 | foreach ($generators as $index => $generator) { 118 | // send first and between parts of the structure 119 | echo $jsonParts[$index]; 120 | 121 | $this->streamData($generator, $jsonEncodingOptions, $keyEncodingOptions); 122 | } 123 | 124 | // send last part of the structure 125 | echo $jsonParts[array_key_last($jsonParts)]; 126 | } 127 | 128 | private function streamIterable(iterable $iterable, int $jsonEncodingOptions, int $keyEncodingOptions): void 129 | { 130 | $isFirstItem = true; 131 | $startTag = '['; 132 | 133 | foreach ($iterable as $key => $item) { 134 | if ($isFirstItem) { 135 | $isFirstItem = false; 136 | // depending on the first elements key the generator is detected as a list or map 137 | // we can not check for a whole list or map because that would hurt the performance 138 | // of the streamed response which is the main goal of this response class 139 | if (0 !== $key) { 140 | $startTag = '{'; 141 | } 142 | 143 | echo $startTag; 144 | } else { 145 | // if not first element of the generic, a separator is required between the elements 146 | echo ','; 147 | } 148 | 149 | if ('{' === $startTag) { 150 | echo json_encode((string) $key, $keyEncodingOptions).':'; 151 | } 152 | 153 | $this->streamData($item, $jsonEncodingOptions, $keyEncodingOptions); 154 | } 155 | 156 | if ($isFirstItem) { // indicates that the generator was empty 157 | echo '['; 158 | } 159 | 160 | echo '[' === $startTag ? ']' : '}'; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /StreamedResponse.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation; 13 | 14 | /** 15 | * StreamedResponse represents a streamed HTTP response. 16 | * 17 | * A StreamedResponse uses a callback or an iterable of strings for its content. 18 | * 19 | * The callback should use the standard PHP functions like echo 20 | * to stream the response back to the client. The flush() function 21 | * can also be used if needed. 22 | * 23 | * @see flush() 24 | * 25 | * @author Fabien Potencier 26 | */ 27 | class StreamedResponse extends Response 28 | { 29 | protected ?\Closure $callback = null; 30 | protected bool $streamed = false; 31 | 32 | private bool $headersSent = false; 33 | 34 | /** 35 | * @param callable|iterable|null $callbackOrChunks 36 | * @param int $status The HTTP status code (200 "OK" by default) 37 | */ 38 | public function __construct(callable|iterable|null $callbackOrChunks = null, int $status = 200, array $headers = []) 39 | { 40 | parent::__construct(null, $status, $headers); 41 | 42 | if (\is_callable($callbackOrChunks)) { 43 | $this->setCallback($callbackOrChunks); 44 | } elseif ($callbackOrChunks) { 45 | $this->setChunks($callbackOrChunks); 46 | } 47 | $this->streamed = false; 48 | $this->headersSent = false; 49 | } 50 | 51 | /** 52 | * @param iterable $chunks 53 | */ 54 | public function setChunks(iterable $chunks): static 55 | { 56 | $this->callback = static function () use ($chunks): void { 57 | foreach ($chunks as $chunk) { 58 | echo $chunk; 59 | @ob_flush(); 60 | flush(); 61 | } 62 | }; 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * Sets the PHP callback associated with this Response. 69 | * 70 | * @return $this 71 | */ 72 | public function setCallback(callable $callback): static 73 | { 74 | $this->callback = $callback(...); 75 | 76 | return $this; 77 | } 78 | 79 | public function getCallback(): ?\Closure 80 | { 81 | if (!isset($this->callback)) { 82 | return null; 83 | } 84 | 85 | return ($this->callback)(...); 86 | } 87 | 88 | /** 89 | * This method only sends the headers once. 90 | * 91 | * @param positive-int|null $statusCode The status code to use, override the statusCode property if set and not null 92 | * 93 | * @return $this 94 | */ 95 | public function sendHeaders(?int $statusCode = null): static 96 | { 97 | if ($this->headersSent) { 98 | return $this; 99 | } 100 | 101 | if ($statusCode < 100 || $statusCode >= 200) { 102 | $this->headersSent = true; 103 | } 104 | 105 | return parent::sendHeaders($statusCode); 106 | } 107 | 108 | /** 109 | * This method only sends the content once. 110 | * 111 | * @return $this 112 | */ 113 | public function sendContent(): static 114 | { 115 | if ($this->streamed) { 116 | return $this; 117 | } 118 | 119 | $this->streamed = true; 120 | 121 | if (!isset($this->callback)) { 122 | throw new \LogicException('The Response callback must be set.'); 123 | } 124 | 125 | ($this->callback)(); 126 | 127 | return $this; 128 | } 129 | 130 | /** 131 | * @return $this 132 | * 133 | * @throws \LogicException when the content is not null 134 | */ 135 | public function setContent(?string $content): static 136 | { 137 | if (null !== $content) { 138 | throw new \LogicException('The content cannot be set on a StreamedResponse instance.'); 139 | } 140 | 141 | $this->streamed = true; 142 | 143 | return $this; 144 | } 145 | 146 | public function getContent(): string|false 147 | { 148 | return false; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /Test/Constraint/RequestAttributeValueSame.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Test\Constraint; 13 | 14 | use PHPUnit\Framework\Constraint\Constraint; 15 | use Symfony\Component\HttpFoundation\Request; 16 | 17 | final class RequestAttributeValueSame extends Constraint 18 | { 19 | public function __construct( 20 | private string $name, 21 | private string $value, 22 | ) { 23 | } 24 | 25 | public function toString(): string 26 | { 27 | return \sprintf('has attribute "%s" with value "%s"', $this->name, $this->value); 28 | } 29 | 30 | /** 31 | * @param Request $request 32 | */ 33 | protected function matches($request): bool 34 | { 35 | return $this->value === $request->attributes->get($this->name); 36 | } 37 | 38 | /** 39 | * @param Request $request 40 | */ 41 | protected function failureDescription($request): string 42 | { 43 | return 'the Request '.$this->toString(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Test/Constraint/ResponseCookieValueSame.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Test\Constraint; 13 | 14 | use PHPUnit\Framework\Constraint\Constraint; 15 | use Symfony\Component\HttpFoundation\Cookie; 16 | use Symfony\Component\HttpFoundation\Response; 17 | 18 | final class ResponseCookieValueSame extends Constraint 19 | { 20 | public function __construct( 21 | private string $name, 22 | private string $value, 23 | private string $path = '/', 24 | private ?string $domain = null, 25 | ) { 26 | } 27 | 28 | public function toString(): string 29 | { 30 | $str = \sprintf('has cookie "%s"', $this->name); 31 | if ('/' !== $this->path) { 32 | $str .= \sprintf(' with path "%s"', $this->path); 33 | } 34 | if ($this->domain) { 35 | $str .= \sprintf(' for domain "%s"', $this->domain); 36 | } 37 | 38 | return $str.\sprintf(' with value "%s"', $this->value); 39 | } 40 | 41 | /** 42 | * @param Response $response 43 | */ 44 | protected function matches($response): bool 45 | { 46 | $cookie = $this->getCookie($response); 47 | if (!$cookie) { 48 | return false; 49 | } 50 | 51 | return $this->value === (string) $cookie->getValue(); 52 | } 53 | 54 | /** 55 | * @param Response $response 56 | */ 57 | protected function failureDescription($response): string 58 | { 59 | return 'the Response '.$this->toString(); 60 | } 61 | 62 | protected function getCookie(Response $response): ?Cookie 63 | { 64 | $cookies = $response->headers->getCookies(); 65 | 66 | $filteredCookies = array_filter($cookies, fn (Cookie $cookie) => $cookie->getName() === $this->name && $cookie->getPath() === $this->path && $cookie->getDomain() === $this->domain); 67 | 68 | return reset($filteredCookies) ?: null; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Test/Constraint/ResponseFormatSame.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Test\Constraint; 13 | 14 | use PHPUnit\Framework\Constraint\Constraint; 15 | use Symfony\Component\HttpFoundation\Request; 16 | use Symfony\Component\HttpFoundation\Response; 17 | 18 | /** 19 | * Asserts that the response is in the given format. 20 | * 21 | * @author Kévin Dunglas 22 | */ 23 | final class ResponseFormatSame extends Constraint 24 | { 25 | public function __construct( 26 | private Request $request, 27 | private ?string $format, 28 | private readonly bool $verbose = true, 29 | ) { 30 | } 31 | 32 | public function toString(): string 33 | { 34 | return 'format is '.($this->format ?? 'null'); 35 | } 36 | 37 | /** 38 | * @param Response $response 39 | */ 40 | protected function matches($response): bool 41 | { 42 | return $this->format === $this->request->getFormat($response->headers->get('Content-Type')); 43 | } 44 | 45 | /** 46 | * @param Response $response 47 | */ 48 | protected function failureDescription($response): string 49 | { 50 | return 'the Response '.$this->toString(); 51 | } 52 | 53 | /** 54 | * @param Response $response 55 | */ 56 | protected function additionalFailureDescription($response): string 57 | { 58 | return $this->verbose ? (string) $response : explode("\r\n\r\n", (string) $response)[0]; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Test/Constraint/ResponseHasCookie.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Test\Constraint; 13 | 14 | use PHPUnit\Framework\Constraint\Constraint; 15 | use Symfony\Component\HttpFoundation\Cookie; 16 | use Symfony\Component\HttpFoundation\Response; 17 | 18 | final class ResponseHasCookie extends Constraint 19 | { 20 | public function __construct( 21 | private string $name, 22 | private string $path = '/', 23 | private ?string $domain = null, 24 | ) { 25 | } 26 | 27 | public function toString(): string 28 | { 29 | $str = \sprintf('has cookie "%s"', $this->name); 30 | if ('/' !== $this->path) { 31 | $str .= \sprintf(' with path "%s"', $this->path); 32 | } 33 | if ($this->domain) { 34 | $str .= \sprintf(' for domain "%s"', $this->domain); 35 | } 36 | 37 | return $str; 38 | } 39 | 40 | /** 41 | * @param Response $response 42 | */ 43 | protected function matches($response): bool 44 | { 45 | return null !== $this->getCookie($response); 46 | } 47 | 48 | /** 49 | * @param Response $response 50 | */ 51 | protected function failureDescription($response): string 52 | { 53 | return 'the Response '.$this->toString(); 54 | } 55 | 56 | private function getCookie(Response $response): ?Cookie 57 | { 58 | $cookies = $response->headers->getCookies(); 59 | 60 | $filteredCookies = array_filter($cookies, fn (Cookie $cookie) => $cookie->getName() === $this->name && $cookie->getPath() === $this->path && $cookie->getDomain() === $this->domain); 61 | 62 | return reset($filteredCookies) ?: null; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Test/Constraint/ResponseHasHeader.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Test\Constraint; 13 | 14 | use PHPUnit\Framework\Constraint\Constraint; 15 | use Symfony\Component\HttpFoundation\Response; 16 | 17 | final class ResponseHasHeader extends Constraint 18 | { 19 | public function __construct( 20 | private string $headerName, 21 | ) { 22 | } 23 | 24 | public function toString(): string 25 | { 26 | return \sprintf('has header "%s"', $this->headerName); 27 | } 28 | 29 | /** 30 | * @param Response $response 31 | */ 32 | protected function matches($response): bool 33 | { 34 | return $response->headers->has($this->headerName); 35 | } 36 | 37 | /** 38 | * @param Response $response 39 | */ 40 | protected function failureDescription($response): string 41 | { 42 | return 'the Response '.$this->toString(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Test/Constraint/ResponseHeaderLocationSame.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Test\Constraint; 13 | 14 | use PHPUnit\Framework\Constraint\Constraint; 15 | use Symfony\Component\HttpFoundation\Request; 16 | use Symfony\Component\HttpFoundation\Response; 17 | 18 | final class ResponseHeaderLocationSame extends Constraint 19 | { 20 | public function __construct(private Request $request, private string $expectedValue) 21 | { 22 | } 23 | 24 | public function toString(): string 25 | { 26 | return \sprintf('has header "Location" matching "%s"', $this->expectedValue); 27 | } 28 | 29 | protected function matches($other): bool 30 | { 31 | if (!$other instanceof Response) { 32 | return false; 33 | } 34 | 35 | $location = $other->headers->get('Location'); 36 | 37 | if (null === $location) { 38 | return false; 39 | } 40 | 41 | return $this->toFullUrl($this->expectedValue) === $this->toFullUrl($location); 42 | } 43 | 44 | protected function failureDescription($other): string 45 | { 46 | return 'the Response '.$this->toString(); 47 | } 48 | 49 | private function toFullUrl(string $url): string 50 | { 51 | if (null === parse_url($url, \PHP_URL_PATH)) { 52 | $url .= '/'; 53 | } 54 | 55 | if (str_starts_with($url, '//')) { 56 | return \sprintf('%s:%s', $this->request->getScheme(), $url); 57 | } 58 | 59 | if (str_starts_with($url, '/')) { 60 | return $this->request->getSchemeAndHttpHost().$url; 61 | } 62 | 63 | return $url; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Test/Constraint/ResponseHeaderSame.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Test\Constraint; 13 | 14 | use PHPUnit\Framework\Constraint\Constraint; 15 | use Symfony\Component\HttpFoundation\Response; 16 | 17 | final class ResponseHeaderSame extends Constraint 18 | { 19 | public function __construct( 20 | private string $headerName, 21 | private string $expectedValue, 22 | ) { 23 | } 24 | 25 | public function toString(): string 26 | { 27 | return \sprintf('has header "%s" with value "%s"', $this->headerName, $this->expectedValue); 28 | } 29 | 30 | /** 31 | * @param Response $response 32 | */ 33 | protected function matches($response): bool 34 | { 35 | return $this->expectedValue === $response->headers->get($this->headerName, null); 36 | } 37 | 38 | /** 39 | * @param Response $response 40 | */ 41 | protected function failureDescription($response): string 42 | { 43 | return 'the Response '.$this->toString(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Test/Constraint/ResponseIsRedirected.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Test\Constraint; 13 | 14 | use PHPUnit\Framework\Constraint\Constraint; 15 | use Symfony\Component\HttpFoundation\Response; 16 | 17 | final class ResponseIsRedirected extends Constraint 18 | { 19 | /** 20 | * @param bool $verbose If true, the entire response is printed on failure. If false, the response body is omitted. 21 | */ 22 | public function __construct(private readonly bool $verbose = true) 23 | { 24 | } 25 | 26 | public function toString(): string 27 | { 28 | return 'is redirected'; 29 | } 30 | 31 | /** 32 | * @param Response $response 33 | */ 34 | protected function matches($response): bool 35 | { 36 | return $response->isRedirect(); 37 | } 38 | 39 | /** 40 | * @param Response $response 41 | */ 42 | protected function failureDescription($response): string 43 | { 44 | return 'the Response '.$this->toString(); 45 | } 46 | 47 | /** 48 | * @param Response $response 49 | */ 50 | protected function additionalFailureDescription($response): string 51 | { 52 | return $this->verbose ? (string) $response : explode("\r\n\r\n", (string) $response)[0]; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Test/Constraint/ResponseIsSuccessful.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Test\Constraint; 13 | 14 | use PHPUnit\Framework\Constraint\Constraint; 15 | use Symfony\Component\HttpFoundation\Response; 16 | 17 | final class ResponseIsSuccessful extends Constraint 18 | { 19 | /** 20 | * @param bool $verbose If true, the entire response is printed on failure. If false, the response body is omitted. 21 | */ 22 | public function __construct(private readonly bool $verbose = true) 23 | { 24 | } 25 | 26 | public function toString(): string 27 | { 28 | return 'is successful'; 29 | } 30 | 31 | /** 32 | * @param Response $response 33 | */ 34 | protected function matches($response): bool 35 | { 36 | return $response->isSuccessful(); 37 | } 38 | 39 | /** 40 | * @param Response $response 41 | */ 42 | protected function failureDescription($response): string 43 | { 44 | return 'the Response '.$this->toString(); 45 | } 46 | 47 | /** 48 | * @param Response $response 49 | */ 50 | protected function additionalFailureDescription($response): string 51 | { 52 | return $this->verbose ? (string) $response : explode("\r\n\r\n", (string) $response)[0]; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Test/Constraint/ResponseIsUnprocessable.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Test\Constraint; 13 | 14 | use PHPUnit\Framework\Constraint\Constraint; 15 | use Symfony\Component\HttpFoundation\Response; 16 | 17 | final class ResponseIsUnprocessable extends Constraint 18 | { 19 | /** 20 | * @param bool $verbose If true, the entire response is printed on failure. If false, the response body is omitted. 21 | */ 22 | public function __construct(private readonly bool $verbose = true) 23 | { 24 | } 25 | 26 | public function toString(): string 27 | { 28 | return 'is unprocessable'; 29 | } 30 | 31 | /** 32 | * @param Response $other 33 | */ 34 | protected function matches($other): bool 35 | { 36 | return Response::HTTP_UNPROCESSABLE_ENTITY === $other->getStatusCode(); 37 | } 38 | 39 | /** 40 | * @param Response $other 41 | */ 42 | protected function failureDescription($other): string 43 | { 44 | return 'the Response '.$this->toString(); 45 | } 46 | 47 | /** 48 | * @param Response $response 49 | */ 50 | protected function additionalFailureDescription($response): string 51 | { 52 | return $this->verbose ? (string) $response : explode("\r\n\r\n", (string) $response)[0]; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Test/Constraint/ResponseStatusCodeSame.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation\Test\Constraint; 13 | 14 | use PHPUnit\Framework\Constraint\Constraint; 15 | use Symfony\Component\HttpFoundation\Response; 16 | 17 | final class ResponseStatusCodeSame extends Constraint 18 | { 19 | public function __construct( 20 | private int $statusCode, 21 | private readonly bool $verbose = true, 22 | ) { 23 | } 24 | 25 | public function toString(): string 26 | { 27 | return 'status code is '.$this->statusCode; 28 | } 29 | 30 | /** 31 | * @param Response $response 32 | */ 33 | protected function matches($response): bool 34 | { 35 | return $this->statusCode === $response->getStatusCode(); 36 | } 37 | 38 | /** 39 | * @param Response $response 40 | */ 41 | protected function failureDescription($response): string 42 | { 43 | return 'the Response '.$this->toString(); 44 | } 45 | 46 | /** 47 | * @param Response $response 48 | */ 49 | protected function additionalFailureDescription($response): string 50 | { 51 | return $this->verbose ? (string) $response : explode("\r\n\r\n", (string) $response)[0]; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /UrlHelper.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\HttpFoundation; 13 | 14 | use Symfony\Component\Routing\RequestContext; 15 | use Symfony\Component\Routing\RequestContextAwareInterface; 16 | 17 | /** 18 | * A helper service for manipulating URLs within and outside the request scope. 19 | * 20 | * @author Valentin Udaltsov 21 | */ 22 | final class UrlHelper 23 | { 24 | public function __construct( 25 | private RequestStack $requestStack, 26 | private RequestContextAwareInterface|RequestContext|null $requestContext = null, 27 | ) { 28 | } 29 | 30 | public function getAbsoluteUrl(string $path): string 31 | { 32 | if (str_contains($path, '://') || str_starts_with($path, '//')) { 33 | return $path; 34 | } 35 | 36 | if (null === $request = $this->requestStack->getMainRequest()) { 37 | return $this->getAbsoluteUrlFromContext($path); 38 | } 39 | 40 | if ('#' === $path[0]) { 41 | $path = $request->getRequestUri().$path; 42 | } elseif ('?' === $path[0]) { 43 | $path = $request->getPathInfo().$path; 44 | } 45 | 46 | if (!$path || '/' !== $path[0]) { 47 | $prefix = $request->getPathInfo(); 48 | $last = \strlen($prefix) - 1; 49 | if ($last !== $pos = strrpos($prefix, '/')) { 50 | $prefix = substr($prefix, 0, $pos).'/'; 51 | } 52 | 53 | return $request->getUriForPath($prefix.$path); 54 | } 55 | 56 | return $request->getSchemeAndHttpHost().$path; 57 | } 58 | 59 | public function getRelativePath(string $path): string 60 | { 61 | if (str_contains($path, '://') || str_starts_with($path, '//')) { 62 | return $path; 63 | } 64 | 65 | if (null === $request = $this->requestStack->getMainRequest()) { 66 | return $path; 67 | } 68 | 69 | return $request->getRelativeUriForPath($path); 70 | } 71 | 72 | private function getAbsoluteUrlFromContext(string $path): string 73 | { 74 | if (null === $context = $this->requestContext) { 75 | return $path; 76 | } 77 | 78 | if ($context instanceof RequestContextAwareInterface) { 79 | $context = $context->getContext(); 80 | } 81 | 82 | if ('' === $host = $context->getHost()) { 83 | return $path; 84 | } 85 | 86 | $scheme = $context->getScheme(); 87 | $port = ''; 88 | 89 | if ('http' === $scheme && 80 !== $context->getHttpPort()) { 90 | $port = ':'.$context->getHttpPort(); 91 | } elseif ('https' === $scheme && 443 !== $context->getHttpsPort()) { 92 | $port = ':'.$context->getHttpsPort(); 93 | } 94 | 95 | if ('#' === $path[0]) { 96 | $queryString = $context->getQueryString(); 97 | $path = $context->getPathInfo().($queryString ? '?'.$queryString : '').$path; 98 | } elseif ('?' === $path[0]) { 99 | $path = $context->getPathInfo().$path; 100 | } 101 | 102 | if ('/' !== $path[0]) { 103 | $path = rtrim($context->getBaseUrl(), '/').'/'.$path; 104 | } 105 | 106 | return $scheme.'://'.$host.$port.$path; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony/http-foundation", 3 | "type": "library", 4 | "description": "Defines an object-oriented layer for the HTTP specification", 5 | "keywords": [], 6 | "homepage": "https://symfony.com", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Fabien Potencier", 11 | "email": "fabien@symfony.com" 12 | }, 13 | { 14 | "name": "Symfony Community", 15 | "homepage": "https://symfony.com/contributors" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=8.2", 20 | "symfony/deprecation-contracts": "^2.5|^3.0", 21 | "symfony/polyfill-mbstring": "~1.1", 22 | "symfony/polyfill-php83": "^1.27" 23 | }, 24 | "require-dev": { 25 | "doctrine/dbal": "^3.6|^4", 26 | "predis/predis": "^1.1|^2.0", 27 | "symfony/cache": "^6.4.12|^7.1.5", 28 | "symfony/clock": "^6.4|^7.0", 29 | "symfony/dependency-injection": "^6.4|^7.0", 30 | "symfony/http-kernel": "^6.4|^7.0", 31 | "symfony/mime": "^6.4|^7.0", 32 | "symfony/expression-language": "^6.4|^7.0", 33 | "symfony/rate-limiter": "^6.4|^7.0" 34 | }, 35 | "conflict": { 36 | "doctrine/dbal": "<3.6", 37 | "symfony/cache": "<6.4.12|>=7.0,<7.1.5" 38 | }, 39 | "autoload": { 40 | "psr-4": { "Symfony\\Component\\HttpFoundation\\": "" }, 41 | "exclude-from-classmap": [ 42 | "/Tests/" 43 | ] 44 | }, 45 | "minimum-stability": "dev" 46 | } 47 | --------------------------------------------------------------------------------