Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/3rdparty.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCôme Chilliet <come.chilliet@nextcloud.com>2022-07-12 11:16:14 +0300
committerCôme Chilliet <come.chilliet@nextcloud.com>2022-07-12 11:21:34 +0300
commit891e35b8ab54f52b7b27b8b26f0492aaa9ed0fcc (patch)
tree31bb1fd477239636c3a4b5aaf7a4b14e23445ee6
parenta5d796d02631b21546accdafc09021f96787bb8b (diff)
Add symfony/http-foundation dependency
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
-rw-r--r--composer.json1
-rw-r--r--composer.lock124
-rw-r--r--composer/autoload_classmap.php85
-rw-r--r--composer/autoload_files.php2
-rw-r--r--composer/autoload_psr4.php1
-rw-r--r--composer/autoload_static.php92
-rw-r--r--composer/installed.json131
-rw-r--r--composer/installed.php29
-rw-r--r--symfony/http-foundation/AcceptHeader.php168
-rw-r--r--symfony/http-foundation/AcceptHeaderItem.php177
-rw-r--r--symfony/http-foundation/BinaryFileResponse.php363
-rw-r--r--symfony/http-foundation/Cookie.php422
-rw-r--r--symfony/http-foundation/Exception/BadRequestException.php19
-rw-r--r--symfony/http-foundation/Exception/ConflictingHeadersException.php21
-rw-r--r--symfony/http-foundation/Exception/JsonException.php21
-rw-r--r--symfony/http-foundation/Exception/RequestExceptionInterface.php21
-rw-r--r--symfony/http-foundation/Exception/SessionNotFoundException.php27
-rw-r--r--symfony/http-foundation/Exception/SuspiciousOperationException.php20
-rw-r--r--symfony/http-foundation/ExpressionRequestMatcher.php47
-rw-r--r--symfony/http-foundation/File/Exception/AccessDeniedException.php25
-rw-r--r--symfony/http-foundation/File/Exception/CannotWriteFileException.php21
-rw-r--r--symfony/http-foundation/File/Exception/ExtensionFileException.php21
-rw-r--r--symfony/http-foundation/File/Exception/FileException.php21
-rw-r--r--symfony/http-foundation/File/Exception/FileNotFoundException.php25
-rw-r--r--symfony/http-foundation/File/Exception/FormSizeFileException.php21
-rw-r--r--symfony/http-foundation/File/Exception/IniSizeFileException.php21
-rw-r--r--symfony/http-foundation/File/Exception/NoFileException.php21
-rw-r--r--symfony/http-foundation/File/Exception/NoTmpDirFileException.php21
-rw-r--r--symfony/http-foundation/File/Exception/PartialFileException.php21
-rw-r--r--symfony/http-foundation/File/Exception/UnexpectedTypeException.php20
-rw-r--r--symfony/http-foundation/File/Exception/UploadException.php21
-rw-r--r--symfony/http-foundation/File/File.php152
-rw-r--r--symfony/http-foundation/File/Stream.php31
-rw-r--r--symfony/http-foundation/File/UploadedFile.php287
-rw-r--r--symfony/http-foundation/FileBag.php140
-rw-r--r--symfony/http-foundation/HeaderBag.php295
-rw-r--r--symfony/http-foundation/HeaderUtils.php293
-rw-r--r--symfony/http-foundation/InputBag.php113
-rw-r--r--symfony/http-foundation/IpUtils.php203
-rw-r--r--symfony/http-foundation/JsonResponse.php221
-rw-r--r--symfony/http-foundation/LICENSE19
-rw-r--r--symfony/http-foundation/ParameterBag.php228
-rw-r--r--symfony/http-foundation/README.md28
-rw-r--r--symfony/http-foundation/RateLimiter/AbstractRequestRateLimiter.php57
-rw-r--r--symfony/http-foundation/RateLimiter/RequestRateLimiterInterface.php30
-rw-r--r--symfony/http-foundation/RedirectResponse.php109
-rw-r--r--symfony/http-foundation/Request.php2147
-rw-r--r--symfony/http-foundation/RequestMatcher.php196
-rw-r--r--symfony/http-foundation/RequestMatcherInterface.php27
-rw-r--r--symfony/http-foundation/RequestStack.php128
-rw-r--r--symfony/http-foundation/Response.php1285
-rw-r--r--symfony/http-foundation/ResponseHeaderBag.php291
-rw-r--r--symfony/http-foundation/ServerBag.php99
-rw-r--r--symfony/http-foundation/Session/Attribute/AttributeBag.php152
-rw-r--r--symfony/http-foundation/Session/Attribute/AttributeBagInterface.php61
-rw-r--r--symfony/http-foundation/Session/Attribute/NamespacedAttributeBag.php161
-rw-r--r--symfony/http-foundation/Session/Flash/AutoExpireFlashBag.php161
-rw-r--r--symfony/http-foundation/Session/Flash/FlashBag.php152
-rw-r--r--symfony/http-foundation/Session/Flash/FlashBagInterface.php88
-rw-r--r--symfony/http-foundation/Session/Session.php285
-rw-r--r--symfony/http-foundation/Session/SessionBagInterface.php46
-rw-r--r--symfony/http-foundation/Session/SessionBagProxy.php95
-rw-r--r--symfony/http-foundation/Session/SessionFactory.php40
-rw-r--r--symfony/http-foundation/Session/SessionFactoryInterface.php20
-rw-r--r--symfony/http-foundation/Session/SessionInterface.php166
-rw-r--r--symfony/http-foundation/Session/SessionUtils.php59
-rw-r--r--symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php155
-rw-r--r--symfony/http-foundation/Session/Storage/Handler/IdentityMarshaller.php42
-rw-r--r--symfony/http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php108
-rw-r--r--symfony/http-foundation/Session/Storage/Handler/MemcachedSessionHandler.php122
-rw-r--r--symfony/http-foundation/Session/Storage/Handler/MigratingSessionHandler.php139
-rw-r--r--symfony/http-foundation/Session/Storage/Handler/MongoDbSessionHandler.php193
-rw-r--r--symfony/http-foundation/Session/Storage/Handler/NativeFileSessionHandler.php55
-rw-r--r--symfony/http-foundation/Session/Storage/Handler/NullSessionHandler.php80
-rw-r--r--symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php943
-rw-r--r--symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php137
-rw-r--r--symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php91
-rw-r--r--symfony/http-foundation/Session/Storage/Handler/StrictSessionHandler.php108
-rw-r--r--symfony/http-foundation/Session/Storage/MetadataBag.php167
-rw-r--r--symfony/http-foundation/Session/Storage/MockArraySessionStorage.php252
-rw-r--r--symfony/http-foundation/Session/Storage/MockFileSessionStorage.php160
-rw-r--r--symfony/http-foundation/Session/Storage/MockFileSessionStorageFactory.php42
-rw-r--r--symfony/http-foundation/Session/Storage/NativeSessionStorage.php476
-rw-r--r--symfony/http-foundation/Session/Storage/NativeSessionStorageFactory.php49
-rw-r--r--symfony/http-foundation/Session/Storage/PhpBridgeSessionStorage.php64
-rw-r--r--symfony/http-foundation/Session/Storage/PhpBridgeSessionStorageFactory.php47
-rw-r--r--symfony/http-foundation/Session/Storage/Proxy/AbstractProxy.php118
-rw-r--r--symfony/http-foundation/Session/Storage/Proxy/SessionHandlerProxy.php109
-rw-r--r--symfony/http-foundation/Session/Storage/ServiceSessionFactory.php38
-rw-r--r--symfony/http-foundation/Session/Storage/SessionStorageFactoryInterface.php25
-rw-r--r--symfony/http-foundation/Session/Storage/SessionStorageInterface.php131
-rw-r--r--symfony/http-foundation/StreamedResponse.php139
-rw-r--r--symfony/http-foundation/UrlHelper.php102
-rw-r--r--symfony/http-foundation/composer.json40
-rw-r--r--symfony/polyfill-mbstring/Mbstring.php7
-rw-r--r--symfony/polyfill-mbstring/README.md2
-rw-r--r--symfony/polyfill-mbstring/composer.json5
-rw-r--r--symfony/polyfill-php80/Php80.php12
-rw-r--r--symfony/polyfill-php80/PhpToken.php103
-rw-r--r--symfony/polyfill-php80/README.md7
-rw-r--r--symfony/polyfill-php80/Resources/stubs/PhpToken.php7
-rw-r--r--symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php6
-rw-r--r--symfony/polyfill-php80/Resources/stubs/ValueError.php6
-rw-r--r--symfony/polyfill-php80/composer.json2
104 files changed, 14109 insertions, 75 deletions
diff --git a/composer.json b/composer.json
index 0099fa05..289f1193 100644
--- a/composer.json
+++ b/composer.json
@@ -52,6 +52,7 @@
"symfony/console": "4.4.30",
"symfony/event-dispatcher": "4.4.30",
"symfony/event-dispatcher-contracts": "1.1.9",
+ "symfony/http-foundation": "^5.4.10",
"symfony/polyfill-intl-grapheme": "^1.20",
"symfony/polyfill-intl-normalizer": "^1.20",
"symfony/process": "4.4.30",
diff --git a/composer.lock b/composer.lock
index cf85f0da..4c76909a 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "0ac4f55294fd40eceec3134a252cfc24",
+ "content-hash": "2771f5d423707681693d725b709d0ad8",
"packages": [
{
"name": "aws/aws-sdk-php",
@@ -4521,7 +4521,7 @@
},
{
"name": "symfony/deprecation-contracts",
- "version": "v2.5.1",
+ "version": "v2.5.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
@@ -4568,7 +4568,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.1"
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.2"
},
"funding": [
{
@@ -4750,6 +4750,79 @@
"time": "2020-07-06T13:19:58+00:00"
},
{
+ "name": "symfony/http-foundation",
+ "version": "v5.4.10",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/http-foundation.git",
+ "reference": "e7793b7906f72a8cc51054fbca9dcff7a8af1c1e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e7793b7906f72a8cc51054fbca9dcff7a8af1c1e",
+ "reference": "e7793b7906f72a8cc51054fbca9dcff7a8af1c1e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/deprecation-contracts": "^2.1|^3",
+ "symfony/polyfill-mbstring": "~1.1",
+ "symfony/polyfill-php80": "^1.16"
+ },
+ "require-dev": {
+ "predis/predis": "~1.0",
+ "symfony/cache": "^4.4|^5.0|^6.0",
+ "symfony/expression-language": "^4.4|^5.0|^6.0",
+ "symfony/mime": "^4.4|^5.0|^6.0"
+ },
+ "suggest": {
+ "symfony/mime": "To use the file extension guesser"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\HttpFoundation\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Defines an object-oriented layer for the HTTP specification",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/http-foundation/tree/v5.4.10"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-06-19T13:13:40+00:00"
+ },
+ {
"name": "symfony/polyfill-ctype",
"version": "v1.23.0",
"source": {
@@ -5162,28 +5235,31 @@
},
{
"name": "symfony/polyfill-mbstring",
- "version": "v1.23.1",
+ "version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
- "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6"
+ "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9174a3d80210dca8daa7f31fec659150bbeabfc6",
- "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e",
+ "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
+ "provide": {
+ "ext-mbstring": "*"
+ },
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "1.23-dev"
+ "dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@@ -5191,12 +5267,12 @@
}
},
"autoload": {
- "psr-4": {
- "Symfony\\Polyfill\\Mbstring\\": ""
- },
"files": [
"bootstrap.php"
- ]
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Mbstring\\": ""
+ }
},
"notification-url": "https://packagist.org/downloads/",
"license": [
@@ -5222,7 +5298,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.23.1"
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0"
},
"funding": [
{
@@ -5238,7 +5314,7 @@
"type": "tidelift"
}
],
- "time": "2021-05-27T12:26:48+00:00"
+ "time": "2022-05-24T11:49:31+00:00"
},
{
"name": "symfony/polyfill-php72",
@@ -5397,16 +5473,16 @@
},
{
"name": "symfony/polyfill-php80",
- "version": "v1.23.1",
+ "version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
- "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be"
+ "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/1100343ed1a92e3a38f9ae122fc0eb21602547be",
- "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be",
+ "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace",
+ "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace",
"shasum": ""
},
"require": {
@@ -5415,7 +5491,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "1.23-dev"
+ "dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@@ -5423,12 +5499,12 @@
}
},
"autoload": {
- "psr-4": {
- "Symfony\\Polyfill\\Php80\\": ""
- },
"files": [
"bootstrap.php"
],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php80\\": ""
+ },
"classmap": [
"Resources/stubs"
]
@@ -5460,7 +5536,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php80/tree/v1.23.1"
+ "source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0"
},
"funding": [
{
@@ -5476,7 +5552,7 @@
"type": "tidelift"
}
],
- "time": "2021-07-28T13:41:28+00:00"
+ "time": "2022-05-10T07:21:04+00:00"
},
{
"name": "symfony/process",
diff --git a/composer/autoload_classmap.php b/composer/autoload_classmap.php
index d1c1cfda..170ea4f9 100644
--- a/composer/autoload_classmap.php
+++ b/composer/autoload_classmap.php
@@ -2072,6 +2072,7 @@ return array(
'PhpParser\\Parser\\Tokens' => $vendorDir . '/nikic/php-parser/lib/PhpParser/Parser/Tokens.php',
'PhpParser\\PrettyPrinterAbstract' => $vendorDir . '/nikic/php-parser/lib/PhpParser/PrettyPrinterAbstract.php',
'PhpParser\\PrettyPrinter\\Standard' => $vendorDir . '/nikic/php-parser/lib/PhpParser/PrettyPrinter/Standard.php',
+ 'PhpToken' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
'Pimple\\Container' => $vendorDir . '/pimple/pimple/src/Pimple/Container.php',
'Pimple\\Exception\\ExpectedInvokableException' => $vendorDir . '/pimple/pimple/src/Pimple/Exception/ExpectedInvokableException.php',
'Pimple\\Exception\\FrozenServiceException' => $vendorDir . '/pimple/pimple/src/Pimple/Exception/FrozenServiceException.php',
@@ -2875,6 +2876,89 @@ return array(
'Symfony\\Component\\EventDispatcher\\ImmutableEventDispatcher' => $vendorDir . '/symfony/event-dispatcher/ImmutableEventDispatcher.php',
'Symfony\\Component\\EventDispatcher\\LegacyEventDispatcherProxy' => $vendorDir . '/symfony/event-dispatcher/LegacyEventDispatcherProxy.php',
'Symfony\\Component\\EventDispatcher\\LegacyEventProxy' => $vendorDir . '/symfony/event-dispatcher/LegacyEventProxy.php',
+ 'Symfony\\Component\\HttpFoundation\\AcceptHeader' => $vendorDir . '/symfony/http-foundation/AcceptHeader.php',
+ 'Symfony\\Component\\HttpFoundation\\AcceptHeaderItem' => $vendorDir . '/symfony/http-foundation/AcceptHeaderItem.php',
+ 'Symfony\\Component\\HttpFoundation\\BinaryFileResponse' => $vendorDir . '/symfony/http-foundation/BinaryFileResponse.php',
+ 'Symfony\\Component\\HttpFoundation\\Cookie' => $vendorDir . '/symfony/http-foundation/Cookie.php',
+ 'Symfony\\Component\\HttpFoundation\\Exception\\BadRequestException' => $vendorDir . '/symfony/http-foundation/Exception/BadRequestException.php',
+ 'Symfony\\Component\\HttpFoundation\\Exception\\ConflictingHeadersException' => $vendorDir . '/symfony/http-foundation/Exception/ConflictingHeadersException.php',
+ 'Symfony\\Component\\HttpFoundation\\Exception\\JsonException' => $vendorDir . '/symfony/http-foundation/Exception/JsonException.php',
+ 'Symfony\\Component\\HttpFoundation\\Exception\\RequestExceptionInterface' => $vendorDir . '/symfony/http-foundation/Exception/RequestExceptionInterface.php',
+ 'Symfony\\Component\\HttpFoundation\\Exception\\SessionNotFoundException' => $vendorDir . '/symfony/http-foundation/Exception/SessionNotFoundException.php',
+ 'Symfony\\Component\\HttpFoundation\\Exception\\SuspiciousOperationException' => $vendorDir . '/symfony/http-foundation/Exception/SuspiciousOperationException.php',
+ 'Symfony\\Component\\HttpFoundation\\ExpressionRequestMatcher' => $vendorDir . '/symfony/http-foundation/ExpressionRequestMatcher.php',
+ 'Symfony\\Component\\HttpFoundation\\FileBag' => $vendorDir . '/symfony/http-foundation/FileBag.php',
+ 'Symfony\\Component\\HttpFoundation\\File\\Exception\\AccessDeniedException' => $vendorDir . '/symfony/http-foundation/File/Exception/AccessDeniedException.php',
+ 'Symfony\\Component\\HttpFoundation\\File\\Exception\\CannotWriteFileException' => $vendorDir . '/symfony/http-foundation/File/Exception/CannotWriteFileException.php',
+ 'Symfony\\Component\\HttpFoundation\\File\\Exception\\ExtensionFileException' => $vendorDir . '/symfony/http-foundation/File/Exception/ExtensionFileException.php',
+ 'Symfony\\Component\\HttpFoundation\\File\\Exception\\FileException' => $vendorDir . '/symfony/http-foundation/File/Exception/FileException.php',
+ 'Symfony\\Component\\HttpFoundation\\File\\Exception\\FileNotFoundException' => $vendorDir . '/symfony/http-foundation/File/Exception/FileNotFoundException.php',
+ 'Symfony\\Component\\HttpFoundation\\File\\Exception\\FormSizeFileException' => $vendorDir . '/symfony/http-foundation/File/Exception/FormSizeFileException.php',
+ 'Symfony\\Component\\HttpFoundation\\File\\Exception\\IniSizeFileException' => $vendorDir . '/symfony/http-foundation/File/Exception/IniSizeFileException.php',
+ 'Symfony\\Component\\HttpFoundation\\File\\Exception\\NoFileException' => $vendorDir . '/symfony/http-foundation/File/Exception/NoFileException.php',
+ 'Symfony\\Component\\HttpFoundation\\File\\Exception\\NoTmpDirFileException' => $vendorDir . '/symfony/http-foundation/File/Exception/NoTmpDirFileException.php',
+ 'Symfony\\Component\\HttpFoundation\\File\\Exception\\PartialFileException' => $vendorDir . '/symfony/http-foundation/File/Exception/PartialFileException.php',
+ 'Symfony\\Component\\HttpFoundation\\File\\Exception\\UnexpectedTypeException' => $vendorDir . '/symfony/http-foundation/File/Exception/UnexpectedTypeException.php',
+ 'Symfony\\Component\\HttpFoundation\\File\\Exception\\UploadException' => $vendorDir . '/symfony/http-foundation/File/Exception/UploadException.php',
+ 'Symfony\\Component\\HttpFoundation\\File\\File' => $vendorDir . '/symfony/http-foundation/File/File.php',
+ 'Symfony\\Component\\HttpFoundation\\File\\Stream' => $vendorDir . '/symfony/http-foundation/File/Stream.php',
+ 'Symfony\\Component\\HttpFoundation\\File\\UploadedFile' => $vendorDir . '/symfony/http-foundation/File/UploadedFile.php',
+ 'Symfony\\Component\\HttpFoundation\\HeaderBag' => $vendorDir . '/symfony/http-foundation/HeaderBag.php',
+ 'Symfony\\Component\\HttpFoundation\\HeaderUtils' => $vendorDir . '/symfony/http-foundation/HeaderUtils.php',
+ 'Symfony\\Component\\HttpFoundation\\InputBag' => $vendorDir . '/symfony/http-foundation/InputBag.php',
+ 'Symfony\\Component\\HttpFoundation\\IpUtils' => $vendorDir . '/symfony/http-foundation/IpUtils.php',
+ 'Symfony\\Component\\HttpFoundation\\JsonResponse' => $vendorDir . '/symfony/http-foundation/JsonResponse.php',
+ 'Symfony\\Component\\HttpFoundation\\ParameterBag' => $vendorDir . '/symfony/http-foundation/ParameterBag.php',
+ 'Symfony\\Component\\HttpFoundation\\RateLimiter\\AbstractRequestRateLimiter' => $vendorDir . '/symfony/http-foundation/RateLimiter/AbstractRequestRateLimiter.php',
+ 'Symfony\\Component\\HttpFoundation\\RateLimiter\\RequestRateLimiterInterface' => $vendorDir . '/symfony/http-foundation/RateLimiter/RequestRateLimiterInterface.php',
+ 'Symfony\\Component\\HttpFoundation\\RedirectResponse' => $vendorDir . '/symfony/http-foundation/RedirectResponse.php',
+ 'Symfony\\Component\\HttpFoundation\\Request' => $vendorDir . '/symfony/http-foundation/Request.php',
+ 'Symfony\\Component\\HttpFoundation\\RequestMatcher' => $vendorDir . '/symfony/http-foundation/RequestMatcher.php',
+ 'Symfony\\Component\\HttpFoundation\\RequestMatcherInterface' => $vendorDir . '/symfony/http-foundation/RequestMatcherInterface.php',
+ 'Symfony\\Component\\HttpFoundation\\RequestStack' => $vendorDir . '/symfony/http-foundation/RequestStack.php',
+ 'Symfony\\Component\\HttpFoundation\\Response' => $vendorDir . '/symfony/http-foundation/Response.php',
+ 'Symfony\\Component\\HttpFoundation\\ResponseHeaderBag' => $vendorDir . '/symfony/http-foundation/ResponseHeaderBag.php',
+ 'Symfony\\Component\\HttpFoundation\\ServerBag' => $vendorDir . '/symfony/http-foundation/ServerBag.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBag' => $vendorDir . '/symfony/http-foundation/Session/Attribute/AttributeBag.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBagInterface' => $vendorDir . '/symfony/http-foundation/Session/Attribute/AttributeBagInterface.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Attribute\\NamespacedAttributeBag' => $vendorDir . '/symfony/http-foundation/Session/Attribute/NamespacedAttributeBag.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Flash\\AutoExpireFlashBag' => $vendorDir . '/symfony/http-foundation/Session/Flash/AutoExpireFlashBag.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBag' => $vendorDir . '/symfony/http-foundation/Session/Flash/FlashBag.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface' => $vendorDir . '/symfony/http-foundation/Session/Flash/FlashBagInterface.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Session' => $vendorDir . '/symfony/http-foundation/Session/Session.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\SessionBagInterface' => $vendorDir . '/symfony/http-foundation/Session/SessionBagInterface.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\SessionBagProxy' => $vendorDir . '/symfony/http-foundation/Session/SessionBagProxy.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\SessionFactory' => $vendorDir . '/symfony/http-foundation/Session/SessionFactory.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\SessionFactoryInterface' => $vendorDir . '/symfony/http-foundation/Session/SessionFactoryInterface.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\SessionInterface' => $vendorDir . '/symfony/http-foundation/Session/SessionInterface.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\SessionUtils' => $vendorDir . '/symfony/http-foundation/Session/SessionUtils.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\AbstractSessionHandler' => $vendorDir . '/symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\IdentityMarshaller' => $vendorDir . '/symfony/http-foundation/Session/Storage/Handler/IdentityMarshaller.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MarshallingSessionHandler' => $vendorDir . '/symfony/http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MemcachedSessionHandler' => $vendorDir . '/symfony/http-foundation/Session/Storage/Handler/MemcachedSessionHandler.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MigratingSessionHandler' => $vendorDir . '/symfony/http-foundation/Session/Storage/Handler/MigratingSessionHandler.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MongoDbSessionHandler' => $vendorDir . '/symfony/http-foundation/Session/Storage/Handler/MongoDbSessionHandler.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\NativeFileSessionHandler' => $vendorDir . '/symfony/http-foundation/Session/Storage/Handler/NativeFileSessionHandler.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\NullSessionHandler' => $vendorDir . '/symfony/http-foundation/Session/Storage/Handler/NullSessionHandler.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\PdoSessionHandler' => $vendorDir . '/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\RedisSessionHandler' => $vendorDir . '/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\SessionHandlerFactory' => $vendorDir . '/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\StrictSessionHandler' => $vendorDir . '/symfony/http-foundation/Session/Storage/Handler/StrictSessionHandler.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\MetadataBag' => $vendorDir . '/symfony/http-foundation/Session/Storage/MetadataBag.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\MockArraySessionStorage' => $vendorDir . '/symfony/http-foundation/Session/Storage/MockArraySessionStorage.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\MockFileSessionStorage' => $vendorDir . '/symfony/http-foundation/Session/Storage/MockFileSessionStorage.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\MockFileSessionStorageFactory' => $vendorDir . '/symfony/http-foundation/Session/Storage/MockFileSessionStorageFactory.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\NativeSessionStorage' => $vendorDir . '/symfony/http-foundation/Session/Storage/NativeSessionStorage.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\NativeSessionStorageFactory' => $vendorDir . '/symfony/http-foundation/Session/Storage/NativeSessionStorageFactory.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\PhpBridgeSessionStorage' => $vendorDir . '/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorage.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\PhpBridgeSessionStorageFactory' => $vendorDir . '/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorageFactory.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Proxy\\AbstractProxy' => $vendorDir . '/symfony/http-foundation/Session/Storage/Proxy/AbstractProxy.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Proxy\\SessionHandlerProxy' => $vendorDir . '/symfony/http-foundation/Session/Storage/Proxy/SessionHandlerProxy.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\ServiceSessionFactory' => $vendorDir . '/symfony/http-foundation/Session/Storage/ServiceSessionFactory.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\SessionStorageFactoryInterface' => $vendorDir . '/symfony/http-foundation/Session/Storage/SessionStorageFactoryInterface.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\SessionStorageInterface' => $vendorDir . '/symfony/http-foundation/Session/Storage/SessionStorageInterface.php',
+ 'Symfony\\Component\\HttpFoundation\\StreamedResponse' => $vendorDir . '/symfony/http-foundation/StreamedResponse.php',
+ 'Symfony\\Component\\HttpFoundation\\UrlHelper' => $vendorDir . '/symfony/http-foundation/UrlHelper.php',
'Symfony\\Component\\Process\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/process/Exception/ExceptionInterface.php',
'Symfony\\Component\\Process\\Exception\\InvalidArgumentException' => $vendorDir . '/symfony/process/Exception/InvalidArgumentException.php',
'Symfony\\Component\\Process\\Exception\\LogicException' => $vendorDir . '/symfony/process/Exception/LogicException.php',
@@ -3045,6 +3129,7 @@ return array(
'Symfony\\Polyfill\\Php72\\Php72' => $vendorDir . '/symfony/polyfill-php72/Php72.php',
'Symfony\\Polyfill\\Php73\\Php73' => $vendorDir . '/symfony/polyfill-php73/Php73.php',
'Symfony\\Polyfill\\Php80\\Php80' => $vendorDir . '/symfony/polyfill-php80/Php80.php',
+ 'Symfony\\Polyfill\\Php80\\PhpToken' => $vendorDir . '/symfony/polyfill-php80/PhpToken.php',
'System' => $vendorDir . '/pear/pear-core-minimal/src/System.php',
'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
diff --git a/composer/autoload_files.php b/composer/autoload_files.php
index 02b06c86..d6a3e264 100644
--- a/composer/autoload_files.php
+++ b/composer/autoload_files.php
@@ -9,9 +9,9 @@ return array(
'383eaff206634a77a1be54e64e6459c7' => $vendorDir . '/sabre/uri/lib/functions.php',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
+ '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php',
'a0edc8309cc5e1d60e3047b5df6b7052' => $vendorDir . '/guzzlehttp/psr7/src/functions_include.php',
- '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'a4ecaeafb8cfb009ad0e052c90355e98' => $vendorDir . '/beberlei/assert/lib/Assert/functions.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
diff --git a/composer/autoload_psr4.php b/composer/autoload_psr4.php
index 64242249..e7b84631 100644
--- a/composer/autoload_psr4.php
+++ b/composer/autoload_psr4.php
@@ -29,6 +29,7 @@ return array(
'Symfony\\Component\\Translation\\' => array($vendorDir . '/symfony/translation'),
'Symfony\\Component\\Routing\\' => array($vendorDir . '/symfony/routing'),
'Symfony\\Component\\Process\\' => array($vendorDir . '/symfony/process'),
+ 'Symfony\\Component\\HttpFoundation\\' => array($vendorDir . '/symfony/http-foundation'),
'Symfony\\Component\\EventDispatcher\\' => array($vendorDir . '/symfony/event-dispatcher'),
'Symfony\\Component\\Console\\' => array($vendorDir . '/symfony/console'),
'Stecman\\Component\\Symfony\\Console\\BashCompletion\\' => array($vendorDir . '/stecman/symfony-console-completion/src'),
diff --git a/composer/autoload_static.php b/composer/autoload_static.php
index d1087101..b375a6d9 100644
--- a/composer/autoload_static.php
+++ b/composer/autoload_static.php
@@ -10,9 +10,9 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652
'383eaff206634a77a1be54e64e6459c7' => __DIR__ . '/..' . '/sabre/uri/lib/functions.php',
'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
'7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
+ '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
'c964ee0ededf28c96ebd9db5099ef910' => __DIR__ . '/..' . '/guzzlehttp/promises/src/functions_include.php',
'a0edc8309cc5e1d60e3047b5df6b7052' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/functions_include.php',
- '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
'6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
'a4ecaeafb8cfb009ad0e052c90355e98' => __DIR__ . '/..' . '/beberlei/assert/lib/Assert/functions.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
@@ -173,6 +173,7 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652
'Symfony\\Component\\Translation\\' => 30,
'Symfony\\Component\\Routing\\' => 26,
'Symfony\\Component\\Process\\' => 26,
+ 'Symfony\\Component\\HttpFoundation\\' => 33,
'Symfony\\Component\\EventDispatcher\\' => 34,
'Symfony\\Component\\Console\\' => 26,
'Stecman\\Component\\Symfony\\Console\\BashCompletion\\' => 49,
@@ -375,6 +376,10 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652
array (
0 => __DIR__ . '/..' . '/symfony/process',
),
+ 'Symfony\\Component\\HttpFoundation\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/symfony/http-foundation',
+ ),
'Symfony\\Component\\EventDispatcher\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/event-dispatcher',
@@ -2708,6 +2713,7 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652
'PhpParser\\Parser\\Tokens' => __DIR__ . '/..' . '/nikic/php-parser/lib/PhpParser/Parser/Tokens.php',
'PhpParser\\PrettyPrinterAbstract' => __DIR__ . '/..' . '/nikic/php-parser/lib/PhpParser/PrettyPrinterAbstract.php',
'PhpParser\\PrettyPrinter\\Standard' => __DIR__ . '/..' . '/nikic/php-parser/lib/PhpParser/PrettyPrinter/Standard.php',
+ 'PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
'Pimple\\Container' => __DIR__ . '/..' . '/pimple/pimple/src/Pimple/Container.php',
'Pimple\\Exception\\ExpectedInvokableException' => __DIR__ . '/..' . '/pimple/pimple/src/Pimple/Exception/ExpectedInvokableException.php',
'Pimple\\Exception\\FrozenServiceException' => __DIR__ . '/..' . '/pimple/pimple/src/Pimple/Exception/FrozenServiceException.php',
@@ -3511,6 +3517,89 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652
'Symfony\\Component\\EventDispatcher\\ImmutableEventDispatcher' => __DIR__ . '/..' . '/symfony/event-dispatcher/ImmutableEventDispatcher.php',
'Symfony\\Component\\EventDispatcher\\LegacyEventDispatcherProxy' => __DIR__ . '/..' . '/symfony/event-dispatcher/LegacyEventDispatcherProxy.php',
'Symfony\\Component\\EventDispatcher\\LegacyEventProxy' => __DIR__ . '/..' . '/symfony/event-dispatcher/LegacyEventProxy.php',
+ 'Symfony\\Component\\HttpFoundation\\AcceptHeader' => __DIR__ . '/..' . '/symfony/http-foundation/AcceptHeader.php',
+ 'Symfony\\Component\\HttpFoundation\\AcceptHeaderItem' => __DIR__ . '/..' . '/symfony/http-foundation/AcceptHeaderItem.php',
+ 'Symfony\\Component\\HttpFoundation\\BinaryFileResponse' => __DIR__ . '/..' . '/symfony/http-foundation/BinaryFileResponse.php',
+ 'Symfony\\Component\\HttpFoundation\\Cookie' => __DIR__ . '/..' . '/symfony/http-foundation/Cookie.php',
+ 'Symfony\\Component\\HttpFoundation\\Exception\\BadRequestException' => __DIR__ . '/..' . '/symfony/http-foundation/Exception/BadRequestException.php',
+ 'Symfony\\Component\\HttpFoundation\\Exception\\ConflictingHeadersException' => __DIR__ . '/..' . '/symfony/http-foundation/Exception/ConflictingHeadersException.php',
+ 'Symfony\\Component\\HttpFoundation\\Exception\\JsonException' => __DIR__ . '/..' . '/symfony/http-foundation/Exception/JsonException.php',
+ 'Symfony\\Component\\HttpFoundation\\Exception\\RequestExceptionInterface' => __DIR__ . '/..' . '/symfony/http-foundation/Exception/RequestExceptionInterface.php',
+ 'Symfony\\Component\\HttpFoundation\\Exception\\SessionNotFoundException' => __DIR__ . '/..' . '/symfony/http-foundation/Exception/SessionNotFoundException.php',
+ 'Symfony\\Component\\HttpFoundation\\Exception\\SuspiciousOperationException' => __DIR__ . '/..' . '/symfony/http-foundation/Exception/SuspiciousOperationException.php',
+ 'Symfony\\Component\\HttpFoundation\\ExpressionRequestMatcher' => __DIR__ . '/..' . '/symfony/http-foundation/ExpressionRequestMatcher.php',
+ 'Symfony\\Component\\HttpFoundation\\FileBag' => __DIR__ . '/..' . '/symfony/http-foundation/FileBag.php',
+ 'Symfony\\Component\\HttpFoundation\\File\\Exception\\AccessDeniedException' => __DIR__ . '/..' . '/symfony/http-foundation/File/Exception/AccessDeniedException.php',
+ 'Symfony\\Component\\HttpFoundation\\File\\Exception\\CannotWriteFileException' => __DIR__ . '/..' . '/symfony/http-foundation/File/Exception/CannotWriteFileException.php',
+ 'Symfony\\Component\\HttpFoundation\\File\\Exception\\ExtensionFileException' => __DIR__ . '/..' . '/symfony/http-foundation/File/Exception/ExtensionFileException.php',
+ 'Symfony\\Component\\HttpFoundation\\File\\Exception\\FileException' => __DIR__ . '/..' . '/symfony/http-foundation/File/Exception/FileException.php',
+ 'Symfony\\Component\\HttpFoundation\\File\\Exception\\FileNotFoundException' => __DIR__ . '/..' . '/symfony/http-foundation/File/Exception/FileNotFoundException.php',
+ 'Symfony\\Component\\HttpFoundation\\File\\Exception\\FormSizeFileException' => __DIR__ . '/..' . '/symfony/http-foundation/File/Exception/FormSizeFileException.php',
+ 'Symfony\\Component\\HttpFoundation\\File\\Exception\\IniSizeFileException' => __DIR__ . '/..' . '/symfony/http-foundation/File/Exception/IniSizeFileException.php',
+ 'Symfony\\Component\\HttpFoundation\\File\\Exception\\NoFileException' => __DIR__ . '/..' . '/symfony/http-foundation/File/Exception/NoFileException.php',
+ 'Symfony\\Component\\HttpFoundation\\File\\Exception\\NoTmpDirFileException' => __DIR__ . '/..' . '/symfony/http-foundation/File/Exception/NoTmpDirFileException.php',
+ 'Symfony\\Component\\HttpFoundation\\File\\Exception\\PartialFileException' => __DIR__ . '/..' . '/symfony/http-foundation/File/Exception/PartialFileException.php',
+ 'Symfony\\Component\\HttpFoundation\\File\\Exception\\UnexpectedTypeException' => __DIR__ . '/..' . '/symfony/http-foundation/File/Exception/UnexpectedTypeException.php',
+ 'Symfony\\Component\\HttpFoundation\\File\\Exception\\UploadException' => __DIR__ . '/..' . '/symfony/http-foundation/File/Exception/UploadException.php',
+ 'Symfony\\Component\\HttpFoundation\\File\\File' => __DIR__ . '/..' . '/symfony/http-foundation/File/File.php',
+ 'Symfony\\Component\\HttpFoundation\\File\\Stream' => __DIR__ . '/..' . '/symfony/http-foundation/File/Stream.php',
+ 'Symfony\\Component\\HttpFoundation\\File\\UploadedFile' => __DIR__ . '/..' . '/symfony/http-foundation/File/UploadedFile.php',
+ 'Symfony\\Component\\HttpFoundation\\HeaderBag' => __DIR__ . '/..' . '/symfony/http-foundation/HeaderBag.php',
+ 'Symfony\\Component\\HttpFoundation\\HeaderUtils' => __DIR__ . '/..' . '/symfony/http-foundation/HeaderUtils.php',
+ 'Symfony\\Component\\HttpFoundation\\InputBag' => __DIR__ . '/..' . '/symfony/http-foundation/InputBag.php',
+ 'Symfony\\Component\\HttpFoundation\\IpUtils' => __DIR__ . '/..' . '/symfony/http-foundation/IpUtils.php',
+ 'Symfony\\Component\\HttpFoundation\\JsonResponse' => __DIR__ . '/..' . '/symfony/http-foundation/JsonResponse.php',
+ 'Symfony\\Component\\HttpFoundation\\ParameterBag' => __DIR__ . '/..' . '/symfony/http-foundation/ParameterBag.php',
+ 'Symfony\\Component\\HttpFoundation\\RateLimiter\\AbstractRequestRateLimiter' => __DIR__ . '/..' . '/symfony/http-foundation/RateLimiter/AbstractRequestRateLimiter.php',
+ 'Symfony\\Component\\HttpFoundation\\RateLimiter\\RequestRateLimiterInterface' => __DIR__ . '/..' . '/symfony/http-foundation/RateLimiter/RequestRateLimiterInterface.php',
+ 'Symfony\\Component\\HttpFoundation\\RedirectResponse' => __DIR__ . '/..' . '/symfony/http-foundation/RedirectResponse.php',
+ 'Symfony\\Component\\HttpFoundation\\Request' => __DIR__ . '/..' . '/symfony/http-foundation/Request.php',
+ 'Symfony\\Component\\HttpFoundation\\RequestMatcher' => __DIR__ . '/..' . '/symfony/http-foundation/RequestMatcher.php',
+ 'Symfony\\Component\\HttpFoundation\\RequestMatcherInterface' => __DIR__ . '/..' . '/symfony/http-foundation/RequestMatcherInterface.php',
+ 'Symfony\\Component\\HttpFoundation\\RequestStack' => __DIR__ . '/..' . '/symfony/http-foundation/RequestStack.php',
+ 'Symfony\\Component\\HttpFoundation\\Response' => __DIR__ . '/..' . '/symfony/http-foundation/Response.php',
+ 'Symfony\\Component\\HttpFoundation\\ResponseHeaderBag' => __DIR__ . '/..' . '/symfony/http-foundation/ResponseHeaderBag.php',
+ 'Symfony\\Component\\HttpFoundation\\ServerBag' => __DIR__ . '/..' . '/symfony/http-foundation/ServerBag.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBag' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Attribute/AttributeBag.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBagInterface' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Attribute/AttributeBagInterface.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Attribute\\NamespacedAttributeBag' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Attribute/NamespacedAttributeBag.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Flash\\AutoExpireFlashBag' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Flash/AutoExpireFlashBag.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBag' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Flash/FlashBag.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Flash/FlashBagInterface.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Session' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Session.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\SessionBagInterface' => __DIR__ . '/..' . '/symfony/http-foundation/Session/SessionBagInterface.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\SessionBagProxy' => __DIR__ . '/..' . '/symfony/http-foundation/Session/SessionBagProxy.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\SessionFactory' => __DIR__ . '/..' . '/symfony/http-foundation/Session/SessionFactory.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\SessionFactoryInterface' => __DIR__ . '/..' . '/symfony/http-foundation/Session/SessionFactoryInterface.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\SessionInterface' => __DIR__ . '/..' . '/symfony/http-foundation/Session/SessionInterface.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\SessionUtils' => __DIR__ . '/..' . '/symfony/http-foundation/Session/SessionUtils.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\AbstractSessionHandler' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\IdentityMarshaller' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/Handler/IdentityMarshaller.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MarshallingSessionHandler' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MemcachedSessionHandler' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/Handler/MemcachedSessionHandler.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MigratingSessionHandler' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/Handler/MigratingSessionHandler.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MongoDbSessionHandler' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/Handler/MongoDbSessionHandler.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\NativeFileSessionHandler' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/Handler/NativeFileSessionHandler.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\NullSessionHandler' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/Handler/NullSessionHandler.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\PdoSessionHandler' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\RedisSessionHandler' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\SessionHandlerFactory' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\StrictSessionHandler' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/Handler/StrictSessionHandler.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\MetadataBag' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/MetadataBag.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\MockArraySessionStorage' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/MockArraySessionStorage.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\MockFileSessionStorage' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/MockFileSessionStorage.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\MockFileSessionStorageFactory' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/MockFileSessionStorageFactory.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\NativeSessionStorage' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/NativeSessionStorage.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\NativeSessionStorageFactory' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/NativeSessionStorageFactory.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\PhpBridgeSessionStorage' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorage.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\PhpBridgeSessionStorageFactory' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorageFactory.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Proxy\\AbstractProxy' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/Proxy/AbstractProxy.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Proxy\\SessionHandlerProxy' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/Proxy/SessionHandlerProxy.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\ServiceSessionFactory' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/ServiceSessionFactory.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\SessionStorageFactoryInterface' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/SessionStorageFactoryInterface.php',
+ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\SessionStorageInterface' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/SessionStorageInterface.php',
+ 'Symfony\\Component\\HttpFoundation\\StreamedResponse' => __DIR__ . '/..' . '/symfony/http-foundation/StreamedResponse.php',
+ 'Symfony\\Component\\HttpFoundation\\UrlHelper' => __DIR__ . '/..' . '/symfony/http-foundation/UrlHelper.php',
'Symfony\\Component\\Process\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/process/Exception/ExceptionInterface.php',
'Symfony\\Component\\Process\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/symfony/process/Exception/InvalidArgumentException.php',
'Symfony\\Component\\Process\\Exception\\LogicException' => __DIR__ . '/..' . '/symfony/process/Exception/LogicException.php',
@@ -3681,6 +3770,7 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652
'Symfony\\Polyfill\\Php72\\Php72' => __DIR__ . '/..' . '/symfony/polyfill-php72/Php72.php',
'Symfony\\Polyfill\\Php73\\Php73' => __DIR__ . '/..' . '/symfony/polyfill-php73/Php73.php',
'Symfony\\Polyfill\\Php80\\Php80' => __DIR__ . '/..' . '/symfony/polyfill-php80/Php80.php',
+ 'Symfony\\Polyfill\\Php80\\PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/PhpToken.php',
'System' => __DIR__ . '/..' . '/pear/pear-core-minimal/src/System.php',
'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
diff --git a/composer/installed.json b/composer/installed.json
index d6a64e89..88fb0177 100644
--- a/composer/installed.json
+++ b/composer/installed.json
@@ -4719,8 +4719,8 @@
},
{
"name": "symfony/deprecation-contracts",
- "version": "v2.5.1",
- "version_normalized": "2.5.1.0",
+ "version": "v2.5.2",
+ "version_normalized": "2.5.2.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
@@ -4769,7 +4769,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.1"
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.2"
},
"funding": [
{
@@ -4957,6 +4957,82 @@
"install-path": "../symfony/event-dispatcher-contracts"
},
{
+ "name": "symfony/http-foundation",
+ "version": "v5.4.10",
+ "version_normalized": "5.4.10.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/http-foundation.git",
+ "reference": "e7793b7906f72a8cc51054fbca9dcff7a8af1c1e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e7793b7906f72a8cc51054fbca9dcff7a8af1c1e",
+ "reference": "e7793b7906f72a8cc51054fbca9dcff7a8af1c1e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/deprecation-contracts": "^2.1|^3",
+ "symfony/polyfill-mbstring": "~1.1",
+ "symfony/polyfill-php80": "^1.16"
+ },
+ "require-dev": {
+ "predis/predis": "~1.0",
+ "symfony/cache": "^4.4|^5.0|^6.0",
+ "symfony/expression-language": "^4.4|^5.0|^6.0",
+ "symfony/mime": "^4.4|^5.0|^6.0"
+ },
+ "suggest": {
+ "symfony/mime": "To use the file extension guesser"
+ },
+ "time": "2022-06-19T13:13:40+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\HttpFoundation\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Defines an object-oriented layer for the HTTP specification",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/http-foundation/tree/v5.4.10"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "install-path": "../symfony/http-foundation"
+ },
+ {
"name": "symfony/polyfill-ctype",
"version": "v1.23.0",
"version_normalized": "1.23.0.0",
@@ -5384,30 +5460,33 @@
},
{
"name": "symfony/polyfill-mbstring",
- "version": "v1.23.1",
- "version_normalized": "1.23.1.0",
+ "version": "v1.26.0",
+ "version_normalized": "1.26.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
- "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6"
+ "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9174a3d80210dca8daa7f31fec659150bbeabfc6",
- "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e",
+ "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
+ "provide": {
+ "ext-mbstring": "*"
+ },
"suggest": {
"ext-mbstring": "For best performance"
},
- "time": "2021-05-27T12:26:48+00:00",
+ "time": "2022-05-24T11:49:31+00:00",
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "1.23-dev"
+ "dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@@ -5416,12 +5495,12 @@
},
"installation-source": "dist",
"autoload": {
- "psr-4": {
- "Symfony\\Polyfill\\Mbstring\\": ""
- },
"files": [
"bootstrap.php"
- ]
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Mbstring\\": ""
+ }
},
"notification-url": "https://packagist.org/downloads/",
"license": [
@@ -5447,7 +5526,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.23.1"
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0"
},
"funding": [
{
@@ -5628,27 +5707,27 @@
},
{
"name": "symfony/polyfill-php80",
- "version": "v1.23.1",
- "version_normalized": "1.23.1.0",
+ "version": "v1.26.0",
+ "version_normalized": "1.26.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
- "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be"
+ "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/1100343ed1a92e3a38f9ae122fc0eb21602547be",
- "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be",
+ "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace",
+ "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
- "time": "2021-07-28T13:41:28+00:00",
+ "time": "2022-05-10T07:21:04+00:00",
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "1.23-dev"
+ "dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@@ -5657,12 +5736,12 @@
},
"installation-source": "dist",
"autoload": {
- "psr-4": {
- "Symfony\\Polyfill\\Php80\\": ""
- },
"files": [
"bootstrap.php"
],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php80\\": ""
+ },
"classmap": [
"Resources/stubs"
]
@@ -5694,7 +5773,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php80/tree/v1.23.1"
+ "source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0"
},
"funding": [
{
diff --git a/composer/installed.php b/composer/installed.php
index ba7cb887..4de98785 100644
--- a/composer/installed.php
+++ b/composer/installed.php
@@ -3,7 +3,7 @@
'name' => 'nextcloud/3rdparty',
'pretty_version' => 'dev-master',
'version' => 'dev-master',
- 'reference' => 'e7734546c48c106a9d22730073024bad3de3a7b6',
+ 'reference' => '4df515095bbbcca750a6d5e4c0d50fe1d6960b62',
'type' => 'library',
'install_path' => __DIR__ . '/../',
'aliases' => array(),
@@ -301,7 +301,7 @@
'nextcloud/3rdparty' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
- 'reference' => 'e7734546c48c106a9d22730073024bad3de3a7b6',
+ 'reference' => '4df515095bbbcca750a6d5e4c0d50fe1d6960b62',
'type' => 'library',
'install_path' => __DIR__ . '/../',
'aliases' => array(),
@@ -698,8 +698,8 @@
'dev_requirement' => false,
),
'symfony/deprecation-contracts' => array(
- 'pretty_version' => 'v2.5.1',
- 'version' => '2.5.1.0',
+ 'pretty_version' => 'v2.5.2',
+ 'version' => '2.5.2.0',
'reference' => 'e8b495ea28c1d97b5e0c121748d6f9b53d075c66',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
@@ -730,6 +730,15 @@
0 => '1.1',
),
),
+ 'symfony/http-foundation' => array(
+ 'pretty_version' => 'v5.4.10',
+ 'version' => '5.4.10.0',
+ 'reference' => 'e7793b7906f72a8cc51054fbca9dcff7a8af1c1e',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../symfony/http-foundation',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
'symfony/polyfill-ctype' => array(
'pretty_version' => 'v1.23.0',
'version' => '1.23.0.0',
@@ -776,9 +785,9 @@
'dev_requirement' => false,
),
'symfony/polyfill-mbstring' => array(
- 'pretty_version' => 'v1.23.1',
- 'version' => '1.23.1.0',
- 'reference' => '9174a3d80210dca8daa7f31fec659150bbeabfc6',
+ 'pretty_version' => 'v1.26.0',
+ 'version' => '1.26.0.0',
+ 'reference' => '9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-mbstring',
'aliases' => array(),
@@ -803,9 +812,9 @@
'dev_requirement' => false,
),
'symfony/polyfill-php80' => array(
- 'pretty_version' => 'v1.23.1',
- 'version' => '1.23.1.0',
- 'reference' => '1100343ed1a92e3a38f9ae122fc0eb21602547be',
+ 'pretty_version' => 'v1.26.0',
+ 'version' => '1.26.0.0',
+ 'reference' => 'cfa0ae98841b9e461207c13ab093d76b0fa7bace',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-php80',
'aliases' => array(),
diff --git a/symfony/http-foundation/AcceptHeader.php b/symfony/http-foundation/AcceptHeader.php
new file mode 100644
index 00000000..057c6b53
--- /dev/null
+++ b/symfony/http-foundation/AcceptHeader.php
@@ -0,0 +1,168 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation;
+
+// Help opcache.preload discover always-needed symbols
+class_exists(AcceptHeaderItem::class);
+
+/**
+ * Represents an Accept-* header.
+ *
+ * An accept header is compound with a list of items,
+ * sorted by descending quality.
+ *
+ * @author Jean-François Simon <contact@jfsimon.fr>
+ */
+class AcceptHeader
+{
+ /**
+ * @var AcceptHeaderItem[]
+ */
+ private $items = [];
+
+ /**
+ * @var bool
+ */
+ private $sorted = true;
+
+ /**
+ * @param AcceptHeaderItem[] $items
+ */
+ public function __construct(array $items)
+ {
+ foreach ($items as $item) {
+ $this->add($item);
+ }
+ }
+
+ /**
+ * Builds an AcceptHeader instance from a string.
+ *
+ * @return self
+ */
+ public static function fromString(?string $headerValue)
+ {
+ $index = 0;
+
+ $parts = HeaderUtils::split($headerValue ?? '', ',;=');
+
+ return new self(array_map(function ($subParts) use (&$index) {
+ $part = array_shift($subParts);
+ $attributes = HeaderUtils::combine($subParts);
+
+ $item = new AcceptHeaderItem($part[0], $attributes);
+ $item->setIndex($index++);
+
+ return $item;
+ }, $parts));
+ }
+
+ /**
+ * Returns header value's string representation.
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return implode(',', $this->items);
+ }
+
+ /**
+ * Tests if header has given value.
+ *
+ * @return bool
+ */
+ public function has(string $value)
+ {
+ return isset($this->items[$value]);
+ }
+
+ /**
+ * Returns given value's item, if exists.
+ *
+ * @return AcceptHeaderItem|null
+ */
+ public function get(string $value)
+ {
+ return $this->items[$value] ?? $this->items[explode('/', $value)[0].'/*'] ?? $this->items['*/*'] ?? $this->items['*'] ?? null;
+ }
+
+ /**
+ * Adds an item.
+ *
+ * @return $this
+ */
+ public function add(AcceptHeaderItem $item)
+ {
+ $this->items[$item->getValue()] = $item;
+ $this->sorted = false;
+
+ return $this;
+ }
+
+ /**
+ * Returns all items.
+ *
+ * @return AcceptHeaderItem[]
+ */
+ public function all()
+ {
+ $this->sort();
+
+ return $this->items;
+ }
+
+ /**
+ * Filters items on their value using given regex.
+ *
+ * @return self
+ */
+ public function filter(string $pattern)
+ {
+ return new self(array_filter($this->items, function (AcceptHeaderItem $item) use ($pattern) {
+ return preg_match($pattern, $item->getValue());
+ }));
+ }
+
+ /**
+ * Returns first item.
+ *
+ * @return AcceptHeaderItem|null
+ */
+ public function first()
+ {
+ $this->sort();
+
+ return !empty($this->items) ? reset($this->items) : null;
+ }
+
+ /**
+ * Sorts items by descending quality.
+ */
+ private function sort(): void
+ {
+ if (!$this->sorted) {
+ uasort($this->items, function (AcceptHeaderItem $a, AcceptHeaderItem $b) {
+ $qA = $a->getQuality();
+ $qB = $b->getQuality();
+
+ if ($qA === $qB) {
+ return $a->getIndex() > $b->getIndex() ? 1 : -1;
+ }
+
+ return $qA > $qB ? -1 : 1;
+ });
+
+ $this->sorted = true;
+ }
+ }
+}
diff --git a/symfony/http-foundation/AcceptHeaderItem.php b/symfony/http-foundation/AcceptHeaderItem.php
new file mode 100644
index 00000000..8b86eee6
--- /dev/null
+++ b/symfony/http-foundation/AcceptHeaderItem.php
@@ -0,0 +1,177 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation;
+
+/**
+ * Represents an Accept-* header item.
+ *
+ * @author Jean-François Simon <contact@jfsimon.fr>
+ */
+class AcceptHeaderItem
+{
+ private $value;
+ private $quality = 1.0;
+ private $index = 0;
+ private $attributes = [];
+
+ public function __construct(string $value, array $attributes = [])
+ {
+ $this->value = $value;
+ foreach ($attributes as $name => $value) {
+ $this->setAttribute($name, $value);
+ }
+ }
+
+ /**
+ * Builds an AcceptHeaderInstance instance from a string.
+ *
+ * @return self
+ */
+ public static function fromString(?string $itemValue)
+ {
+ $parts = HeaderUtils::split($itemValue ?? '', ';=');
+
+ $part = array_shift($parts);
+ $attributes = HeaderUtils::combine($parts);
+
+ return new self($part[0], $attributes);
+ }
+
+ /**
+ * Returns header value's string representation.
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ $string = $this->value.($this->quality < 1 ? ';q='.$this->quality : '');
+ if (\count($this->attributes) > 0) {
+ $string .= '; '.HeaderUtils::toString($this->attributes, ';');
+ }
+
+ return $string;
+ }
+
+ /**
+ * Set the item value.
+ *
+ * @return $this
+ */
+ public function setValue(string $value)
+ {
+ $this->value = $value;
+
+ return $this;
+ }
+
+ /**
+ * Returns the item value.
+ *
+ * @return string
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * Set the item quality.
+ *
+ * @return $this
+ */
+ public function setQuality(float $quality)
+ {
+ $this->quality = $quality;
+
+ return $this;
+ }
+
+ /**
+ * Returns the item quality.
+ *
+ * @return float
+ */
+ public function getQuality()
+ {
+ return $this->quality;
+ }
+
+ /**
+ * Set the item index.
+ *
+ * @return $this
+ */
+ public function setIndex(int $index)
+ {
+ $this->index = $index;
+
+ return $this;
+ }
+
+ /**
+ * Returns the item index.
+ *
+ * @return int
+ */
+ public function getIndex()
+ {
+ return $this->index;
+ }
+
+ /**
+ * Tests if an attribute exists.
+ *
+ * @return bool
+ */
+ public function hasAttribute(string $name)
+ {
+ return isset($this->attributes[$name]);
+ }
+
+ /**
+ * Returns an attribute by its name.
+ *
+ * @param mixed $default
+ *
+ * @return mixed
+ */
+ public function getAttribute(string $name, $default = null)
+ {
+ return $this->attributes[$name] ?? $default;
+ }
+
+ /**
+ * Returns all attributes.
+ *
+ * @return array
+ */
+ public function getAttributes()
+ {
+ return $this->attributes;
+ }
+
+ /**
+ * Set an attribute.
+ *
+ * @return $this
+ */
+ public function setAttribute(string $name, string $value)
+ {
+ if ('q' === $name) {
+ $this->quality = (float) $value;
+ } else {
+ $this->attributes[$name] = $value;
+ }
+
+ return $this;
+ }
+}
diff --git a/symfony/http-foundation/BinaryFileResponse.php b/symfony/http-foundation/BinaryFileResponse.php
new file mode 100644
index 00000000..4769cab0
--- /dev/null
+++ b/symfony/http-foundation/BinaryFileResponse.php
@@ -0,0 +1,363 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation;
+
+use Symfony\Component\HttpFoundation\File\Exception\FileException;
+use Symfony\Component\HttpFoundation\File\File;
+
+/**
+ * BinaryFileResponse represents an HTTP response delivering a file.
+ *
+ * @author Niklas Fiekas <niklas.fiekas@tu-clausthal.de>
+ * @author stealth35 <stealth35-php@live.fr>
+ * @author Igor Wiedler <igor@wiedler.ch>
+ * @author Jordan Alliot <jordan.alliot@gmail.com>
+ * @author Sergey Linnik <linniksa@gmail.com>
+ */
+class BinaryFileResponse extends Response
+{
+ protected static $trustXSendfileTypeHeader = false;
+
+ /**
+ * @var File
+ */
+ protected $file;
+ protected $offset = 0;
+ protected $maxlen = -1;
+ protected $deleteFileAfterSend = false;
+
+ /**
+ * @param \SplFileInfo|string $file The file to stream
+ * @param int $status The response status code
+ * @param array $headers An array of response headers
+ * @param bool $public Files are public by default
+ * @param string|null $contentDisposition The type of Content-Disposition to set automatically with the filename
+ * @param bool $autoEtag Whether the ETag header should be automatically set
+ * @param bool $autoLastModified Whether the Last-Modified header should be automatically set
+ */
+ public function __construct($file, int $status = 200, array $headers = [], bool $public = true, string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true)
+ {
+ parent::__construct(null, $status, $headers);
+
+ $this->setFile($file, $contentDisposition, $autoEtag, $autoLastModified);
+
+ if ($public) {
+ $this->setPublic();
+ }
+ }
+
+ /**
+ * @param \SplFileInfo|string $file The file to stream
+ * @param int $status The response status code
+ * @param array $headers An array of response headers
+ * @param bool $public Files are public by default
+ * @param string|null $contentDisposition The type of Content-Disposition to set automatically with the filename
+ * @param bool $autoEtag Whether the ETag header should be automatically set
+ * @param bool $autoLastModified Whether the Last-Modified header should be automatically set
+ *
+ * @return static
+ *
+ * @deprecated since Symfony 5.2, use __construct() instead.
+ */
+ public static function create($file = null, int $status = 200, array $headers = [], bool $public = true, string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true)
+ {
+ trigger_deprecation('symfony/http-foundation', '5.2', 'The "%s()" method is deprecated, use "new %s()" instead.', __METHOD__, static::class);
+
+ return new static($file, $status, $headers, $public, $contentDisposition, $autoEtag, $autoLastModified);
+ }
+
+ /**
+ * Sets the file to stream.
+ *
+ * @param \SplFileInfo|string $file The file to stream
+ *
+ * @return $this
+ *
+ * @throws FileException
+ */
+ public function setFile($file, string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true)
+ {
+ if (!$file instanceof File) {
+ if ($file instanceof \SplFileInfo) {
+ $file = new File($file->getPathname());
+ } else {
+ $file = new File((string) $file);
+ }
+ }
+
+ if (!$file->isReadable()) {
+ throw new FileException('File must be readable.');
+ }
+
+ $this->file = $file;
+
+ if ($autoEtag) {
+ $this->setAutoEtag();
+ }
+
+ if ($autoLastModified) {
+ $this->setAutoLastModified();
+ }
+
+ if ($contentDisposition) {
+ $this->setContentDisposition($contentDisposition);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Gets the file.
+ *
+ * @return File
+ */
+ public function getFile()
+ {
+ return $this->file;
+ }
+
+ /**
+ * Automatically sets the Last-Modified header according the file modification date.
+ *
+ * @return $this
+ */
+ public function setAutoLastModified()
+ {
+ $this->setLastModified(\DateTime::createFromFormat('U', $this->file->getMTime()));
+
+ return $this;
+ }
+
+ /**
+ * Automatically sets the ETag header according to the checksum of the file.
+ *
+ * @return $this
+ */
+ public function setAutoEtag()
+ {
+ $this->setEtag(base64_encode(hash_file('sha256', $this->file->getPathname(), true)));
+
+ return $this;
+ }
+
+ /**
+ * Sets the Content-Disposition header with the given filename.
+ *
+ * @param string $disposition ResponseHeaderBag::DISPOSITION_INLINE or ResponseHeaderBag::DISPOSITION_ATTACHMENT
+ * @param string $filename Optionally use this UTF-8 encoded filename instead of the real name of the file
+ * @param string $filenameFallback A fallback filename, containing only ASCII characters. Defaults to an automatically encoded filename
+ *
+ * @return $this
+ */
+ public function setContentDisposition(string $disposition, string $filename = '', string $filenameFallback = '')
+ {
+ if ('' === $filename) {
+ $filename = $this->file->getFilename();
+ }
+
+ if ('' === $filenameFallback && (!preg_match('/^[\x20-\x7e]*$/', $filename) || str_contains($filename, '%'))) {
+ $encoding = mb_detect_encoding($filename, null, true) ?: '8bit';
+
+ for ($i = 0, $filenameLength = mb_strlen($filename, $encoding); $i < $filenameLength; ++$i) {
+ $char = mb_substr($filename, $i, 1, $encoding);
+
+ if ('%' === $char || \ord($char) < 32 || \ord($char) > 126) {
+ $filenameFallback .= '_';
+ } else {
+ $filenameFallback .= $char;
+ }
+ }
+ }
+
+ $dispositionHeader = $this->headers->makeDisposition($disposition, $filename, $filenameFallback);
+ $this->headers->set('Content-Disposition', $dispositionHeader);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function prepare(Request $request)
+ {
+ if (!$this->headers->has('Content-Type')) {
+ $this->headers->set('Content-Type', $this->file->getMimeType() ?: 'application/octet-stream');
+ }
+
+ if ('HTTP/1.0' !== $request->server->get('SERVER_PROTOCOL')) {
+ $this->setProtocolVersion('1.1');
+ }
+
+ $this->ensureIEOverSSLCompatibility($request);
+
+ $this->offset = 0;
+ $this->maxlen = -1;
+
+ if (false === $fileSize = $this->file->getSize()) {
+ return $this;
+ }
+ $this->headers->set('Content-Length', $fileSize);
+
+ if (!$this->headers->has('Accept-Ranges')) {
+ // Only accept ranges on safe HTTP methods
+ $this->headers->set('Accept-Ranges', $request->isMethodSafe() ? 'bytes' : 'none');
+ }
+
+ if (self::$trustXSendfileTypeHeader && $request->headers->has('X-Sendfile-Type')) {
+ // Use X-Sendfile, do not send any content.
+ $type = $request->headers->get('X-Sendfile-Type');
+ $path = $this->file->getRealPath();
+ // Fall back to scheme://path for stream wrapped locations.
+ if (false === $path) {
+ $path = $this->file->getPathname();
+ }
+ if ('x-accel-redirect' === strtolower($type)) {
+ // Do X-Accel-Mapping substitutions.
+ // @link https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/#x-accel-redirect
+ $parts = HeaderUtils::split($request->headers->get('X-Accel-Mapping', ''), ',=');
+ foreach ($parts as $part) {
+ [$pathPrefix, $location] = $part;
+ if (substr($path, 0, \strlen($pathPrefix)) === $pathPrefix) {
+ $path = $location.substr($path, \strlen($pathPrefix));
+ // Only set X-Accel-Redirect header if a valid URI can be produced
+ // as nginx does not serve arbitrary file paths.
+ $this->headers->set($type, $path);
+ $this->maxlen = 0;
+ break;
+ }
+ }
+ } else {
+ $this->headers->set($type, $path);
+ $this->maxlen = 0;
+ }
+ } elseif ($request->headers->has('Range') && $request->isMethod('GET')) {
+ // Process the range headers.
+ if (!$request->headers->has('If-Range') || $this->hasValidIfRangeHeader($request->headers->get('If-Range'))) {
+ $range = $request->headers->get('Range');
+
+ if (str_starts_with($range, 'bytes=')) {
+ [$start, $end] = explode('-', substr($range, 6), 2) + [0];
+
+ $end = ('' === $end) ? $fileSize - 1 : (int) $end;
+
+ if ('' === $start) {
+ $start = $fileSize - $end;
+ $end = $fileSize - 1;
+ } else {
+ $start = (int) $start;
+ }
+
+ if ($start <= $end) {
+ $end = min($end, $fileSize - 1);
+ if ($start < 0 || $start > $end) {
+ $this->setStatusCode(416);
+ $this->headers->set('Content-Range', sprintf('bytes */%s', $fileSize));
+ } elseif ($end - $start < $fileSize - 1) {
+ $this->maxlen = $end < $fileSize ? $end - $start + 1 : -1;
+ $this->offset = $start;
+
+ $this->setStatusCode(206);
+ $this->headers->set('Content-Range', sprintf('bytes %s-%s/%s', $start, $end, $fileSize));
+ $this->headers->set('Content-Length', $end - $start + 1);
+ }
+ }
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ private function hasValidIfRangeHeader(?string $header): bool
+ {
+ if ($this->getEtag() === $header) {
+ return true;
+ }
+
+ if (null === $lastModified = $this->getLastModified()) {
+ return false;
+ }
+
+ return $lastModified->format('D, d M Y H:i:s').' GMT' === $header;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function sendContent()
+ {
+ if (!$this->isSuccessful()) {
+ return parent::sendContent();
+ }
+
+ if (0 === $this->maxlen) {
+ return $this;
+ }
+
+ $out = fopen('php://output', 'w');
+ $file = fopen($this->file->getPathname(), 'r');
+
+ stream_copy_to_stream($file, $out, $this->maxlen, $this->offset);
+
+ fclose($out);
+ fclose($file);
+
+ if ($this->deleteFileAfterSend && is_file($this->file->getPathname())) {
+ unlink($this->file->getPathname());
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @throws \LogicException when the content is not null
+ */
+ public function setContent(?string $content)
+ {
+ if (null !== $content) {
+ throw new \LogicException('The content cannot be set on a BinaryFileResponse instance.');
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getContent()
+ {
+ return false;
+ }
+
+ /**
+ * Trust X-Sendfile-Type header.
+ */
+ public static function trustXSendfileTypeHeader()
+ {
+ self::$trustXSendfileTypeHeader = true;
+ }
+
+ /**
+ * If this is set to true, the file will be unlinked after the request is sent
+ * Note: If the X-Sendfile header is used, the deleteFileAfterSend setting will not be used.
+ *
+ * @return $this
+ */
+ public function deleteFileAfterSend(bool $shouldDelete = true)
+ {
+ $this->deleteFileAfterSend = $shouldDelete;
+
+ return $this;
+ }
+}
diff --git a/symfony/http-foundation/Cookie.php b/symfony/http-foundation/Cookie.php
new file mode 100644
index 00000000..b4b26c01
--- /dev/null
+++ b/symfony/http-foundation/Cookie.php
@@ -0,0 +1,422 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation;
+
+/**
+ * Represents a cookie.
+ *
+ * @author Johannes M. Schmitt <schmittjoh@gmail.com>
+ */
+class Cookie
+{
+ public const SAMESITE_NONE = 'none';
+ public const SAMESITE_LAX = 'lax';
+ public const SAMESITE_STRICT = 'strict';
+
+ protected $name;
+ protected $value;
+ protected $domain;
+ protected $expire;
+ protected $path;
+ protected $secure;
+ protected $httpOnly;
+
+ private $raw;
+ private $sameSite;
+ private $secureDefault = false;
+
+ private const RESERVED_CHARS_LIST = "=,; \t\r\n\v\f";
+ private const RESERVED_CHARS_FROM = ['=', ',', ';', ' ', "\t", "\r", "\n", "\v", "\f"];
+ private const RESERVED_CHARS_TO = ['%3D', '%2C', '%3B', '%20', '%09', '%0D', '%0A', '%0B', '%0C'];
+
+ /**
+ * Creates cookie from raw header string.
+ *
+ * @return static
+ */
+ public static function fromString(string $cookie, bool $decode = false)
+ {
+ $data = [
+ 'expires' => 0,
+ 'path' => '/',
+ 'domain' => null,
+ 'secure' => false,
+ 'httponly' => false,
+ 'raw' => !$decode,
+ 'samesite' => null,
+ ];
+
+ $parts = HeaderUtils::split($cookie, ';=');
+ $part = array_shift($parts);
+
+ $name = $decode ? urldecode($part[0]) : $part[0];
+ $value = isset($part[1]) ? ($decode ? urldecode($part[1]) : $part[1]) : null;
+
+ $data = HeaderUtils::combine($parts) + $data;
+ $data['expires'] = self::expiresTimestamp($data['expires']);
+
+ if (isset($data['max-age']) && ($data['max-age'] > 0 || $data['expires'] > time())) {
+ $data['expires'] = time() + (int) $data['max-age'];
+ }
+
+ return new static($name, $value, $data['expires'], $data['path'], $data['domain'], $data['secure'], $data['httponly'], $data['raw'], $data['samesite']);
+ }
+
+ public static function create(string $name, string $value = null, $expire = 0, ?string $path = '/', string $domain = null, bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = self::SAMESITE_LAX): self
+ {
+ return new self($name, $value, $expire, $path, $domain, $secure, $httpOnly, $raw, $sameSite);
+ }
+
+ /**
+ * @param string $name The name of the cookie
+ * @param string|null $value The value of the cookie
+ * @param int|string|\DateTimeInterface $expire The time the cookie expires
+ * @param string $path The path on the server in which the cookie will be available on
+ * @param string|null $domain The domain that the cookie is available to
+ * @param bool|null $secure Whether the client should send back the cookie only over HTTPS or null to auto-enable this when the request is already using HTTPS
+ * @param bool $httpOnly Whether the cookie will be made accessible only through the HTTP protocol
+ * @param bool $raw Whether the cookie value should be sent with no url encoding
+ * @param string|null $sameSite Whether the cookie will be available for cross-site requests
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function __construct(string $name, string $value = null, $expire = 0, ?string $path = '/', string $domain = null, bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = 'lax')
+ {
+ // from PHP source code
+ if ($raw && false !== strpbrk($name, self::RESERVED_CHARS_LIST)) {
+ throw new \InvalidArgumentException(sprintf('The cookie name "%s" contains invalid characters.', $name));
+ }
+
+ if (empty($name)) {
+ throw new \InvalidArgumentException('The cookie name cannot be empty.');
+ }
+
+ $this->name = $name;
+ $this->value = $value;
+ $this->domain = $domain;
+ $this->expire = self::expiresTimestamp($expire);
+ $this->path = empty($path) ? '/' : $path;
+ $this->secure = $secure;
+ $this->httpOnly = $httpOnly;
+ $this->raw = $raw;
+ $this->sameSite = $this->withSameSite($sameSite)->sameSite;
+ }
+
+ /**
+ * Creates a cookie copy with a new value.
+ *
+ * @return static
+ */
+ public function withValue(?string $value): self
+ {
+ $cookie = clone $this;
+ $cookie->value = $value;
+
+ return $cookie;
+ }
+
+ /**
+ * Creates a cookie copy with a new domain that the cookie is available to.
+ *
+ * @return static
+ */
+ public function withDomain(?string $domain): self
+ {
+ $cookie = clone $this;
+ $cookie->domain = $domain;
+
+ return $cookie;
+ }
+
+ /**
+ * Creates a cookie copy with a new time the cookie expires.
+ *
+ * @param int|string|\DateTimeInterface $expire
+ *
+ * @return static
+ */
+ public function withExpires($expire = 0): self
+ {
+ $cookie = clone $this;
+ $cookie->expire = self::expiresTimestamp($expire);
+
+ return $cookie;
+ }
+
+ /**
+ * Converts expires formats to a unix timestamp.
+ *
+ * @param int|string|\DateTimeInterface $expire
+ */
+ private static function expiresTimestamp($expire = 0): int
+ {
+ // convert expiration time to a Unix timestamp
+ if ($expire instanceof \DateTimeInterface) {
+ $expire = $expire->format('U');
+ } elseif (!is_numeric($expire)) {
+ $expire = strtotime($expire);
+
+ if (false === $expire) {
+ throw new \InvalidArgumentException('The cookie expiration time is not valid.');
+ }
+ }
+
+ return 0 < $expire ? (int) $expire : 0;
+ }
+
+ /**
+ * Creates a cookie copy with a new path on the server in which the cookie will be available on.
+ *
+ * @return static
+ */
+ public function withPath(string $path): self
+ {
+ $cookie = clone $this;
+ $cookie->path = '' === $path ? '/' : $path;
+
+ return $cookie;
+ }
+
+ /**
+ * Creates a cookie copy that only be transmitted over a secure HTTPS connection from the client.
+ *
+ * @return static
+ */
+ public function withSecure(bool $secure = true): self
+ {
+ $cookie = clone $this;
+ $cookie->secure = $secure;
+
+ return $cookie;
+ }
+
+ /**
+ * Creates a cookie copy that be accessible only through the HTTP protocol.
+ *
+ * @return static
+ */
+ public function withHttpOnly(bool $httpOnly = true): self
+ {
+ $cookie = clone $this;
+ $cookie->httpOnly = $httpOnly;
+
+ return $cookie;
+ }
+
+ /**
+ * Creates a cookie copy that uses no url encoding.
+ *
+ * @return static
+ */
+ public function withRaw(bool $raw = true): self
+ {
+ if ($raw && false !== strpbrk($this->name, self::RESERVED_CHARS_LIST)) {
+ throw new \InvalidArgumentException(sprintf('The cookie name "%s" contains invalid characters.', $this->name));
+ }
+
+ $cookie = clone $this;
+ $cookie->raw = $raw;
+
+ return $cookie;
+ }
+
+ /**
+ * Creates a cookie copy with SameSite attribute.
+ *
+ * @return static
+ */
+ public function withSameSite(?string $sameSite): self
+ {
+ if ('' === $sameSite) {
+ $sameSite = null;
+ } elseif (null !== $sameSite) {
+ $sameSite = strtolower($sameSite);
+ }
+
+ if (!\in_array($sameSite, [self::SAMESITE_LAX, self::SAMESITE_STRICT, self::SAMESITE_NONE, null], true)) {
+ throw new \InvalidArgumentException('The "sameSite" parameter value is not valid.');
+ }
+
+ $cookie = clone $this;
+ $cookie->sameSite = $sameSite;
+
+ return $cookie;
+ }
+
+ /**
+ * Returns the cookie as a string.
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ if ($this->isRaw()) {
+ $str = $this->getName();
+ } else {
+ $str = str_replace(self::RESERVED_CHARS_FROM, self::RESERVED_CHARS_TO, $this->getName());
+ }
+
+ $str .= '=';
+
+ if ('' === (string) $this->getValue()) {
+ $str .= 'deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; Max-Age=0';
+ } else {
+ $str .= $this->isRaw() ? $this->getValue() : rawurlencode($this->getValue());
+
+ if (0 !== $this->getExpiresTime()) {
+ $str .= '; expires='.gmdate('D, d-M-Y H:i:s T', $this->getExpiresTime()).'; Max-Age='.$this->getMaxAge();
+ }
+ }
+
+ if ($this->getPath()) {
+ $str .= '; path='.$this->getPath();
+ }
+
+ if ($this->getDomain()) {
+ $str .= '; domain='.$this->getDomain();
+ }
+
+ if (true === $this->isSecure()) {
+ $str .= '; secure';
+ }
+
+ if (true === $this->isHttpOnly()) {
+ $str .= '; httponly';
+ }
+
+ if (null !== $this->getSameSite()) {
+ $str .= '; samesite='.$this->getSameSite();
+ }
+
+ return $str;
+ }
+
+ /**
+ * Gets the name of the cookie.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Gets the value of the cookie.
+ *
+ * @return string|null
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * Gets the domain that the cookie is available to.
+ *
+ * @return string|null
+ */
+ public function getDomain()
+ {
+ return $this->domain;
+ }
+
+ /**
+ * Gets the time the cookie expires.
+ *
+ * @return int
+ */
+ public function getExpiresTime()
+ {
+ return $this->expire;
+ }
+
+ /**
+ * Gets the max-age attribute.
+ *
+ * @return int
+ */
+ public function getMaxAge()
+ {
+ $maxAge = $this->expire - time();
+
+ return 0 >= $maxAge ? 0 : $maxAge;
+ }
+
+ /**
+ * Gets the path on the server in which the cookie will be available on.
+ *
+ * @return string
+ */
+ public function getPath()
+ {
+ return $this->path;
+ }
+
+ /**
+ * Checks whether the cookie should only be transmitted over a secure HTTPS connection from the client.
+ *
+ * @return bool
+ */
+ public function isSecure()
+ {
+ return $this->secure ?? $this->secureDefault;
+ }
+
+ /**
+ * Checks whether the cookie will be made accessible only through the HTTP protocol.
+ *
+ * @return bool
+ */
+ public function isHttpOnly()
+ {
+ return $this->httpOnly;
+ }
+
+ /**
+ * Whether this cookie is about to be cleared.
+ *
+ * @return bool
+ */
+ public function isCleared()
+ {
+ return 0 !== $this->expire && $this->expire < time();
+ }
+
+ /**
+ * Checks if the cookie value should be sent with no url encoding.
+ *
+ * @return bool
+ */
+ public function isRaw()
+ {
+ return $this->raw;
+ }
+
+ /**
+ * Gets the SameSite attribute.
+ *
+ * @return string|null
+ */
+ public function getSameSite()
+ {
+ return $this->sameSite;
+ }
+
+ /**
+ * @param bool $default The default value of the "secure" flag when it is set to null
+ */
+ public function setSecureDefault(bool $default): void
+ {
+ $this->secureDefault = $default;
+ }
+}
diff --git a/symfony/http-foundation/Exception/BadRequestException.php b/symfony/http-foundation/Exception/BadRequestException.php
new file mode 100644
index 00000000..e4bb309c
--- /dev/null
+++ b/symfony/http-foundation/Exception/BadRequestException.php
@@ -0,0 +1,19 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Exception;
+
+/**
+ * Raised when a user sends a malformed request.
+ */
+class BadRequestException extends \UnexpectedValueException implements RequestExceptionInterface
+{
+}
diff --git a/symfony/http-foundation/Exception/ConflictingHeadersException.php b/symfony/http-foundation/Exception/ConflictingHeadersException.php
new file mode 100644
index 00000000..5fcf5b42
--- /dev/null
+++ b/symfony/http-foundation/Exception/ConflictingHeadersException.php
@@ -0,0 +1,21 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Exception;
+
+/**
+ * The HTTP request contains headers with conflicting information.
+ *
+ * @author Magnus Nordlander <magnus@fervo.se>
+ */
+class ConflictingHeadersException extends \UnexpectedValueException implements RequestExceptionInterface
+{
+}
diff --git a/symfony/http-foundation/Exception/JsonException.php b/symfony/http-foundation/Exception/JsonException.php
new file mode 100644
index 00000000..5990e760
--- /dev/null
+++ b/symfony/http-foundation/Exception/JsonException.php
@@ -0,0 +1,21 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Exception;
+
+/**
+ * Thrown by Request::toArray() when the content cannot be JSON-decoded.
+ *
+ * @author Tobias Nyholm <tobias.nyholm@gmail.com>
+ */
+final class JsonException extends \UnexpectedValueException implements RequestExceptionInterface
+{
+}
diff --git a/symfony/http-foundation/Exception/RequestExceptionInterface.php b/symfony/http-foundation/Exception/RequestExceptionInterface.php
new file mode 100644
index 00000000..478d0dc7
--- /dev/null
+++ b/symfony/http-foundation/Exception/RequestExceptionInterface.php
@@ -0,0 +1,21 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Exception;
+
+/**
+ * Interface for Request exceptions.
+ *
+ * Exceptions implementing this interface should trigger an HTTP 400 response in the application code.
+ */
+interface RequestExceptionInterface
+{
+}
diff --git a/symfony/http-foundation/Exception/SessionNotFoundException.php b/symfony/http-foundation/Exception/SessionNotFoundException.php
new file mode 100644
index 00000000..9c719aa0
--- /dev/null
+++ b/symfony/http-foundation/Exception/SessionNotFoundException.php
@@ -0,0 +1,27 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Exception;
+
+/**
+ * Raised when a session does not exists. This happens in the following cases:
+ * - the session is not enabled
+ * - attempt to read a session outside a request context (ie. cli script).
+ *
+ * @author Jérémy Derussé <jeremy@derusse.com>
+ */
+class SessionNotFoundException extends \LogicException implements RequestExceptionInterface
+{
+ public function __construct(string $message = 'There is currently no session available.', int $code = 0, \Throwable $previous = null)
+ {
+ parent::__construct($message, $code, $previous);
+ }
+}
diff --git a/symfony/http-foundation/Exception/SuspiciousOperationException.php b/symfony/http-foundation/Exception/SuspiciousOperationException.php
new file mode 100644
index 00000000..ae7a5f13
--- /dev/null
+++ b/symfony/http-foundation/Exception/SuspiciousOperationException.php
@@ -0,0 +1,20 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Exception;
+
+/**
+ * Raised when a user has performed an operation that should be considered
+ * suspicious from a security perspective.
+ */
+class SuspiciousOperationException extends \UnexpectedValueException implements RequestExceptionInterface
+{
+}
diff --git a/symfony/http-foundation/ExpressionRequestMatcher.php b/symfony/http-foundation/ExpressionRequestMatcher.php
new file mode 100644
index 00000000..26bed7d3
--- /dev/null
+++ b/symfony/http-foundation/ExpressionRequestMatcher.php
@@ -0,0 +1,47 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation;
+
+use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
+
+/**
+ * ExpressionRequestMatcher uses an expression to match a Request.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class ExpressionRequestMatcher extends RequestMatcher
+{
+ private $language;
+ private $expression;
+
+ public function setExpression(ExpressionLanguage $language, $expression)
+ {
+ $this->language = $language;
+ $this->expression = $expression;
+ }
+
+ public function matches(Request $request)
+ {
+ if (!$this->language) {
+ throw new \LogicException('Unable to match the request as the expression language is not available.');
+ }
+
+ return $this->language->evaluate($this->expression, [
+ 'request' => $request,
+ 'method' => $request->getMethod(),
+ 'path' => rawurldecode($request->getPathInfo()),
+ 'host' => $request->getHost(),
+ 'ip' => $request->getClientIp(),
+ 'attributes' => $request->attributes->all(),
+ ]) && parent::matches($request);
+ }
+}
diff --git a/symfony/http-foundation/File/Exception/AccessDeniedException.php b/symfony/http-foundation/File/Exception/AccessDeniedException.php
new file mode 100644
index 00000000..136d2a9f
--- /dev/null
+++ b/symfony/http-foundation/File/Exception/AccessDeniedException.php
@@ -0,0 +1,25 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\File\Exception;
+
+/**
+ * Thrown when the access on a file was denied.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class AccessDeniedException extends FileException
+{
+ public function __construct(string $path)
+ {
+ parent::__construct(sprintf('The file %s could not be accessed', $path));
+ }
+}
diff --git a/symfony/http-foundation/File/Exception/CannotWriteFileException.php b/symfony/http-foundation/File/Exception/CannotWriteFileException.php
new file mode 100644
index 00000000..c49f53a6
--- /dev/null
+++ b/symfony/http-foundation/File/Exception/CannotWriteFileException.php
@@ -0,0 +1,21 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\File\Exception;
+
+/**
+ * Thrown when an UPLOAD_ERR_CANT_WRITE error occurred with UploadedFile.
+ *
+ * @author Florent Mata <florentmata@gmail.com>
+ */
+class CannotWriteFileException extends FileException
+{
+}
diff --git a/symfony/http-foundation/File/Exception/ExtensionFileException.php b/symfony/http-foundation/File/Exception/ExtensionFileException.php
new file mode 100644
index 00000000..ed83499c
--- /dev/null
+++ b/symfony/http-foundation/File/Exception/ExtensionFileException.php
@@ -0,0 +1,21 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\File\Exception;
+
+/**
+ * Thrown when an UPLOAD_ERR_EXTENSION error occurred with UploadedFile.
+ *
+ * @author Florent Mata <florentmata@gmail.com>
+ */
+class ExtensionFileException extends FileException
+{
+}
diff --git a/symfony/http-foundation/File/Exception/FileException.php b/symfony/http-foundation/File/Exception/FileException.php
new file mode 100644
index 00000000..fad5133e
--- /dev/null
+++ b/symfony/http-foundation/File/Exception/FileException.php
@@ -0,0 +1,21 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\File\Exception;
+
+/**
+ * Thrown when an error occurred in the component File.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class FileException extends \RuntimeException
+{
+}
diff --git a/symfony/http-foundation/File/Exception/FileNotFoundException.php b/symfony/http-foundation/File/Exception/FileNotFoundException.php
new file mode 100644
index 00000000..31bdf68f
--- /dev/null
+++ b/symfony/http-foundation/File/Exception/FileNotFoundException.php
@@ -0,0 +1,25 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\File\Exception;
+
+/**
+ * Thrown when a file was not found.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class FileNotFoundException extends FileException
+{
+ public function __construct(string $path)
+ {
+ parent::__construct(sprintf('The file "%s" does not exist', $path));
+ }
+}
diff --git a/symfony/http-foundation/File/Exception/FormSizeFileException.php b/symfony/http-foundation/File/Exception/FormSizeFileException.php
new file mode 100644
index 00000000..8741be08
--- /dev/null
+++ b/symfony/http-foundation/File/Exception/FormSizeFileException.php
@@ -0,0 +1,21 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\File\Exception;
+
+/**
+ * Thrown when an UPLOAD_ERR_FORM_SIZE error occurred with UploadedFile.
+ *
+ * @author Florent Mata <florentmata@gmail.com>
+ */
+class FormSizeFileException extends FileException
+{
+}
diff --git a/symfony/http-foundation/File/Exception/IniSizeFileException.php b/symfony/http-foundation/File/Exception/IniSizeFileException.php
new file mode 100644
index 00000000..c8fde610
--- /dev/null
+++ b/symfony/http-foundation/File/Exception/IniSizeFileException.php
@@ -0,0 +1,21 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\File\Exception;
+
+/**
+ * Thrown when an UPLOAD_ERR_INI_SIZE error occurred with UploadedFile.
+ *
+ * @author Florent Mata <florentmata@gmail.com>
+ */
+class IniSizeFileException extends FileException
+{
+}
diff --git a/symfony/http-foundation/File/Exception/NoFileException.php b/symfony/http-foundation/File/Exception/NoFileException.php
new file mode 100644
index 00000000..4b48cc77
--- /dev/null
+++ b/symfony/http-foundation/File/Exception/NoFileException.php
@@ -0,0 +1,21 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\File\Exception;
+
+/**
+ * Thrown when an UPLOAD_ERR_NO_FILE error occurred with UploadedFile.
+ *
+ * @author Florent Mata <florentmata@gmail.com>
+ */
+class NoFileException extends FileException
+{
+}
diff --git a/symfony/http-foundation/File/Exception/NoTmpDirFileException.php b/symfony/http-foundation/File/Exception/NoTmpDirFileException.php
new file mode 100644
index 00000000..bdead2d9
--- /dev/null
+++ b/symfony/http-foundation/File/Exception/NoTmpDirFileException.php
@@ -0,0 +1,21 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\File\Exception;
+
+/**
+ * Thrown when an UPLOAD_ERR_NO_TMP_DIR error occurred with UploadedFile.
+ *
+ * @author Florent Mata <florentmata@gmail.com>
+ */
+class NoTmpDirFileException extends FileException
+{
+}
diff --git a/symfony/http-foundation/File/Exception/PartialFileException.php b/symfony/http-foundation/File/Exception/PartialFileException.php
new file mode 100644
index 00000000..4641efb5
--- /dev/null
+++ b/symfony/http-foundation/File/Exception/PartialFileException.php
@@ -0,0 +1,21 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\File\Exception;
+
+/**
+ * Thrown when an UPLOAD_ERR_PARTIAL error occurred with UploadedFile.
+ *
+ * @author Florent Mata <florentmata@gmail.com>
+ */
+class PartialFileException extends FileException
+{
+}
diff --git a/symfony/http-foundation/File/Exception/UnexpectedTypeException.php b/symfony/http-foundation/File/Exception/UnexpectedTypeException.php
new file mode 100644
index 00000000..8533f99a
--- /dev/null
+++ b/symfony/http-foundation/File/Exception/UnexpectedTypeException.php
@@ -0,0 +1,20 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\File\Exception;
+
+class UnexpectedTypeException extends FileException
+{
+ public function __construct($value, string $expectedType)
+ {
+ parent::__construct(sprintf('Expected argument of type %s, %s given', $expectedType, get_debug_type($value)));
+ }
+}
diff --git a/symfony/http-foundation/File/Exception/UploadException.php b/symfony/http-foundation/File/Exception/UploadException.php
new file mode 100644
index 00000000..7074e765
--- /dev/null
+++ b/symfony/http-foundation/File/Exception/UploadException.php
@@ -0,0 +1,21 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\File\Exception;
+
+/**
+ * Thrown when an error occurred during file upload.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class UploadException extends FileException
+{
+}
diff --git a/symfony/http-foundation/File/File.php b/symfony/http-foundation/File/File.php
new file mode 100644
index 00000000..d941577d
--- /dev/null
+++ b/symfony/http-foundation/File/File.php
@@ -0,0 +1,152 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\File;
+
+use Symfony\Component\HttpFoundation\File\Exception\FileException;
+use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
+use Symfony\Component\Mime\MimeTypes;
+
+/**
+ * A file in the file system.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class File extends \SplFileInfo
+{
+ /**
+ * Constructs a new file from the given path.
+ *
+ * @param string $path The path to the file
+ * @param bool $checkPath Whether to check the path or not
+ *
+ * @throws FileNotFoundException If the given path is not a file
+ */
+ public function __construct(string $path, bool $checkPath = true)
+ {
+ if ($checkPath && !is_file($path)) {
+ throw new FileNotFoundException($path);
+ }
+
+ parent::__construct($path);
+ }
+
+ /**
+ * Returns the extension based on the mime type.
+ *
+ * If the mime type is unknown, returns null.
+ *
+ * This method uses the mime type as guessed by getMimeType()
+ * to guess the file extension.
+ *
+ * @return string|null
+ *
+ * @see MimeTypes
+ * @see getMimeType()
+ */
+ public function guessExtension()
+ {
+ if (!class_exists(MimeTypes::class)) {
+ throw new \LogicException('You cannot guess the extension as the Mime component is not installed. Try running "composer require symfony/mime".');
+ }
+
+ return MimeTypes::getDefault()->getExtensions($this->getMimeType())[0] ?? null;
+ }
+
+ /**
+ * Returns the mime type of the file.
+ *
+ * The mime type is guessed using a MimeTypeGuesserInterface instance,
+ * which uses finfo_file() then the "file" system binary,
+ * depending on which of those are available.
+ *
+ * @return string|null
+ *
+ * @see MimeTypes
+ */
+ public function getMimeType()
+ {
+ if (!class_exists(MimeTypes::class)) {
+ throw new \LogicException('You cannot guess the mime type as the Mime component is not installed. Try running "composer require symfony/mime".');
+ }
+
+ return MimeTypes::getDefault()->guessMimeType($this->getPathname());
+ }
+
+ /**
+ * Moves the file to a new location.
+ *
+ * @return self
+ *
+ * @throws FileException if the target file could not be created
+ */
+ public function move(string $directory, string $name = null)
+ {
+ $target = $this->getTargetFile($directory, $name);
+
+ set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; });
+ try {
+ $renamed = rename($this->getPathname(), $target);
+ } finally {
+ restore_error_handler();
+ }
+ if (!$renamed) {
+ throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s).', $this->getPathname(), $target, strip_tags($error)));
+ }
+
+ @chmod($target, 0666 & ~umask());
+
+ return $target;
+ }
+
+ public function getContent(): string
+ {
+ $content = file_get_contents($this->getPathname());
+
+ if (false === $content) {
+ throw new FileException(sprintf('Could not get the content of the file "%s".', $this->getPathname()));
+ }
+
+ return $content;
+ }
+
+ /**
+ * @return self
+ */
+ protected function getTargetFile(string $directory, string $name = null)
+ {
+ if (!is_dir($directory)) {
+ if (false === @mkdir($directory, 0777, true) && !is_dir($directory)) {
+ throw new FileException(sprintf('Unable to create the "%s" directory.', $directory));
+ }
+ } elseif (!is_writable($directory)) {
+ throw new FileException(sprintf('Unable to write in the "%s" directory.', $directory));
+ }
+
+ $target = rtrim($directory, '/\\').\DIRECTORY_SEPARATOR.(null === $name ? $this->getBasename() : $this->getName($name));
+
+ return new self($target, false);
+ }
+
+ /**
+ * Returns locale independent base name of the given path.
+ *
+ * @return string
+ */
+ protected function getName(string $name)
+ {
+ $originalName = str_replace('\\', '/', $name);
+ $pos = strrpos($originalName, '/');
+ $originalName = false === $pos ? $originalName : substr($originalName, $pos + 1);
+
+ return $originalName;
+ }
+}
diff --git a/symfony/http-foundation/File/Stream.php b/symfony/http-foundation/File/Stream.php
new file mode 100644
index 00000000..cef3e039
--- /dev/null
+++ b/symfony/http-foundation/File/Stream.php
@@ -0,0 +1,31 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\File;
+
+/**
+ * A PHP stream of unknown size.
+ *
+ * @author Nicolas Grekas <p@tchwork.com>
+ */
+class Stream extends File
+{
+ /**
+ * {@inheritdoc}
+ *
+ * @return int|false
+ */
+ #[\ReturnTypeWillChange]
+ public function getSize()
+ {
+ return false;
+ }
+}
diff --git a/symfony/http-foundation/File/UploadedFile.php b/symfony/http-foundation/File/UploadedFile.php
new file mode 100644
index 00000000..cf50a02c
--- /dev/null
+++ b/symfony/http-foundation/File/UploadedFile.php
@@ -0,0 +1,287 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\File;
+
+use Symfony\Component\HttpFoundation\File\Exception\CannotWriteFileException;
+use Symfony\Component\HttpFoundation\File\Exception\ExtensionFileException;
+use Symfony\Component\HttpFoundation\File\Exception\FileException;
+use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
+use Symfony\Component\HttpFoundation\File\Exception\FormSizeFileException;
+use Symfony\Component\HttpFoundation\File\Exception\IniSizeFileException;
+use Symfony\Component\HttpFoundation\File\Exception\NoFileException;
+use Symfony\Component\HttpFoundation\File\Exception\NoTmpDirFileException;
+use Symfony\Component\HttpFoundation\File\Exception\PartialFileException;
+use Symfony\Component\Mime\MimeTypes;
+
+/**
+ * A file uploaded through a form.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ * @author Florian Eckerstorfer <florian@eckerstorfer.org>
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class UploadedFile extends File
+{
+ private $test;
+ private $originalName;
+ private $mimeType;
+ private $error;
+
+ /**
+ * Accepts the information of the uploaded file as provided by the PHP global $_FILES.
+ *
+ * The file object is only created when the uploaded file is valid (i.e. when the
+ * isValid() method returns true). Otherwise the only methods that could be called
+ * on an UploadedFile instance are:
+ *
+ * * getClientOriginalName,
+ * * getClientMimeType,
+ * * isValid,
+ * * getError.
+ *
+ * Calling any other method on an non-valid instance will cause an unpredictable result.
+ *
+ * @param string $path The full temporary path to the file
+ * @param string $originalName The original file name of the uploaded file
+ * @param string|null $mimeType The type of the file as provided by PHP; null defaults to application/octet-stream
+ * @param int|null $error The error constant of the upload (one of PHP's UPLOAD_ERR_XXX constants); null defaults to UPLOAD_ERR_OK
+ * @param bool $test Whether the test mode is active
+ * Local files are used in test mode hence the code should not enforce HTTP uploads
+ *
+ * @throws FileException If file_uploads is disabled
+ * @throws FileNotFoundException If the file does not exist
+ */
+ public function __construct(string $path, string $originalName, string $mimeType = null, int $error = null, bool $test = false)
+ {
+ $this->originalName = $this->getName($originalName);
+ $this->mimeType = $mimeType ?: 'application/octet-stream';
+ $this->error = $error ?: \UPLOAD_ERR_OK;
+ $this->test = $test;
+
+ parent::__construct($path, \UPLOAD_ERR_OK === $this->error);
+ }
+
+ /**
+ * Returns the original file name.
+ *
+ * It is extracted from the request from which the file has been uploaded.
+ * Then it should not be considered as a safe value.
+ *
+ * @return string
+ */
+ public function getClientOriginalName()
+ {
+ return $this->originalName;
+ }
+
+ /**
+ * Returns the original file extension.
+ *
+ * It is extracted from the original file name that was uploaded.
+ * Then it should not be considered as a safe value.
+ *
+ * @return string
+ */
+ public function getClientOriginalExtension()
+ {
+ return pathinfo($this->originalName, \PATHINFO_EXTENSION);
+ }
+
+ /**
+ * Returns the file mime type.
+ *
+ * The client mime type is extracted from the request from which the file
+ * was uploaded, so it should not be considered as a safe value.
+ *
+ * For a trusted mime type, use getMimeType() instead (which guesses the mime
+ * type based on the file content).
+ *
+ * @return string
+ *
+ * @see getMimeType()
+ */
+ public function getClientMimeType()
+ {
+ return $this->mimeType;
+ }
+
+ /**
+ * Returns the extension based on the client mime type.
+ *
+ * If the mime type is unknown, returns null.
+ *
+ * This method uses the mime type as guessed by getClientMimeType()
+ * to guess the file extension. As such, the extension returned
+ * by this method cannot be trusted.
+ *
+ * For a trusted extension, use guessExtension() instead (which guesses
+ * the extension based on the guessed mime type for the file).
+ *
+ * @return string|null
+ *
+ * @see guessExtension()
+ * @see getClientMimeType()
+ */
+ public function guessClientExtension()
+ {
+ if (!class_exists(MimeTypes::class)) {
+ throw new \LogicException('You cannot guess the extension as the Mime component is not installed. Try running "composer require symfony/mime".');
+ }
+
+ return MimeTypes::getDefault()->getExtensions($this->getClientMimeType())[0] ?? null;
+ }
+
+ /**
+ * Returns the upload error.
+ *
+ * If the upload was successful, the constant UPLOAD_ERR_OK is returned.
+ * Otherwise one of the other UPLOAD_ERR_XXX constants is returned.
+ *
+ * @return int
+ */
+ public function getError()
+ {
+ return $this->error;
+ }
+
+ /**
+ * Returns whether the file has been uploaded with HTTP and no error occurred.
+ *
+ * @return bool
+ */
+ public function isValid()
+ {
+ $isOk = \UPLOAD_ERR_OK === $this->error;
+
+ return $this->test ? $isOk : $isOk && is_uploaded_file($this->getPathname());
+ }
+
+ /**
+ * Moves the file to a new location.
+ *
+ * @return File
+ *
+ * @throws FileException if, for any reason, the file could not have been moved
+ */
+ public function move(string $directory, string $name = null)
+ {
+ if ($this->isValid()) {
+ if ($this->test) {
+ return parent::move($directory, $name);
+ }
+
+ $target = $this->getTargetFile($directory, $name);
+
+ set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; });
+ try {
+ $moved = move_uploaded_file($this->getPathname(), $target);
+ } finally {
+ restore_error_handler();
+ }
+ if (!$moved) {
+ throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s).', $this->getPathname(), $target, strip_tags($error)));
+ }
+
+ @chmod($target, 0666 & ~umask());
+
+ return $target;
+ }
+
+ switch ($this->error) {
+ case \UPLOAD_ERR_INI_SIZE:
+ throw new IniSizeFileException($this->getErrorMessage());
+ case \UPLOAD_ERR_FORM_SIZE:
+ throw new FormSizeFileException($this->getErrorMessage());
+ case \UPLOAD_ERR_PARTIAL:
+ throw new PartialFileException($this->getErrorMessage());
+ case \UPLOAD_ERR_NO_FILE:
+ throw new NoFileException($this->getErrorMessage());
+ case \UPLOAD_ERR_CANT_WRITE:
+ throw new CannotWriteFileException($this->getErrorMessage());
+ case \UPLOAD_ERR_NO_TMP_DIR:
+ throw new NoTmpDirFileException($this->getErrorMessage());
+ case \UPLOAD_ERR_EXTENSION:
+ throw new ExtensionFileException($this->getErrorMessage());
+ }
+
+ throw new FileException($this->getErrorMessage());
+ }
+
+ /**
+ * Returns the maximum size of an uploaded file as configured in php.ini.
+ *
+ * @return int|float The maximum size of an uploaded file in bytes (returns float if size > PHP_INT_MAX)
+ */
+ public static function getMaxFilesize()
+ {
+ $sizePostMax = self::parseFilesize(ini_get('post_max_size'));
+ $sizeUploadMax = self::parseFilesize(ini_get('upload_max_filesize'));
+
+ return min($sizePostMax ?: \PHP_INT_MAX, $sizeUploadMax ?: \PHP_INT_MAX);
+ }
+
+ /**
+ * Returns the given size from an ini value in bytes.
+ *
+ * @return int|float Returns float if size > PHP_INT_MAX
+ */
+ private static function parseFilesize(string $size)
+ {
+ if ('' === $size) {
+ return 0;
+ }
+
+ $size = strtolower($size);
+
+ $max = ltrim($size, '+');
+ if (str_starts_with($max, '0x')) {
+ $max = \intval($max, 16);
+ } elseif (str_starts_with($max, '0')) {
+ $max = \intval($max, 8);
+ } else {
+ $max = (int) $max;
+ }
+
+ switch (substr($size, -1)) {
+ case 't': $max *= 1024;
+ case 'g': $max *= 1024;
+ case 'm': $max *= 1024;
+ case 'k': $max *= 1024;
+ }
+
+ return $max;
+ }
+
+ /**
+ * Returns an informative upload error message.
+ *
+ * @return string
+ */
+ public function getErrorMessage()
+ {
+ static $errors = [
+ \UPLOAD_ERR_INI_SIZE => 'The file "%s" exceeds your upload_max_filesize ini directive (limit is %d KiB).',
+ \UPLOAD_ERR_FORM_SIZE => 'The file "%s" exceeds the upload limit defined in your form.',
+ \UPLOAD_ERR_PARTIAL => 'The file "%s" was only partially uploaded.',
+ \UPLOAD_ERR_NO_FILE => 'No file was uploaded.',
+ \UPLOAD_ERR_CANT_WRITE => 'The file "%s" could not be written on disk.',
+ \UPLOAD_ERR_NO_TMP_DIR => 'File could not be uploaded: missing temporary directory.',
+ \UPLOAD_ERR_EXTENSION => 'File upload was stopped by a PHP extension.',
+ ];
+
+ $errorCode = $this->error;
+ $maxFilesize = \UPLOAD_ERR_INI_SIZE === $errorCode ? self::getMaxFilesize() / 1024 : 0;
+ $message = $errors[$errorCode] ?? 'The file "%s" was not uploaded due to an unknown error.';
+
+ return sprintf($message, $this->getClientOriginalName(), $maxFilesize);
+ }
+}
diff --git a/symfony/http-foundation/FileBag.php b/symfony/http-foundation/FileBag.php
new file mode 100644
index 00000000..ff5ab777
--- /dev/null
+++ b/symfony/http-foundation/FileBag.php
@@ -0,0 +1,140 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation;
+
+use Symfony\Component\HttpFoundation\File\UploadedFile;
+
+/**
+ * FileBag is a container for uploaded files.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Bulat Shakirzyanov <mallluhuct@gmail.com>
+ */
+class FileBag extends ParameterBag
+{
+ private const FILE_KEYS = ['error', 'name', 'size', 'tmp_name', 'type'];
+
+ /**
+ * @param array|UploadedFile[] $parameters An array of HTTP files
+ */
+ public function __construct(array $parameters = [])
+ {
+ $this->replace($parameters);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function replace(array $files = [])
+ {
+ $this->parameters = [];
+ $this->add($files);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set(string $key, $value)
+ {
+ if (!\is_array($value) && !$value instanceof UploadedFile) {
+ throw new \InvalidArgumentException('An uploaded file must be an array or an instance of UploadedFile.');
+ }
+
+ parent::set($key, $this->convertFileInformation($value));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function add(array $files = [])
+ {
+ foreach ($files as $key => $file) {
+ $this->set($key, $file);
+ }
+ }
+
+ /**
+ * Converts uploaded files to UploadedFile instances.
+ *
+ * @param array|UploadedFile $file A (multi-dimensional) array of uploaded file information
+ *
+ * @return UploadedFile[]|UploadedFile|null
+ */
+ protected function convertFileInformation($file)
+ {
+ if ($file instanceof UploadedFile) {
+ return $file;
+ }
+
+ $file = $this->fixPhpFilesArray($file);
+ $keys = array_keys($file);
+ sort($keys);
+
+ if (self::FILE_KEYS == $keys) {
+ if (\UPLOAD_ERR_NO_FILE == $file['error']) {
+ $file = null;
+ } else {
+ $file = new UploadedFile($file['tmp_name'], $file['name'], $file['type'], $file['error'], false);
+ }
+ } else {
+ $file = array_map(function ($v) { return $v instanceof UploadedFile || \is_array($v) ? $this->convertFileInformation($v) : $v; }, $file);
+ if (array_keys($keys) === $keys) {
+ $file = array_filter($file);
+ }
+ }
+
+ return $file;
+ }
+
+ /**
+ * Fixes a malformed PHP $_FILES array.
+ *
+ * PHP has a bug that the format of the $_FILES array differs, depending on
+ * whether the uploaded file fields had normal field names or array-like
+ * field names ("normal" vs. "parent[child]").
+ *
+ * This method fixes the array to look like the "normal" $_FILES array.
+ *
+ * It's safe to pass an already converted array, in which case this method
+ * just returns the original array unmodified.
+ *
+ * @return array
+ */
+ protected function fixPhpFilesArray(array $data)
+ {
+ // Remove extra key added by PHP 8.1.
+ unset($data['full_path']);
+ $keys = array_keys($data);
+ sort($keys);
+
+ if (self::FILE_KEYS != $keys || !isset($data['name']) || !\is_array($data['name'])) {
+ return $data;
+ }
+
+ $files = $data;
+ foreach (self::FILE_KEYS as $k) {
+ unset($files[$k]);
+ }
+
+ foreach ($data['name'] as $key => $name) {
+ $files[$key] = $this->fixPhpFilesArray([
+ 'error' => $data['error'][$key],
+ 'name' => $name,
+ 'type' => $data['type'][$key],
+ 'tmp_name' => $data['tmp_name'][$key],
+ 'size' => $data['size'][$key],
+ ]);
+ }
+
+ return $files;
+ }
+}
diff --git a/symfony/http-foundation/HeaderBag.php b/symfony/http-foundation/HeaderBag.php
new file mode 100644
index 00000000..4683a684
--- /dev/null
+++ b/symfony/http-foundation/HeaderBag.php
@@ -0,0 +1,295 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation;
+
+/**
+ * HeaderBag is a container for HTTP headers.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @implements \IteratorAggregate<string, list<string|null>>
+ */
+class HeaderBag implements \IteratorAggregate, \Countable
+{
+ protected const UPPER = '_ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+ protected const LOWER = '-abcdefghijklmnopqrstuvwxyz';
+
+ /**
+ * @var array<string, list<string|null>>
+ */
+ protected $headers = [];
+ protected $cacheControl = [];
+
+ public function __construct(array $headers = [])
+ {
+ foreach ($headers as $key => $values) {
+ $this->set($key, $values);
+ }
+ }
+
+ /**
+ * Returns the headers as a string.
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ if (!$headers = $this->all()) {
+ return '';
+ }
+
+ ksort($headers);
+ $max = max(array_map('strlen', array_keys($headers))) + 1;
+ $content = '';
+ foreach ($headers as $name => $values) {
+ $name = ucwords($name, '-');
+ foreach ($values as $value) {
+ $content .= sprintf("%-{$max}s %s\r\n", $name.':', $value);
+ }
+ }
+
+ return $content;
+ }
+
+ /**
+ * Returns the headers.
+ *
+ * @param string|null $key The name of the headers to return or null to get them all
+ *
+ * @return array<string, array<int, string|null>>|array<int, string|null>
+ */
+ public function all(string $key = null)
+ {
+ if (null !== $key) {
+ return $this->headers[strtr($key, self::UPPER, self::LOWER)] ?? [];
+ }
+
+ return $this->headers;
+ }
+
+ /**
+ * Returns the parameter keys.
+ *
+ * @return string[]
+ */
+ public function keys()
+ {
+ return array_keys($this->all());
+ }
+
+ /**
+ * Replaces the current HTTP headers by a new set.
+ */
+ public function replace(array $headers = [])
+ {
+ $this->headers = [];
+ $this->add($headers);
+ }
+
+ /**
+ * Adds new headers the current HTTP headers set.
+ */
+ public function add(array $headers)
+ {
+ foreach ($headers as $key => $values) {
+ $this->set($key, $values);
+ }
+ }
+
+ /**
+ * Returns the first header by name or the default one.
+ *
+ * @return string|null
+ */
+ public function get(string $key, string $default = null)
+ {
+ $headers = $this->all($key);
+
+ if (!$headers) {
+ return $default;
+ }
+
+ if (null === $headers[0]) {
+ return null;
+ }
+
+ return (string) $headers[0];
+ }
+
+ /**
+ * Sets a header by name.
+ *
+ * @param string|string[]|null $values The value or an array of values
+ * @param bool $replace Whether to replace the actual value or not (true by default)
+ */
+ public function set(string $key, $values, bool $replace = true)
+ {
+ $key = strtr($key, self::UPPER, self::LOWER);
+
+ if (\is_array($values)) {
+ $values = array_values($values);
+
+ if (true === $replace || !isset($this->headers[$key])) {
+ $this->headers[$key] = $values;
+ } else {
+ $this->headers[$key] = array_merge($this->headers[$key], $values);
+ }
+ } else {
+ if (true === $replace || !isset($this->headers[$key])) {
+ $this->headers[$key] = [$values];
+ } else {
+ $this->headers[$key][] = $values;
+ }
+ }
+
+ if ('cache-control' === $key) {
+ $this->cacheControl = $this->parseCacheControl(implode(', ', $this->headers[$key]));
+ }
+ }
+
+ /**
+ * Returns true if the HTTP header is defined.
+ *
+ * @return bool
+ */
+ public function has(string $key)
+ {
+ return \array_key_exists(strtr($key, self::UPPER, self::LOWER), $this->all());
+ }
+
+ /**
+ * Returns true if the given HTTP header contains the given value.
+ *
+ * @return bool
+ */
+ public function contains(string $key, string $value)
+ {
+ return \in_array($value, $this->all($key));
+ }
+
+ /**
+ * Removes a header.
+ */
+ public function remove(string $key)
+ {
+ $key = strtr($key, self::UPPER, self::LOWER);
+
+ unset($this->headers[$key]);
+
+ if ('cache-control' === $key) {
+ $this->cacheControl = [];
+ }
+ }
+
+ /**
+ * Returns the HTTP header value converted to a date.
+ *
+ * @return \DateTimeInterface|null
+ *
+ * @throws \RuntimeException When the HTTP header is not parseable
+ */
+ public function getDate(string $key, \DateTime $default = null)
+ {
+ if (null === $value = $this->get($key)) {
+ return $default;
+ }
+
+ if (false === $date = \DateTime::createFromFormat(\DATE_RFC2822, $value)) {
+ throw new \RuntimeException(sprintf('The "%s" HTTP header is not parseable (%s).', $key, $value));
+ }
+
+ return $date;
+ }
+
+ /**
+ * Adds a custom Cache-Control directive.
+ *
+ * @param bool|string $value The Cache-Control directive value
+ */
+ public function addCacheControlDirective(string $key, $value = true)
+ {
+ $this->cacheControl[$key] = $value;
+
+ $this->set('Cache-Control', $this->getCacheControlHeader());
+ }
+
+ /**
+ * Returns true if the Cache-Control directive is defined.
+ *
+ * @return bool
+ */
+ public function hasCacheControlDirective(string $key)
+ {
+ return \array_key_exists($key, $this->cacheControl);
+ }
+
+ /**
+ * Returns a Cache-Control directive value by name.
+ *
+ * @return bool|string|null
+ */
+ public function getCacheControlDirective(string $key)
+ {
+ return $this->cacheControl[$key] ?? null;
+ }
+
+ /**
+ * Removes a Cache-Control directive.
+ */
+ public function removeCacheControlDirective(string $key)
+ {
+ unset($this->cacheControl[$key]);
+
+ $this->set('Cache-Control', $this->getCacheControlHeader());
+ }
+
+ /**
+ * Returns an iterator for headers.
+ *
+ * @return \ArrayIterator<string, list<string|null>>
+ */
+ #[\ReturnTypeWillChange]
+ public function getIterator()
+ {
+ return new \ArrayIterator($this->headers);
+ }
+
+ /**
+ * Returns the number of headers.
+ *
+ * @return int
+ */
+ #[\ReturnTypeWillChange]
+ public function count()
+ {
+ return \count($this->headers);
+ }
+
+ protected function getCacheControlHeader()
+ {
+ ksort($this->cacheControl);
+
+ return HeaderUtils::toString($this->cacheControl, ',');
+ }
+
+ /**
+ * Parses a Cache-Control HTTP header.
+ *
+ * @return array
+ */
+ protected function parseCacheControl(string $header)
+ {
+ $parts = HeaderUtils::split($header, ',=');
+
+ return HeaderUtils::combine($parts);
+ }
+}
diff --git a/symfony/http-foundation/HeaderUtils.php b/symfony/http-foundation/HeaderUtils.php
new file mode 100644
index 00000000..1d56be08
--- /dev/null
+++ b/symfony/http-foundation/HeaderUtils.php
@@ -0,0 +1,293 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation;
+
+/**
+ * HTTP header utility functions.
+ *
+ * @author Christian Schmidt <github@chsc.dk>
+ */
+class HeaderUtils
+{
+ public const DISPOSITION_ATTACHMENT = 'attachment';
+ public const DISPOSITION_INLINE = 'inline';
+
+ /**
+ * This class should not be instantiated.
+ */
+ private function __construct()
+ {
+ }
+
+ /**
+ * Splits an HTTP header by one or more separators.
+ *
+ * Example:
+ *
+ * HeaderUtils::split("da, en-gb;q=0.8", ",;")
+ * // => ['da'], ['en-gb', 'q=0.8']]
+ *
+ * @param string $separators List of characters to split on, ordered by
+ * precedence, e.g. ",", ";=", or ",;="
+ *
+ * @return array Nested array with as many levels as there are characters in
+ * $separators
+ */
+ public static function split(string $header, string $separators): array
+ {
+ $quotedSeparators = preg_quote($separators, '/');
+
+ preg_match_all('
+ /
+ (?!\s)
+ (?:
+ # quoted-string
+ "(?:[^"\\\\]|\\\\.)*(?:"|\\\\|$)
+ |
+ # token
+ [^"'.$quotedSeparators.']+
+ )+
+ (?<!\s)
+ |
+ # separator
+ \s*
+ (?<separator>['.$quotedSeparators.'])
+ \s*
+ /x', trim($header), $matches, \PREG_SET_ORDER);
+
+ return self::groupParts($matches, $separators);
+ }
+
+ /**
+ * Combines an array of arrays into one associative array.
+ *
+ * Each of the nested arrays should have one or two elements. The first
+ * value will be used as the keys in the associative array, and the second
+ * will be used as the values, or true if the nested array only contains one
+ * element. Array keys are lowercased.
+ *
+ * Example:
+ *
+ * HeaderUtils::combine([["foo", "abc"], ["bar"]])
+ * // => ["foo" => "abc", "bar" => true]
+ */
+ public static function combine(array $parts): array
+ {
+ $assoc = [];
+ foreach ($parts as $part) {
+ $name = strtolower($part[0]);
+ $value = $part[1] ?? true;
+ $assoc[$name] = $value;
+ }
+
+ return $assoc;
+ }
+
+ /**
+ * Joins an associative array into a string for use in an HTTP header.
+ *
+ * The key and value of each entry are joined with "=", and all entries
+ * are joined with the specified separator and an additional space (for
+ * readability). Values are quoted if necessary.
+ *
+ * Example:
+ *
+ * HeaderUtils::toString(["foo" => "abc", "bar" => true, "baz" => "a b c"], ",")
+ * // => 'foo=abc, bar, baz="a b c"'
+ */
+ public static function toString(array $assoc, string $separator): string
+ {
+ $parts = [];
+ foreach ($assoc as $name => $value) {
+ if (true === $value) {
+ $parts[] = $name;
+ } else {
+ $parts[] = $name.'='.self::quote($value);
+ }
+ }
+
+ return implode($separator.' ', $parts);
+ }
+
+ /**
+ * Encodes a string as a quoted string, if necessary.
+ *
+ * If a string contains characters not allowed by the "token" construct in
+ * the HTTP specification, it is backslash-escaped and enclosed in quotes
+ * to match the "quoted-string" construct.
+ */
+ public static function quote(string $s): string
+ {
+ if (preg_match('/^[a-z0-9!#$%&\'*.^_`|~-]+$/i', $s)) {
+ return $s;
+ }
+
+ return '"'.addcslashes($s, '"\\"').'"';
+ }
+
+ /**
+ * Decodes a quoted string.
+ *
+ * If passed an unquoted string that matches the "token" construct (as
+ * defined in the HTTP specification), it is passed through verbatimly.
+ */
+ public static function unquote(string $s): string
+ {
+ return preg_replace('/\\\\(.)|"/', '$1', $s);
+ }
+
+ /**
+ * Generates an HTTP Content-Disposition field-value.
+ *
+ * @param string $disposition One of "inline" or "attachment"
+ * @param string $filename A unicode string
+ * @param string $filenameFallback A string containing only ASCII characters that
+ * is semantically equivalent to $filename. If the filename is already ASCII,
+ * it can be omitted, or just copied from $filename
+ *
+ * @throws \InvalidArgumentException
+ *
+ * @see RFC 6266
+ */
+ public static function makeDisposition(string $disposition, string $filename, string $filenameFallback = ''): string
+ {
+ if (!\in_array($disposition, [self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE])) {
+ throw new \InvalidArgumentException(sprintf('The disposition must be either "%s" or "%s".', self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE));
+ }
+
+ if ('' === $filenameFallback) {
+ $filenameFallback = $filename;
+ }
+
+ // filenameFallback is not ASCII.
+ if (!preg_match('/^[\x20-\x7e]*$/', $filenameFallback)) {
+ throw new \InvalidArgumentException('The filename fallback must only contain ASCII characters.');
+ }
+
+ // percent characters aren't safe in fallback.
+ if (str_contains($filenameFallback, '%')) {
+ throw new \InvalidArgumentException('The filename fallback cannot contain the "%" character.');
+ }
+
+ // path separators aren't allowed in either.
+ if (str_contains($filename, '/') || str_contains($filename, '\\') || str_contains($filenameFallback, '/') || str_contains($filenameFallback, '\\')) {
+ throw new \InvalidArgumentException('The filename and the fallback cannot contain the "/" and "\\" characters.');
+ }
+
+ $params = ['filename' => $filenameFallback];
+ if ($filename !== $filenameFallback) {
+ $params['filename*'] = "utf-8''".rawurlencode($filename);
+ }
+
+ return $disposition.'; '.self::toString($params, ';');
+ }
+
+ /**
+ * Like parse_str(), but preserves dots in variable names.
+ */
+ public static function parseQuery(string $query, bool $ignoreBrackets = false, string $separator = '&'): array
+ {
+ $q = [];
+
+ foreach (explode($separator, $query) as $v) {
+ if (false !== $i = strpos($v, "\0")) {
+ $v = substr($v, 0, $i);
+ }
+
+ if (false === $i = strpos($v, '=')) {
+ $k = urldecode($v);
+ $v = '';
+ } else {
+ $k = urldecode(substr($v, 0, $i));
+ $v = substr($v, $i);
+ }
+
+ if (false !== $i = strpos($k, "\0")) {
+ $k = substr($k, 0, $i);
+ }
+
+ $k = ltrim($k, ' ');
+
+ if ($ignoreBrackets) {
+ $q[$k][] = urldecode(substr($v, 1));
+
+ continue;
+ }
+
+ if (false === $i = strpos($k, '[')) {
+ $q[] = bin2hex($k).$v;
+ } else {
+ $q[] = bin2hex(substr($k, 0, $i)).rawurlencode(substr($k, $i)).$v;
+ }
+ }
+
+ if ($ignoreBrackets) {
+ return $q;
+ }
+
+ parse_str(implode('&', $q), $q);
+
+ $query = [];
+
+ foreach ($q as $k => $v) {
+ if (false !== $i = strpos($k, '_')) {
+ $query[substr_replace($k, hex2bin(substr($k, 0, $i)).'[', 0, 1 + $i)] = $v;
+ } else {
+ $query[hex2bin($k)] = $v;
+ }
+ }
+
+ return $query;
+ }
+
+ private static function groupParts(array $matches, string $separators, bool $first = true): array
+ {
+ $separator = $separators[0];
+ $partSeparators = substr($separators, 1);
+
+ $i = 0;
+ $partMatches = [];
+ $previousMatchWasSeparator = false;
+ foreach ($matches as $match) {
+ if (!$first && $previousMatchWasSeparator && isset($match['separator']) && $match['separator'] === $separator) {
+ $previousMatchWasSeparator = true;
+ $partMatches[$i][] = $match;
+ } elseif (isset($match['separator']) && $match['separator'] === $separator) {
+ $previousMatchWasSeparator = true;
+ ++$i;
+ } else {
+ $previousMatchWasSeparator = false;
+ $partMatches[$i][] = $match;
+ }
+ }
+
+ $parts = [];
+ if ($partSeparators) {
+ foreach ($partMatches as $matches) {
+ $parts[] = self::groupParts($matches, $partSeparators, false);
+ }
+ } else {
+ foreach ($partMatches as $matches) {
+ $parts[] = self::unquote($matches[0][0]);
+ }
+
+ if (!$first && 2 < \count($parts)) {
+ $parts = [
+ $parts[0],
+ implode($separator, \array_slice($parts, 1)),
+ ];
+ }
+ }
+
+ return $parts;
+ }
+}
diff --git a/symfony/http-foundation/InputBag.php b/symfony/http-foundation/InputBag.php
new file mode 100644
index 00000000..b36001d8
--- /dev/null
+++ b/symfony/http-foundation/InputBag.php
@@ -0,0 +1,113 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation;
+
+use Symfony\Component\HttpFoundation\Exception\BadRequestException;
+
+/**
+ * InputBag is a container for user input values such as $_GET, $_POST, $_REQUEST, and $_COOKIE.
+ *
+ * @author Saif Eddin Gmati <azjezz@protonmail.com>
+ */
+final class InputBag extends ParameterBag
+{
+ /**
+ * Returns a scalar input value by name.
+ *
+ * @param string|int|float|bool|null $default The default value if the input key does not exist
+ *
+ * @return string|int|float|bool|null
+ */
+ public function get(string $key, $default = null)
+ {
+ if (null !== $default && !is_scalar($default) && !(\is_object($default) && method_exists($default, '__toString'))) {
+ trigger_deprecation('symfony/http-foundation', '5.1', 'Passing a non-scalar value as 2nd argument to "%s()" is deprecated, pass a scalar or null instead.', __METHOD__);
+ }
+
+ $value = parent::get($key, $this);
+
+ if (null !== $value && $this !== $value && !is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) {
+ trigger_deprecation('symfony/http-foundation', '5.1', 'Retrieving a non-string value from "%s()" is deprecated, and will throw a "%s" exception in Symfony 6.0, use "%s::all($key)" instead.', __METHOD__, BadRequestException::class, __CLASS__);
+ }
+
+ return $this === $value ? $default : $value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function all(string $key = null): array
+ {
+ return parent::all($key);
+ }
+
+ /**
+ * Replaces the current input values by a new set.
+ */
+ public function replace(array $inputs = [])
+ {
+ $this->parameters = [];
+ $this->add($inputs);
+ }
+
+ /**
+ * Adds input values.
+ */
+ public function add(array $inputs = [])
+ {
+ foreach ($inputs as $input => $value) {
+ $this->set($input, $value);
+ }
+ }
+
+ /**
+ * Sets an input by name.
+ *
+ * @param string|int|float|bool|array|null $value
+ */
+ public function set(string $key, $value)
+ {
+ if (null !== $value && !is_scalar($value) && !\is_array($value) && !method_exists($value, '__toString')) {
+ trigger_deprecation('symfony/http-foundation', '5.1', 'Passing "%s" as a 2nd Argument to "%s()" is deprecated, pass a scalar, array, or null instead.', get_debug_type($value), __METHOD__);
+ }
+
+ $this->parameters[$key] = $value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function filter(string $key, $default = null, int $filter = \FILTER_DEFAULT, $options = [])
+ {
+ $value = $this->has($key) ? $this->all()[$key] : $default;
+
+ // Always turn $options into an array - this allows filter_var option shortcuts.
+ if (!\is_array($options) && $options) {
+ $options = ['flags' => $options];
+ }
+
+ if (\is_array($value) && !(($options['flags'] ?? 0) & (\FILTER_REQUIRE_ARRAY | \FILTER_FORCE_ARRAY))) {
+ trigger_deprecation('symfony/http-foundation', '5.1', 'Filtering an array value with "%s()" without passing the FILTER_REQUIRE_ARRAY or FILTER_FORCE_ARRAY flag is deprecated', __METHOD__);
+
+ if (!isset($options['flags'])) {
+ $options['flags'] = \FILTER_REQUIRE_ARRAY;
+ }
+ }
+
+ if ((\FILTER_CALLBACK & $filter) && !(($options['options'] ?? null) instanceof \Closure)) {
+ trigger_deprecation('symfony/http-foundation', '5.2', 'Not passing a Closure together with FILTER_CALLBACK to "%s()" is deprecated. Wrap your filter in a closure instead.', __METHOD__);
+ // 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)));
+ }
+
+ return filter_var($value, $filter, $options);
+ }
+}
diff --git a/symfony/http-foundation/IpUtils.php b/symfony/http-foundation/IpUtils.php
new file mode 100644
index 00000000..24111f1e
--- /dev/null
+++ b/symfony/http-foundation/IpUtils.php
@@ -0,0 +1,203 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation;
+
+/**
+ * Http utility functions.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class IpUtils
+{
+ private static $checkedIps = [];
+
+ /**
+ * This class should not be instantiated.
+ */
+ private function __construct()
+ {
+ }
+
+ /**
+ * Checks if an IPv4 or IPv6 address is contained in the list of given IPs or subnets.
+ *
+ * @param string|array $ips List of IPs or subnets (can be a string if only a single one)
+ *
+ * @return bool
+ */
+ public static function checkIp(?string $requestIp, $ips)
+ {
+ if (null === $requestIp) {
+ trigger_deprecation('symfony/http-foundation', '5.4', 'Passing null as $requestIp to "%s()" is deprecated, pass an empty string instead.', __METHOD__);
+
+ return false;
+ }
+
+ if (!\is_array($ips)) {
+ $ips = [$ips];
+ }
+
+ $method = substr_count($requestIp, ':') > 1 ? 'checkIp6' : 'checkIp4';
+
+ foreach ($ips as $ip) {
+ if (self::$method($requestIp, $ip)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Compares two IPv4 addresses.
+ * In case a subnet is given, it checks if it contains the request IP.
+ *
+ * @param string $ip IPv4 address or subnet in CIDR notation
+ *
+ * @return bool Whether the request IP matches the IP, or whether the request IP is within the CIDR subnet
+ */
+ public static function checkIp4(?string $requestIp, string $ip)
+ {
+ if (null === $requestIp) {
+ trigger_deprecation('symfony/http-foundation', '5.4', 'Passing null as $requestIp to "%s()" is deprecated, pass an empty string instead.', __METHOD__);
+
+ return false;
+ }
+
+ $cacheKey = $requestIp.'-'.$ip;
+ if (isset(self::$checkedIps[$cacheKey])) {
+ return self::$checkedIps[$cacheKey];
+ }
+
+ if (!filter_var($requestIp, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)) {
+ return self::$checkedIps[$cacheKey] = false;
+ }
+
+ if (str_contains($ip, '/')) {
+ [$address, $netmask] = explode('/', $ip, 2);
+
+ if ('0' === $netmask) {
+ return self::$checkedIps[$cacheKey] = filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4);
+ }
+
+ if ($netmask < 0 || $netmask > 32) {
+ return self::$checkedIps[$cacheKey] = false;
+ }
+ } else {
+ $address = $ip;
+ $netmask = 32;
+ }
+
+ if (false === ip2long($address)) {
+ return self::$checkedIps[$cacheKey] = false;
+ }
+
+ return self::$checkedIps[$cacheKey] = 0 === substr_compare(sprintf('%032b', ip2long($requestIp)), sprintf('%032b', ip2long($address)), 0, $netmask);
+ }
+
+ /**
+ * Compares two IPv6 addresses.
+ * In case a subnet is given, it checks if it contains the request IP.
+ *
+ * @author David Soria Parra <dsp at php dot net>
+ *
+ * @see https://github.com/dsp/v6tools
+ *
+ * @param string $ip IPv6 address or subnet in CIDR notation
+ *
+ * @return bool
+ *
+ * @throws \RuntimeException When IPV6 support is not enabled
+ */
+ public static function checkIp6(?string $requestIp, string $ip)
+ {
+ if (null === $requestIp) {
+ trigger_deprecation('symfony/http-foundation', '5.4', 'Passing null as $requestIp to "%s()" is deprecated, pass an empty string instead.', __METHOD__);
+
+ return false;
+ }
+
+ $cacheKey = $requestIp.'-'.$ip;
+ if (isset(self::$checkedIps[$cacheKey])) {
+ return self::$checkedIps[$cacheKey];
+ }
+
+ if (!((\extension_loaded('sockets') && \defined('AF_INET6')) || @inet_pton('::1'))) {
+ throw new \RuntimeException('Unable to check Ipv6. Check that PHP was not compiled with option "disable-ipv6".');
+ }
+
+ if (str_contains($ip, '/')) {
+ [$address, $netmask] = explode('/', $ip, 2);
+
+ if ('0' === $netmask) {
+ return (bool) unpack('n*', @inet_pton($address));
+ }
+
+ if ($netmask < 1 || $netmask > 128) {
+ return self::$checkedIps[$cacheKey] = false;
+ }
+ } else {
+ $address = $ip;
+ $netmask = 128;
+ }
+
+ $bytesAddr = unpack('n*', @inet_pton($address));
+ $bytesTest = unpack('n*', @inet_pton($requestIp));
+
+ if (!$bytesAddr || !$bytesTest) {
+ return self::$checkedIps[$cacheKey] = false;
+ }
+
+ for ($i = 1, $ceil = ceil($netmask / 16); $i <= $ceil; ++$i) {
+ $left = $netmask - 16 * ($i - 1);
+ $left = ($left <= 16) ? $left : 16;
+ $mask = ~(0xFFFF >> $left) & 0xFFFF;
+ if (($bytesAddr[$i] & $mask) != ($bytesTest[$i] & $mask)) {
+ return self::$checkedIps[$cacheKey] = false;
+ }
+ }
+
+ return self::$checkedIps[$cacheKey] = true;
+ }
+
+ /**
+ * Anonymizes an IP/IPv6.
+ *
+ * Removes the last byte for v4 and the last 8 bytes for v6 IPs
+ */
+ public static function anonymize(string $ip): string
+ {
+ $wrappedIPv6 = false;
+ if ('[' === substr($ip, 0, 1) && ']' === substr($ip, -1, 1)) {
+ $wrappedIPv6 = true;
+ $ip = substr($ip, 1, -1);
+ }
+
+ $packedAddress = inet_pton($ip);
+ if (4 === \strlen($packedAddress)) {
+ $mask = '255.255.255.0';
+ } elseif ($ip === inet_ntop($packedAddress & inet_pton('::ffff:ffff:ffff'))) {
+ $mask = '::ffff:ffff:ff00';
+ } elseif ($ip === inet_ntop($packedAddress & inet_pton('::ffff:ffff'))) {
+ $mask = '::ffff:ff00';
+ } else {
+ $mask = 'ffff:ffff:ffff:ffff:0000:0000:0000:0000';
+ }
+ $ip = inet_ntop($packedAddress & inet_pton($mask));
+
+ if ($wrappedIPv6) {
+ $ip = '['.$ip.']';
+ }
+
+ return $ip;
+ }
+}
diff --git a/symfony/http-foundation/JsonResponse.php b/symfony/http-foundation/JsonResponse.php
new file mode 100644
index 00000000..501a6387
--- /dev/null
+++ b/symfony/http-foundation/JsonResponse.php
@@ -0,0 +1,221 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation;
+
+/**
+ * Response represents an HTTP response in JSON format.
+ *
+ * Note that this class does not force the returned JSON content to be an
+ * object. It is however recommended that you do return an object as it
+ * protects yourself against XSSI and JSON-JavaScript Hijacking.
+ *
+ * @see https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/AJAX_Security_Cheat_Sheet.md#always-return-json-with-an-object-on-the-outside
+ *
+ * @author Igor Wiedler <igor@wiedler.ch>
+ */
+class JsonResponse extends Response
+{
+ protected $data;
+ protected $callback;
+
+ // Encode <, >, ', &, and " characters in the JSON, making it also safe to be embedded into HTML.
+ // 15 === JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT
+ public const DEFAULT_ENCODING_OPTIONS = 15;
+
+ protected $encodingOptions = self::DEFAULT_ENCODING_OPTIONS;
+
+ /**
+ * @param mixed $data The response data
+ * @param int $status The response status code
+ * @param array $headers An array of response headers
+ * @param bool $json If the data is already a JSON string
+ */
+ public function __construct($data = null, int $status = 200, array $headers = [], bool $json = false)
+ {
+ parent::__construct('', $status, $headers);
+
+ if ($json && !\is_string($data) && !is_numeric($data) && !\is_callable([$data, '__toString'])) {
+ 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)));
+ }
+
+ if (null === $data) {
+ $data = new \ArrayObject();
+ }
+
+ $json ? $this->setJson($data) : $this->setData($data);
+ }
+
+ /**
+ * Factory method for chainability.
+ *
+ * Example:
+ *
+ * return JsonResponse::create(['key' => 'value'])
+ * ->setSharedMaxAge(300);
+ *
+ * @param mixed $data The JSON response data
+ * @param int $status The response status code
+ * @param array $headers An array of response headers
+ *
+ * @return static
+ *
+ * @deprecated since Symfony 5.1, use __construct() instead.
+ */
+ public static function create($data = null, int $status = 200, array $headers = [])
+ {
+ trigger_deprecation('symfony/http-foundation', '5.1', 'The "%s()" method is deprecated, use "new %s()" instead.', __METHOD__, static::class);
+
+ return new static($data, $status, $headers);
+ }
+
+ /**
+ * Factory method for chainability.
+ *
+ * Example:
+ *
+ * return JsonResponse::fromJsonString('{"key": "value"}')
+ * ->setSharedMaxAge(300);
+ *
+ * @param string $data The JSON response string
+ * @param int $status The response status code
+ * @param array $headers An array of response headers
+ *
+ * @return static
+ */
+ public static function fromJsonString(string $data, int $status = 200, array $headers = [])
+ {
+ return new static($data, $status, $headers, true);
+ }
+
+ /**
+ * Sets the JSONP callback.
+ *
+ * @param string|null $callback The JSONP callback or null to use none
+ *
+ * @return $this
+ *
+ * @throws \InvalidArgumentException When the callback name is not valid
+ */
+ public function setCallback(string $callback = null)
+ {
+ if (null !== $callback) {
+ // partially taken from https://geekality.net/2011/08/03/valid-javascript-identifier/
+ // partially taken from https://github.com/willdurand/JsonpCallbackValidator
+ // JsonpCallbackValidator is released under the MIT License. See https://github.com/willdurand/JsonpCallbackValidator/blob/v1.1.0/LICENSE for details.
+ // (c) William Durand <william.durand1@gmail.com>
+ $pattern = '/^[$_\p{L}][$_\p{L}\p{Mn}\p{Mc}\p{Nd}\p{Pc}\x{200C}\x{200D}]*(?:\[(?:"(?:\\\.|[^"\\\])*"|\'(?:\\\.|[^\'\\\])*\'|\d+)\])*?$/u';
+ $reserved = [
+ 'break', 'do', 'instanceof', 'typeof', 'case', 'else', 'new', 'var', 'catch', 'finally', 'return', 'void', 'continue', 'for', 'switch', 'while',
+ 'debugger', 'function', 'this', 'with', 'default', 'if', 'throw', 'delete', 'in', 'try', 'class', 'enum', 'extends', 'super', 'const', 'export',
+ 'import', 'implements', 'let', 'private', 'public', 'yield', 'interface', 'package', 'protected', 'static', 'null', 'true', 'false',
+ ];
+ $parts = explode('.', $callback);
+ foreach ($parts as $part) {
+ if (!preg_match($pattern, $part) || \in_array($part, $reserved, true)) {
+ throw new \InvalidArgumentException('The callback name is not valid.');
+ }
+ }
+ }
+
+ $this->callback = $callback;
+
+ return $this->update();
+ }
+
+ /**
+ * Sets a raw string containing a JSON document to be sent.
+ *
+ * @return $this
+ */
+ public function setJson(string $json)
+ {
+ $this->data = $json;
+
+ return $this->update();
+ }
+
+ /**
+ * Sets the data to be sent as JSON.
+ *
+ * @param mixed $data
+ *
+ * @return $this
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function setData($data = [])
+ {
+ try {
+ $data = json_encode($data, $this->encodingOptions);
+ } catch (\Exception $e) {
+ if ('Exception' === \get_class($e) && str_starts_with($e->getMessage(), 'Failed calling ')) {
+ throw $e->getPrevious() ?: $e;
+ }
+ throw $e;
+ }
+
+ if (\PHP_VERSION_ID >= 70300 && (\JSON_THROW_ON_ERROR & $this->encodingOptions)) {
+ return $this->setJson($data);
+ }
+
+ if (\JSON_ERROR_NONE !== json_last_error()) {
+ throw new \InvalidArgumentException(json_last_error_msg());
+ }
+
+ return $this->setJson($data);
+ }
+
+ /**
+ * Returns options used while encoding data to JSON.
+ *
+ * @return int
+ */
+ public function getEncodingOptions()
+ {
+ return $this->encodingOptions;
+ }
+
+ /**
+ * Sets options used while encoding data to JSON.
+ *
+ * @return $this
+ */
+ public function setEncodingOptions(int $encodingOptions)
+ {
+ $this->encodingOptions = $encodingOptions;
+
+ return $this->setData(json_decode($this->data));
+ }
+
+ /**
+ * Updates the content and headers according to the JSON data and callback.
+ *
+ * @return $this
+ */
+ protected function update()
+ {
+ if (null !== $this->callback) {
+ // Not using application/javascript for compatibility reasons with older browsers.
+ $this->headers->set('Content-Type', 'text/javascript');
+
+ return $this->setContent(sprintf('/**/%s(%s);', $this->callback, $this->data));
+ }
+
+ // Only set the header when there is none or when it equals 'text/javascript' (from a previous update with callback)
+ // in order to not overwrite a custom definition.
+ if (!$this->headers->has('Content-Type') || 'text/javascript' === $this->headers->get('Content-Type')) {
+ $this->headers->set('Content-Type', 'application/json');
+ }
+
+ return $this->setContent($this->data);
+ }
+}
diff --git a/symfony/http-foundation/LICENSE b/symfony/http-foundation/LICENSE
new file mode 100644
index 00000000..88bf75bb
--- /dev/null
+++ b/symfony/http-foundation/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2004-2022 Fabien Potencier
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/symfony/http-foundation/ParameterBag.php b/symfony/http-foundation/ParameterBag.php
new file mode 100644
index 00000000..7d051abe
--- /dev/null
+++ b/symfony/http-foundation/ParameterBag.php
@@ -0,0 +1,228 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation;
+
+use Symfony\Component\HttpFoundation\Exception\BadRequestException;
+
+/**
+ * ParameterBag is a container for key/value pairs.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @implements \IteratorAggregate<string, mixed>
+ */
+class ParameterBag implements \IteratorAggregate, \Countable
+{
+ /**
+ * Parameter storage.
+ */
+ protected $parameters;
+
+ public function __construct(array $parameters = [])
+ {
+ $this->parameters = $parameters;
+ }
+
+ /**
+ * Returns the parameters.
+ *
+ * @param string|null $key The name of the parameter to return or null to get them all
+ *
+ * @return array
+ */
+ public function all(/*string $key = null*/)
+ {
+ $key = \func_num_args() > 0 ? func_get_arg(0) : null;
+
+ if (null === $key) {
+ return $this->parameters;
+ }
+
+ if (!\is_array($value = $this->parameters[$key] ?? [])) {
+ throw new BadRequestException(sprintf('Unexpected value for parameter "%s": expecting "array", got "%s".', $key, get_debug_type($value)));
+ }
+
+ return $value;
+ }
+
+ /**
+ * Returns the parameter keys.
+ *
+ * @return array
+ */
+ public function keys()
+ {
+ return array_keys($this->parameters);
+ }
+
+ /**
+ * Replaces the current parameters by a new set.
+ */
+ public function replace(array $parameters = [])
+ {
+ $this->parameters = $parameters;
+ }
+
+ /**
+ * Adds parameters.
+ */
+ public function add(array $parameters = [])
+ {
+ $this->parameters = array_replace($this->parameters, $parameters);
+ }
+
+ /**
+ * Returns a parameter by name.
+ *
+ * @param mixed $default The default value if the parameter key does not exist
+ *
+ * @return mixed
+ */
+ public function get(string $key, $default = null)
+ {
+ return \array_key_exists($key, $this->parameters) ? $this->parameters[$key] : $default;
+ }
+
+ /**
+ * Sets a parameter by name.
+ *
+ * @param mixed $value The value
+ */
+ public function set(string $key, $value)
+ {
+ $this->parameters[$key] = $value;
+ }
+
+ /**
+ * Returns true if the parameter is defined.
+ *
+ * @return bool
+ */
+ public function has(string $key)
+ {
+ return \array_key_exists($key, $this->parameters);
+ }
+
+ /**
+ * Removes a parameter.
+ */
+ public function remove(string $key)
+ {
+ unset($this->parameters[$key]);
+ }
+
+ /**
+ * Returns the alphabetic characters of the parameter value.
+ *
+ * @return string
+ */
+ public function getAlpha(string $key, string $default = '')
+ {
+ return preg_replace('/[^[:alpha:]]/', '', $this->get($key, $default));
+ }
+
+ /**
+ * Returns the alphabetic characters and digits of the parameter value.
+ *
+ * @return string
+ */
+ public function getAlnum(string $key, string $default = '')
+ {
+ return preg_replace('/[^[:alnum:]]/', '', $this->get($key, $default));
+ }
+
+ /**
+ * Returns the digits of the parameter value.
+ *
+ * @return string
+ */
+ public function getDigits(string $key, string $default = '')
+ {
+ // we need to remove - and + because they're allowed in the filter
+ return str_replace(['-', '+'], '', $this->filter($key, $default, \FILTER_SANITIZE_NUMBER_INT));
+ }
+
+ /**
+ * Returns the parameter value converted to integer.
+ *
+ * @return int
+ */
+ public function getInt(string $key, int $default = 0)
+ {
+ return (int) $this->get($key, $default);
+ }
+
+ /**
+ * Returns the parameter value converted to boolean.
+ *
+ * @return bool
+ */
+ public function getBoolean(string $key, bool $default = false)
+ {
+ return $this->filter($key, $default, \FILTER_VALIDATE_BOOLEAN);
+ }
+
+ /**
+ * Filter key.
+ *
+ * @param mixed $default Default = null
+ * @param int $filter FILTER_* constant
+ * @param mixed $options Filter options
+ *
+ * @see https://php.net/filter-var
+ *
+ * @return mixed
+ */
+ public function filter(string $key, $default = null, int $filter = \FILTER_DEFAULT, $options = [])
+ {
+ $value = $this->get($key, $default);
+
+ // Always turn $options into an array - this allows filter_var option shortcuts.
+ if (!\is_array($options) && $options) {
+ $options = ['flags' => $options];
+ }
+
+ // Add a convenience check for arrays.
+ if (\is_array($value) && !isset($options['flags'])) {
+ $options['flags'] = \FILTER_REQUIRE_ARRAY;
+ }
+
+ if ((\FILTER_CALLBACK & $filter) && !(($options['options'] ?? null) instanceof \Closure)) {
+ trigger_deprecation('symfony/http-foundation', '5.2', 'Not passing a Closure together with FILTER_CALLBACK to "%s()" is deprecated. Wrap your filter in a closure instead.', __METHOD__);
+ // 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)));
+ }
+
+ return filter_var($value, $filter, $options);
+ }
+
+ /**
+ * Returns an iterator for parameters.
+ *
+ * @return \ArrayIterator<string, mixed>
+ */
+ #[\ReturnTypeWillChange]
+ public function getIterator()
+ {
+ return new \ArrayIterator($this->parameters);
+ }
+
+ /**
+ * Returns the number of parameters.
+ *
+ * @return int
+ */
+ #[\ReturnTypeWillChange]
+ public function count()
+ {
+ return \count($this->parameters);
+ }
+}
diff --git a/symfony/http-foundation/README.md b/symfony/http-foundation/README.md
new file mode 100644
index 00000000..424f2c4f
--- /dev/null
+++ b/symfony/http-foundation/README.md
@@ -0,0 +1,28 @@
+HttpFoundation Component
+========================
+
+The HttpFoundation component defines an object-oriented layer for the HTTP
+specification.
+
+Sponsor
+-------
+
+The HttpFoundation component for Symfony 5.4/6.0 is [backed][1] by [Laravel][2].
+
+Laravel is a PHP web development framework that is passionate about maximum developer
+happiness. Laravel is built using a variety of bespoke and Symfony based components.
+
+Help Symfony by [sponsoring][3] its development!
+
+Resources
+---------
+
+ * [Documentation](https://symfony.com/doc/current/components/http_foundation.html)
+ * [Contributing](https://symfony.com/doc/current/contributing/index.html)
+ * [Report issues](https://github.com/symfony/symfony/issues) and
+ [send Pull Requests](https://github.com/symfony/symfony/pulls)
+ in the [main Symfony repository](https://github.com/symfony/symfony)
+
+[1]: https://symfony.com/backers
+[2]: https://laravel.com/
+[3]: https://symfony.com/sponsor
diff --git a/symfony/http-foundation/RateLimiter/AbstractRequestRateLimiter.php b/symfony/http-foundation/RateLimiter/AbstractRequestRateLimiter.php
new file mode 100644
index 00000000..c91d614f
--- /dev/null
+++ b/symfony/http-foundation/RateLimiter/AbstractRequestRateLimiter.php
@@ -0,0 +1,57 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\RateLimiter;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\RateLimiter\LimiterInterface;
+use Symfony\Component\RateLimiter\Policy\NoLimiter;
+use Symfony\Component\RateLimiter\RateLimit;
+
+/**
+ * An implementation of RequestRateLimiterInterface that
+ * fits most use-cases.
+ *
+ * @author Wouter de Jong <wouter@wouterj.nl>
+ */
+abstract class AbstractRequestRateLimiter implements RequestRateLimiterInterface
+{
+ public function consume(Request $request): RateLimit
+ {
+ $limiters = $this->getLimiters($request);
+ if (0 === \count($limiters)) {
+ $limiters = [new NoLimiter()];
+ }
+
+ $minimalRateLimit = null;
+ foreach ($limiters as $limiter) {
+ $rateLimit = $limiter->consume(1);
+
+ if (null === $minimalRateLimit || $rateLimit->getRemainingTokens() < $minimalRateLimit->getRemainingTokens()) {
+ $minimalRateLimit = $rateLimit;
+ }
+ }
+
+ return $minimalRateLimit;
+ }
+
+ public function reset(Request $request): void
+ {
+ foreach ($this->getLimiters($request) as $limiter) {
+ $limiter->reset();
+ }
+ }
+
+ /**
+ * @return LimiterInterface[] a set of limiters using keys extracted from the request
+ */
+ abstract protected function getLimiters(Request $request): array;
+}
diff --git a/symfony/http-foundation/RateLimiter/RequestRateLimiterInterface.php b/symfony/http-foundation/RateLimiter/RequestRateLimiterInterface.php
new file mode 100644
index 00000000..4c87a40a
--- /dev/null
+++ b/symfony/http-foundation/RateLimiter/RequestRateLimiterInterface.php
@@ -0,0 +1,30 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\RateLimiter;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\RateLimiter\RateLimit;
+
+/**
+ * A special type of limiter that deals with requests.
+ *
+ * This allows to limit on different types of information
+ * from the requests.
+ *
+ * @author Wouter de Jong <wouter@wouterj.nl>
+ */
+interface RequestRateLimiterInterface
+{
+ public function consume(Request $request): RateLimit;
+
+ public function reset(Request $request): void;
+}
diff --git a/symfony/http-foundation/RedirectResponse.php b/symfony/http-foundation/RedirectResponse.php
new file mode 100644
index 00000000..2103280c
--- /dev/null
+++ b/symfony/http-foundation/RedirectResponse.php
@@ -0,0 +1,109 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation;
+
+/**
+ * RedirectResponse represents an HTTP response doing a redirect.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class RedirectResponse extends Response
+{
+ protected $targetUrl;
+
+ /**
+ * Creates a redirect response so that it conforms to the rules defined for a redirect status code.
+ *
+ * @param string $url The URL to redirect to. The URL should be a full URL, with schema etc.,
+ * but practically every browser redirects on paths only as well
+ * @param int $status The status code (302 by default)
+ * @param array $headers The headers (Location is always set to the given URL)
+ *
+ * @throws \InvalidArgumentException
+ *
+ * @see https://tools.ietf.org/html/rfc2616#section-10.3
+ */
+ public function __construct(string $url, int $status = 302, array $headers = [])
+ {
+ parent::__construct('', $status, $headers);
+
+ $this->setTargetUrl($url);
+
+ if (!$this->isRedirect()) {
+ throw new \InvalidArgumentException(sprintf('The HTTP status code is not a redirect ("%s" given).', $status));
+ }
+
+ if (301 == $status && !\array_key_exists('cache-control', array_change_key_case($headers, \CASE_LOWER))) {
+ $this->headers->remove('cache-control');
+ }
+ }
+
+ /**
+ * Factory method for chainability.
+ *
+ * @param string $url The URL to redirect to
+ *
+ * @return static
+ *
+ * @deprecated since Symfony 5.1, use __construct() instead.
+ */
+ public static function create($url = '', int $status = 302, array $headers = [])
+ {
+ trigger_deprecation('symfony/http-foundation', '5.1', 'The "%s()" method is deprecated, use "new %s()" instead.', __METHOD__, static::class);
+
+ return new static($url, $status, $headers);
+ }
+
+ /**
+ * Returns the target URL.
+ *
+ * @return string
+ */
+ public function getTargetUrl()
+ {
+ return $this->targetUrl;
+ }
+
+ /**
+ * Sets the redirect target of this response.
+ *
+ * @return $this
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function setTargetUrl(string $url)
+ {
+ if ('' === $url) {
+ throw new \InvalidArgumentException('Cannot redirect to an empty URL.');
+ }
+
+ $this->targetUrl = $url;
+
+ $this->setContent(
+ sprintf('<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <meta http-equiv="refresh" content="0;url=\'%1$s\'" />
+
+ <title>Redirecting to %1$s</title>
+ </head>
+ <body>
+ Redirecting to <a href="%1$s">%1$s</a>.
+ </body>
+</html>', htmlspecialchars($url, \ENT_QUOTES, 'UTF-8')));
+
+ $this->headers->set('Location', $url);
+
+ return $this;
+ }
+}
diff --git a/symfony/http-foundation/Request.php b/symfony/http-foundation/Request.php
new file mode 100644
index 00000000..d112b1f1
--- /dev/null
+++ b/symfony/http-foundation/Request.php
@@ -0,0 +1,2147 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation;
+
+use Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException;
+use Symfony\Component\HttpFoundation\Exception\JsonException;
+use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException;
+use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException;
+use Symfony\Component\HttpFoundation\Session\SessionInterface;
+
+// Help opcache.preload discover always-needed symbols
+class_exists(AcceptHeader::class);
+class_exists(FileBag::class);
+class_exists(HeaderBag::class);
+class_exists(HeaderUtils::class);
+class_exists(InputBag::class);
+class_exists(ParameterBag::class);
+class_exists(ServerBag::class);
+
+/**
+ * Request represents an HTTP request.
+ *
+ * The methods dealing with URL accept / return a raw path (% encoded):
+ * * getBasePath
+ * * getBaseUrl
+ * * getPathInfo
+ * * getRequestUri
+ * * getUri
+ * * getUriForPath
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class Request
+{
+ public const HEADER_FORWARDED = 0b000001; // When using RFC 7239
+ public const HEADER_X_FORWARDED_FOR = 0b000010;
+ public const HEADER_X_FORWARDED_HOST = 0b000100;
+ public const HEADER_X_FORWARDED_PROTO = 0b001000;
+ public const HEADER_X_FORWARDED_PORT = 0b010000;
+ public const HEADER_X_FORWARDED_PREFIX = 0b100000;
+
+ /** @deprecated since Symfony 5.2, use either "HEADER_X_FORWARDED_FOR | HEADER_X_FORWARDED_HOST | HEADER_X_FORWARDED_PORT | HEADER_X_FORWARDED_PROTO" or "HEADER_X_FORWARDED_AWS_ELB" or "HEADER_X_FORWARDED_TRAEFIK" constants instead. */
+ public const HEADER_X_FORWARDED_ALL = 0b1011110; // All "X-Forwarded-*" headers sent by "usual" reverse proxy
+ public const HEADER_X_FORWARDED_AWS_ELB = 0b0011010; // AWS ELB doesn't send X-Forwarded-Host
+ public const HEADER_X_FORWARDED_TRAEFIK = 0b0111110; // All "X-Forwarded-*" headers sent by Traefik reverse proxy
+
+ public const METHOD_HEAD = 'HEAD';
+ public const METHOD_GET = 'GET';
+ public const METHOD_POST = 'POST';
+ public const METHOD_PUT = 'PUT';
+ public const METHOD_PATCH = 'PATCH';
+ public const METHOD_DELETE = 'DELETE';
+ public const METHOD_PURGE = 'PURGE';
+ public const METHOD_OPTIONS = 'OPTIONS';
+ public const METHOD_TRACE = 'TRACE';
+ public const METHOD_CONNECT = 'CONNECT';
+
+ /**
+ * @var string[]
+ */
+ protected static $trustedProxies = [];
+
+ /**
+ * @var string[]
+ */
+ protected static $trustedHostPatterns = [];
+
+ /**
+ * @var string[]
+ */
+ protected static $trustedHosts = [];
+
+ protected static $httpMethodParameterOverride = false;
+
+ /**
+ * Custom parameters.
+ *
+ * @var ParameterBag
+ */
+ public $attributes;
+
+ /**
+ * Request body parameters ($_POST).
+ *
+ * @var InputBag
+ */
+ public $request;
+
+ /**
+ * Query string parameters ($_GET).
+ *
+ * @var InputBag
+ */
+ public $query;
+
+ /**
+ * Server and execution environment parameters ($_SERVER).
+ *
+ * @var ServerBag
+ */
+ public $server;
+
+ /**
+ * Uploaded files ($_FILES).
+ *
+ * @var FileBag
+ */
+ public $files;
+
+ /**
+ * Cookies ($_COOKIE).
+ *
+ * @var InputBag
+ */
+ public $cookies;
+
+ /**
+ * Headers (taken from the $_SERVER).
+ *
+ * @var HeaderBag
+ */
+ public $headers;
+
+ /**
+ * @var string|resource|false|null
+ */
+ protected $content;
+
+ /**
+ * @var array
+ */
+ protected $languages;
+
+ /**
+ * @var array
+ */
+ protected $charsets;
+
+ /**
+ * @var array
+ */
+ protected $encodings;
+
+ /**
+ * @var array
+ */
+ protected $acceptableContentTypes;
+
+ /**
+ * @var string
+ */
+ protected $pathInfo;
+
+ /**
+ * @var string
+ */
+ protected $requestUri;
+
+ /**
+ * @var string
+ */
+ protected $baseUrl;
+
+ /**
+ * @var string
+ */
+ protected $basePath;
+
+ /**
+ * @var string
+ */
+ protected $method;
+
+ /**
+ * @var string
+ */
+ protected $format;
+
+ /**
+ * @var SessionInterface|callable(): SessionInterface
+ */
+ protected $session;
+
+ /**
+ * @var string
+ */
+ protected $locale;
+
+ /**
+ * @var string
+ */
+ protected $defaultLocale = 'en';
+
+ /**
+ * @var array
+ */
+ protected static $formats;
+
+ protected static $requestFactory;
+
+ /**
+ * @var string|null
+ */
+ private $preferredFormat;
+ private $isHostValid = true;
+ private $isForwardedValid = true;
+
+ /**
+ * @var bool|null
+ */
+ private $isSafeContentPreferred;
+
+ private static $trustedHeaderSet = -1;
+
+ private const FORWARDED_PARAMS = [
+ self::HEADER_X_FORWARDED_FOR => 'for',
+ self::HEADER_X_FORWARDED_HOST => 'host',
+ self::HEADER_X_FORWARDED_PROTO => 'proto',
+ self::HEADER_X_FORWARDED_PORT => 'host',
+ ];
+
+ /**
+ * Names for headers that can be trusted when
+ * using trusted proxies.
+ *
+ * The FORWARDED header is the standard as of rfc7239.
+ *
+ * The other headers are non-standard, but widely used
+ * by popular reverse proxies (like Apache mod_proxy or Amazon EC2).
+ */
+ private const TRUSTED_HEADERS = [
+ self::HEADER_FORWARDED => 'FORWARDED',
+ self::HEADER_X_FORWARDED_FOR => 'X_FORWARDED_FOR',
+ self::HEADER_X_FORWARDED_HOST => 'X_FORWARDED_HOST',
+ self::HEADER_X_FORWARDED_PROTO => 'X_FORWARDED_PROTO',
+ self::HEADER_X_FORWARDED_PORT => 'X_FORWARDED_PORT',
+ self::HEADER_X_FORWARDED_PREFIX => 'X_FORWARDED_PREFIX',
+ ];
+
+ /**
+ * @param array $query The GET parameters
+ * @param array $request The POST parameters
+ * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...)
+ * @param array $cookies The COOKIE parameters
+ * @param array $files The FILES parameters
+ * @param array $server The SERVER parameters
+ * @param string|resource|null $content The raw body data
+ */
+ public function __construct(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null)
+ {
+ $this->initialize($query, $request, $attributes, $cookies, $files, $server, $content);
+ }
+
+ /**
+ * Sets the parameters for this request.
+ *
+ * This method also re-initializes all properties.
+ *
+ * @param array $query The GET parameters
+ * @param array $request The POST parameters
+ * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...)
+ * @param array $cookies The COOKIE parameters
+ * @param array $files The FILES parameters
+ * @param array $server The SERVER parameters
+ * @param string|resource|null $content The raw body data
+ */
+ public function initialize(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null)
+ {
+ $this->request = new InputBag($request);
+ $this->query = new InputBag($query);
+ $this->attributes = new ParameterBag($attributes);
+ $this->cookies = new InputBag($cookies);
+ $this->files = new FileBag($files);
+ $this->server = new ServerBag($server);
+ $this->headers = new HeaderBag($this->server->getHeaders());
+
+ $this->content = $content;
+ $this->languages = null;
+ $this->charsets = null;
+ $this->encodings = null;
+ $this->acceptableContentTypes = null;
+ $this->pathInfo = null;
+ $this->requestUri = null;
+ $this->baseUrl = null;
+ $this->basePath = null;
+ $this->method = null;
+ $this->format = null;
+ }
+
+ /**
+ * Creates a new request with values from PHP's super globals.
+ *
+ * @return static
+ */
+ public static function createFromGlobals()
+ {
+ $request = self::createRequestFromFactory($_GET, $_POST, [], $_COOKIE, $_FILES, $_SERVER);
+
+ if (str_starts_with($request->headers->get('CONTENT_TYPE', ''), 'application/x-www-form-urlencoded')
+ && \in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), ['PUT', 'DELETE', 'PATCH'])
+ ) {
+ parse_str($request->getContent(), $data);
+ $request->request = new InputBag($data);
+ }
+
+ return $request;
+ }
+
+ /**
+ * Creates a Request based on a given URI and configuration.
+ *
+ * The information contained in the URI always take precedence
+ * over the other information (server and parameters).
+ *
+ * @param string $uri The URI
+ * @param string $method The HTTP method
+ * @param array $parameters The query (GET) or request (POST) parameters
+ * @param array $cookies The request cookies ($_COOKIE)
+ * @param array $files The request files ($_FILES)
+ * @param array $server The server parameters ($_SERVER)
+ * @param string|resource|null $content The raw body data
+ *
+ * @return static
+ */
+ public static function create(string $uri, string $method = 'GET', array $parameters = [], array $cookies = [], array $files = [], array $server = [], $content = null)
+ {
+ $server = array_replace([
+ 'SERVER_NAME' => 'localhost',
+ 'SERVER_PORT' => 80,
+ 'HTTP_HOST' => 'localhost',
+ 'HTTP_USER_AGENT' => 'Symfony',
+ 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+ 'HTTP_ACCEPT_LANGUAGE' => 'en-us,en;q=0.5',
+ 'HTTP_ACCEPT_CHARSET' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
+ 'REMOTE_ADDR' => '127.0.0.1',
+ 'SCRIPT_NAME' => '',
+ 'SCRIPT_FILENAME' => '',
+ 'SERVER_PROTOCOL' => 'HTTP/1.1',
+ 'REQUEST_TIME' => time(),
+ 'REQUEST_TIME_FLOAT' => microtime(true),
+ ], $server);
+
+ $server['PATH_INFO'] = '';
+ $server['REQUEST_METHOD'] = strtoupper($method);
+
+ $components = parse_url($uri);
+ if (isset($components['host'])) {
+ $server['SERVER_NAME'] = $components['host'];
+ $server['HTTP_HOST'] = $components['host'];
+ }
+
+ if (isset($components['scheme'])) {
+ if ('https' === $components['scheme']) {
+ $server['HTTPS'] = 'on';
+ $server['SERVER_PORT'] = 443;
+ } else {
+ unset($server['HTTPS']);
+ $server['SERVER_PORT'] = 80;
+ }
+ }
+
+ if (isset($components['port'])) {
+ $server['SERVER_PORT'] = $components['port'];
+ $server['HTTP_HOST'] .= ':'.$components['port'];
+ }
+
+ if (isset($components['user'])) {
+ $server['PHP_AUTH_USER'] = $components['user'];
+ }
+
+ if (isset($components['pass'])) {
+ $server['PHP_AUTH_PW'] = $components['pass'];
+ }
+
+ if (!isset($components['path'])) {
+ $components['path'] = '/';
+ }
+
+ switch (strtoupper($method)) {
+ case 'POST':
+ case 'PUT':
+ case 'DELETE':
+ if (!isset($server['CONTENT_TYPE'])) {
+ $server['CONTENT_TYPE'] = 'application/x-www-form-urlencoded';
+ }
+ // no break
+ case 'PATCH':
+ $request = $parameters;
+ $query = [];
+ break;
+ default:
+ $request = [];
+ $query = $parameters;
+ break;
+ }
+
+ $queryString = '';
+ if (isset($components['query'])) {
+ parse_str(html_entity_decode($components['query']), $qs);
+
+ if ($query) {
+ $query = array_replace($qs, $query);
+ $queryString = http_build_query($query, '', '&');
+ } else {
+ $query = $qs;
+ $queryString = $components['query'];
+ }
+ } elseif ($query) {
+ $queryString = http_build_query($query, '', '&');
+ }
+
+ $server['REQUEST_URI'] = $components['path'].('' !== $queryString ? '?'.$queryString : '');
+ $server['QUERY_STRING'] = $queryString;
+
+ return self::createRequestFromFactory($query, $request, [], $cookies, $files, $server, $content);
+ }
+
+ /**
+ * Sets a callable able to create a Request instance.
+ *
+ * This is mainly useful when you need to override the Request class
+ * to keep BC with an existing system. It should not be used for any
+ * other purpose.
+ */
+ public static function setFactory(?callable $callable)
+ {
+ self::$requestFactory = $callable;
+ }
+
+ /**
+ * Clones a request and overrides some of its parameters.
+ *
+ * @param array $query The GET parameters
+ * @param array $request The POST parameters
+ * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...)
+ * @param array $cookies The COOKIE parameters
+ * @param array $files The FILES parameters
+ * @param array $server The SERVER parameters
+ *
+ * @return static
+ */
+ public function duplicate(array $query = null, array $request = null, array $attributes = null, array $cookies = null, array $files = null, array $server = null)
+ {
+ $dup = clone $this;
+ if (null !== $query) {
+ $dup->query = new InputBag($query);
+ }
+ if (null !== $request) {
+ $dup->request = new InputBag($request);
+ }
+ if (null !== $attributes) {
+ $dup->attributes = new ParameterBag($attributes);
+ }
+ if (null !== $cookies) {
+ $dup->cookies = new InputBag($cookies);
+ }
+ if (null !== $files) {
+ $dup->files = new FileBag($files);
+ }
+ if (null !== $server) {
+ $dup->server = new ServerBag($server);
+ $dup->headers = new HeaderBag($dup->server->getHeaders());
+ }
+ $dup->languages = null;
+ $dup->charsets = null;
+ $dup->encodings = null;
+ $dup->acceptableContentTypes = null;
+ $dup->pathInfo = null;
+ $dup->requestUri = null;
+ $dup->baseUrl = null;
+ $dup->basePath = null;
+ $dup->method = null;
+ $dup->format = null;
+
+ if (!$dup->get('_format') && $this->get('_format')) {
+ $dup->attributes->set('_format', $this->get('_format'));
+ }
+
+ if (!$dup->getRequestFormat(null)) {
+ $dup->setRequestFormat($this->getRequestFormat(null));
+ }
+
+ return $dup;
+ }
+
+ /**
+ * Clones the current request.
+ *
+ * Note that the session is not cloned as duplicated requests
+ * are most of the time sub-requests of the main one.
+ */
+ public function __clone()
+ {
+ $this->query = clone $this->query;
+ $this->request = clone $this->request;
+ $this->attributes = clone $this->attributes;
+ $this->cookies = clone $this->cookies;
+ $this->files = clone $this->files;
+ $this->server = clone $this->server;
+ $this->headers = clone $this->headers;
+ }
+
+ /**
+ * Returns the request as a string.
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ $content = $this->getContent();
+
+ $cookieHeader = '';
+ $cookies = [];
+
+ foreach ($this->cookies as $k => $v) {
+ $cookies[] = $k.'='.$v;
+ }
+
+ if (!empty($cookies)) {
+ $cookieHeader = 'Cookie: '.implode('; ', $cookies)."\r\n";
+ }
+
+ return
+ sprintf('%s %s %s', $this->getMethod(), $this->getRequestUri(), $this->server->get('SERVER_PROTOCOL'))."\r\n".
+ $this->headers.
+ $cookieHeader."\r\n".
+ $content;
+ }
+
+ /**
+ * Overrides the PHP global variables according to this request instance.
+ *
+ * It overrides $_GET, $_POST, $_REQUEST, $_SERVER, $_COOKIE.
+ * $_FILES is never overridden, see rfc1867
+ */
+ public function overrideGlobals()
+ {
+ $this->server->set('QUERY_STRING', static::normalizeQueryString(http_build_query($this->query->all(), '', '&')));
+
+ $_GET = $this->query->all();
+ $_POST = $this->request->all();
+ $_SERVER = $this->server->all();
+ $_COOKIE = $this->cookies->all();
+
+ foreach ($this->headers->all() as $key => $value) {
+ $key = strtoupper(str_replace('-', '_', $key));
+ if (\in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH', 'CONTENT_MD5'], true)) {
+ $_SERVER[$key] = implode(', ', $value);
+ } else {
+ $_SERVER['HTTP_'.$key] = implode(', ', $value);
+ }
+ }
+
+ $request = ['g' => $_GET, 'p' => $_POST, 'c' => $_COOKIE];
+
+ $requestOrder = ini_get('request_order') ?: ini_get('variables_order');
+ $requestOrder = preg_replace('#[^cgp]#', '', strtolower($requestOrder)) ?: 'gp';
+
+ $_REQUEST = [[]];
+
+ foreach (str_split($requestOrder) as $order) {
+ $_REQUEST[] = $request[$order];
+ }
+
+ $_REQUEST = array_merge(...$_REQUEST);
+ }
+
+ /**
+ * Sets a list of trusted proxies.
+ *
+ * You should only list the reverse proxies that you manage directly.
+ *
+ * @param array $proxies A list of trusted proxies, the string 'REMOTE_ADDR' will be replaced with $_SERVER['REMOTE_ADDR']
+ * @param int $trustedHeaderSet A bit field of Request::HEADER_*, to set which headers to trust from your proxies
+ */
+ public static function setTrustedProxies(array $proxies, int $trustedHeaderSet)
+ {
+ if (self::HEADER_X_FORWARDED_ALL === $trustedHeaderSet) {
+ trigger_deprecation('symfony/http-foundation', '5.2', 'The "HEADER_X_FORWARDED_ALL" constant is deprecated, use either "HEADER_X_FORWARDED_FOR | HEADER_X_FORWARDED_HOST | HEADER_X_FORWARDED_PORT | HEADER_X_FORWARDED_PROTO" or "HEADER_X_FORWARDED_AWS_ELB" or "HEADER_X_FORWARDED_TRAEFIK" constants instead.');
+ }
+ self::$trustedProxies = array_reduce($proxies, function ($proxies, $proxy) {
+ if ('REMOTE_ADDR' !== $proxy) {
+ $proxies[] = $proxy;
+ } elseif (isset($_SERVER['REMOTE_ADDR'])) {
+ $proxies[] = $_SERVER['REMOTE_ADDR'];
+ }
+
+ return $proxies;
+ }, []);
+ self::$trustedHeaderSet = $trustedHeaderSet;
+ }
+
+ /**
+ * Gets the list of trusted proxies.
+ *
+ * @return array
+ */
+ public static function getTrustedProxies()
+ {
+ return self::$trustedProxies;
+ }
+
+ /**
+ * Gets the set of trusted headers from trusted proxies.
+ *
+ * @return int A bit field of Request::HEADER_* that defines which headers are trusted from your proxies
+ */
+ public static function getTrustedHeaderSet()
+ {
+ return self::$trustedHeaderSet;
+ }
+
+ /**
+ * Sets a list of trusted host patterns.
+ *
+ * You should only list the hosts you manage using regexs.
+ *
+ * @param array $hostPatterns A list of trusted host patterns
+ */
+ public static function setTrustedHosts(array $hostPatterns)
+ {
+ self::$trustedHostPatterns = array_map(function ($hostPattern) {
+ return sprintf('{%s}i', $hostPattern);
+ }, $hostPatterns);
+ // we need to reset trusted hosts on trusted host patterns change
+ self::$trustedHosts = [];
+ }
+
+ /**
+ * Gets the list of trusted host patterns.
+ *
+ * @return array
+ */
+ public static function getTrustedHosts()
+ {
+ return self::$trustedHostPatterns;
+ }
+
+ /**
+ * Normalizes a query string.
+ *
+ * It builds a normalized query string, where keys/value pairs are alphabetized,
+ * have consistent escaping and unneeded delimiters are removed.
+ *
+ * @return string
+ */
+ public static function normalizeQueryString(?string $qs)
+ {
+ if ('' === ($qs ?? '')) {
+ return '';
+ }
+
+ $qs = HeaderUtils::parseQuery($qs);
+ ksort($qs);
+
+ return http_build_query($qs, '', '&', \PHP_QUERY_RFC3986);
+ }
+
+ /**
+ * Enables support for the _method request parameter to determine the intended HTTP method.
+ *
+ * Be warned that enabling this feature might lead to CSRF issues in your code.
+ * Check that you are using CSRF tokens when required.
+ * If the HTTP method parameter override is enabled, an html-form with method "POST" can be altered
+ * and used to send a "PUT" or "DELETE" request via the _method request parameter.
+ * If these methods are not protected against CSRF, this presents a possible vulnerability.
+ *
+ * The HTTP method can only be overridden when the real HTTP method is POST.
+ */
+ public static function enableHttpMethodParameterOverride()
+ {
+ self::$httpMethodParameterOverride = true;
+ }
+
+ /**
+ * Checks whether support for the _method request parameter is enabled.
+ *
+ * @return bool
+ */
+ public static function getHttpMethodParameterOverride()
+ {
+ return self::$httpMethodParameterOverride;
+ }
+
+ /**
+ * Gets a "parameter" value from any bag.
+ *
+ * This method is mainly useful for libraries that want to provide some flexibility. If you don't need the
+ * flexibility in controllers, it is better to explicitly get request parameters from the appropriate
+ * public property instead (attributes, query, request).
+ *
+ * Order of precedence: PATH (routing placeholders or custom attributes), GET, POST
+ *
+ * @param mixed $default The default value if the parameter key does not exist
+ *
+ * @return mixed
+ *
+ * @internal since Symfony 5.4, use explicit input sources instead
+ */
+ public function get(string $key, $default = null)
+ {
+ if ($this !== $result = $this->attributes->get($key, $this)) {
+ return $result;
+ }
+
+ if ($this->query->has($key)) {
+ return $this->query->all()[$key];
+ }
+
+ if ($this->request->has($key)) {
+ return $this->request->all()[$key];
+ }
+
+ return $default;
+ }
+
+ /**
+ * Gets the Session.
+ *
+ * @return SessionInterface
+ */
+ public function getSession()
+ {
+ $session = $this->session;
+ if (!$session instanceof SessionInterface && null !== $session) {
+ $this->setSession($session = $session());
+ }
+
+ if (null === $session) {
+ throw new SessionNotFoundException('Session has not been set.');
+ }
+
+ return $session;
+ }
+
+ /**
+ * Whether the request contains a Session which was started in one of the
+ * previous requests.
+ *
+ * @return bool
+ */
+ public function hasPreviousSession()
+ {
+ // the check for $this->session avoids malicious users trying to fake a session cookie with proper name
+ return $this->hasSession() && $this->cookies->has($this->getSession()->getName());
+ }
+
+ /**
+ * Whether the request contains a Session object.
+ *
+ * This method does not give any information about the state of the session object,
+ * like whether the session is started or not. It is just a way to check if this Request
+ * is associated with a Session instance.
+ *
+ * @param bool $skipIfUninitialized When true, ignores factories injected by `setSessionFactory`
+ *
+ * @return bool
+ */
+ public function hasSession(/* bool $skipIfUninitialized = false */)
+ {
+ $skipIfUninitialized = \func_num_args() > 0 ? func_get_arg(0) : false;
+
+ return null !== $this->session && (!$skipIfUninitialized || $this->session instanceof SessionInterface);
+ }
+
+ public function setSession(SessionInterface $session)
+ {
+ $this->session = $session;
+ }
+
+ /**
+ * @internal
+ *
+ * @param callable(): SessionInterface $factory
+ */
+ public function setSessionFactory(callable $factory)
+ {
+ $this->session = $factory;
+ }
+
+ /**
+ * Returns the client IP addresses.
+ *
+ * In the returned array the most trusted IP address is first, and the
+ * least trusted one last. The "real" client IP address is the last one,
+ * but this is also the least trusted one. Trusted proxies are stripped.
+ *
+ * Use this method carefully; you should use getClientIp() instead.
+ *
+ * @return array
+ *
+ * @see getClientIp()
+ */
+ public function getClientIps()
+ {
+ $ip = $this->server->get('REMOTE_ADDR');
+
+ if (!$this->isFromTrustedProxy()) {
+ return [$ip];
+ }
+
+ return $this->getTrustedValues(self::HEADER_X_FORWARDED_FOR, $ip) ?: [$ip];
+ }
+
+ /**
+ * Returns the client IP address.
+ *
+ * This method can read the client IP address from the "X-Forwarded-For" header
+ * when trusted proxies were set via "setTrustedProxies()". The "X-Forwarded-For"
+ * header value is a comma+space separated list of IP addresses, the left-most
+ * being the original client, and each successive proxy that passed the request
+ * adding the IP address where it received the request from.
+ *
+ * If your reverse proxy uses a different header name than "X-Forwarded-For",
+ * ("Client-Ip" for instance), configure it via the $trustedHeaderSet
+ * argument of the Request::setTrustedProxies() method instead.
+ *
+ * @return string|null
+ *
+ * @see getClientIps()
+ * @see https://wikipedia.org/wiki/X-Forwarded-For
+ */
+ public function getClientIp()
+ {
+ $ipAddresses = $this->getClientIps();
+
+ return $ipAddresses[0];
+ }
+
+ /**
+ * Returns current script name.
+ *
+ * @return string
+ */
+ public function getScriptName()
+ {
+ return $this->server->get('SCRIPT_NAME', $this->server->get('ORIG_SCRIPT_NAME', ''));
+ }
+
+ /**
+ * Returns the path being requested relative to the executed script.
+ *
+ * The path info always starts with a /.
+ *
+ * Suppose this request is instantiated from /mysite on localhost:
+ *
+ * * http://localhost/mysite returns an empty string
+ * * http://localhost/mysite/about returns '/about'
+ * * http://localhost/mysite/enco%20ded returns '/enco%20ded'
+ * * http://localhost/mysite/about?var=1 returns '/about'
+ *
+ * @return string The raw path (i.e. not urldecoded)
+ */
+ public function getPathInfo()
+ {
+ if (null === $this->pathInfo) {
+ $this->pathInfo = $this->preparePathInfo();
+ }
+
+ return $this->pathInfo;
+ }
+
+ /**
+ * Returns the root path from which this request is executed.
+ *
+ * Suppose that an index.php file instantiates this request object:
+ *
+ * * http://localhost/index.php returns an empty string
+ * * http://localhost/index.php/page returns an empty string
+ * * http://localhost/web/index.php returns '/web'
+ * * http://localhost/we%20b/index.php returns '/we%20b'
+ *
+ * @return string The raw path (i.e. not urldecoded)
+ */
+ public function getBasePath()
+ {
+ if (null === $this->basePath) {
+ $this->basePath = $this->prepareBasePath();
+ }
+
+ return $this->basePath;
+ }
+
+ /**
+ * Returns the root URL from which this request is executed.
+ *
+ * The base URL never ends with a /.
+ *
+ * This is similar to getBasePath(), except that it also includes the
+ * script filename (e.g. index.php) if one exists.
+ *
+ * @return string The raw URL (i.e. not urldecoded)
+ */
+ public function getBaseUrl()
+ {
+ $trustedPrefix = '';
+
+ // the proxy prefix must be prepended to any prefix being needed at the webserver level
+ if ($this->isFromTrustedProxy() && $trustedPrefixValues = $this->getTrustedValues(self::HEADER_X_FORWARDED_PREFIX)) {
+ $trustedPrefix = rtrim($trustedPrefixValues[0], '/');
+ }
+
+ return $trustedPrefix.$this->getBaseUrlReal();
+ }
+
+ /**
+ * Returns the real base URL received by the webserver from which this request is executed.
+ * The URL does not include trusted reverse proxy prefix.
+ *
+ * @return string The raw URL (i.e. not urldecoded)
+ */
+ private function getBaseUrlReal(): string
+ {
+ if (null === $this->baseUrl) {
+ $this->baseUrl = $this->prepareBaseUrl();
+ }
+
+ return $this->baseUrl;
+ }
+
+ /**
+ * Gets the request's scheme.
+ *
+ * @return string
+ */
+ public function getScheme()
+ {
+ return $this->isSecure() ? 'https' : 'http';
+ }
+
+ /**
+ * Returns the port on which the request is made.
+ *
+ * This method can read the client port from the "X-Forwarded-Port" header
+ * when trusted proxies were set via "setTrustedProxies()".
+ *
+ * The "X-Forwarded-Port" header must contain the client port.
+ *
+ * @return int|string|null Can be a string if fetched from the server bag
+ */
+ public function getPort()
+ {
+ if ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_X_FORWARDED_PORT)) {
+ $host = $host[0];
+ } elseif ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_X_FORWARDED_HOST)) {
+ $host = $host[0];
+ } elseif (!$host = $this->headers->get('HOST')) {
+ return $this->server->get('SERVER_PORT');
+ }
+
+ if ('[' === $host[0]) {
+ $pos = strpos($host, ':', strrpos($host, ']'));
+ } else {
+ $pos = strrpos($host, ':');
+ }
+
+ if (false !== $pos && $port = substr($host, $pos + 1)) {
+ return (int) $port;
+ }
+
+ return 'https' === $this->getScheme() ? 443 : 80;
+ }
+
+ /**
+ * Returns the user.
+ *
+ * @return string|null
+ */
+ public function getUser()
+ {
+ return $this->headers->get('PHP_AUTH_USER');
+ }
+
+ /**
+ * Returns the password.
+ *
+ * @return string|null
+ */
+ public function getPassword()
+ {
+ return $this->headers->get('PHP_AUTH_PW');
+ }
+
+ /**
+ * Gets the user info.
+ *
+ * @return string|null A user name if any and, optionally, scheme-specific information about how to gain authorization to access the server
+ */
+ public function getUserInfo()
+ {
+ $userinfo = $this->getUser();
+
+ $pass = $this->getPassword();
+ if ('' != $pass) {
+ $userinfo .= ":$pass";
+ }
+
+ return $userinfo;
+ }
+
+ /**
+ * Returns the HTTP host being requested.
+ *
+ * The port name will be appended to the host if it's non-standard.
+ *
+ * @return string
+ */
+ public function getHttpHost()
+ {
+ $scheme = $this->getScheme();
+ $port = $this->getPort();
+
+ if (('http' == $scheme && 80 == $port) || ('https' == $scheme && 443 == $port)) {
+ return $this->getHost();
+ }
+
+ return $this->getHost().':'.$port;
+ }
+
+ /**
+ * Returns the requested URI (path and query string).
+ *
+ * @return string The raw URI (i.e. not URI decoded)
+ */
+ public function getRequestUri()
+ {
+ if (null === $this->requestUri) {
+ $this->requestUri = $this->prepareRequestUri();
+ }
+
+ return $this->requestUri;
+ }
+
+ /**
+ * Gets the scheme and HTTP host.
+ *
+ * If the URL was called with basic authentication, the user
+ * and the password are not added to the generated string.
+ *
+ * @return string
+ */
+ public function getSchemeAndHttpHost()
+ {
+ return $this->getScheme().'://'.$this->getHttpHost();
+ }
+
+ /**
+ * Generates a normalized URI (URL) for the Request.
+ *
+ * @return string
+ *
+ * @see getQueryString()
+ */
+ public function getUri()
+ {
+ if (null !== $qs = $this->getQueryString()) {
+ $qs = '?'.$qs;
+ }
+
+ return $this->getSchemeAndHttpHost().$this->getBaseUrl().$this->getPathInfo().$qs;
+ }
+
+ /**
+ * Generates a normalized URI for the given path.
+ *
+ * @param string $path A path to use instead of the current one
+ *
+ * @return string
+ */
+ public function getUriForPath(string $path)
+ {
+ return $this->getSchemeAndHttpHost().$this->getBaseUrl().$path;
+ }
+
+ /**
+ * Returns the path as relative reference from the current Request path.
+ *
+ * Only the URIs path component (no schema, host etc.) is relevant and must be given.
+ * Both paths must be absolute and not contain relative parts.
+ * Relative URLs from one resource to another are useful when generating self-contained downloadable document archives.
+ * Furthermore, they can be used to reduce the link size in documents.
+ *
+ * Example target paths, given a base path of "/a/b/c/d":
+ * - "/a/b/c/d" -> ""
+ * - "/a/b/c/" -> "./"
+ * - "/a/b/" -> "../"
+ * - "/a/b/c/other" -> "other"
+ * - "/a/x/y" -> "../../x/y"
+ *
+ * @return string
+ */
+ public function getRelativeUriForPath(string $path)
+ {
+ // be sure that we are dealing with an absolute path
+ if (!isset($path[0]) || '/' !== $path[0]) {
+ return $path;
+ }
+
+ if ($path === $basePath = $this->getPathInfo()) {
+ return '';
+ }
+
+ $sourceDirs = explode('/', isset($basePath[0]) && '/' === $basePath[0] ? substr($basePath, 1) : $basePath);
+ $targetDirs = explode('/', substr($path, 1));
+ array_pop($sourceDirs);
+ $targetFile = array_pop($targetDirs);
+
+ foreach ($sourceDirs as $i => $dir) {
+ if (isset($targetDirs[$i]) && $dir === $targetDirs[$i]) {
+ unset($sourceDirs[$i], $targetDirs[$i]);
+ } else {
+ break;
+ }
+ }
+
+ $targetDirs[] = $targetFile;
+ $path = str_repeat('../', \count($sourceDirs)).implode('/', $targetDirs);
+
+ // A reference to the same base directory or an empty subdirectory must be prefixed with "./".
+ // This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used
+ // as the first segment of a relative-path reference, as it would be mistaken for a scheme name
+ // (see https://tools.ietf.org/html/rfc3986#section-4.2).
+ return !isset($path[0]) || '/' === $path[0]
+ || false !== ($colonPos = strpos($path, ':')) && ($colonPos < ($slashPos = strpos($path, '/')) || false === $slashPos)
+ ? "./$path" : $path;
+ }
+
+ /**
+ * Generates the normalized query string for the Request.
+ *
+ * It builds a normalized query string, where keys/value pairs are alphabetized
+ * and have consistent escaping.
+ *
+ * @return string|null
+ */
+ public function getQueryString()
+ {
+ $qs = static::normalizeQueryString($this->server->get('QUERY_STRING'));
+
+ return '' === $qs ? null : $qs;
+ }
+
+ /**
+ * Checks whether the request is secure or not.
+ *
+ * This method can read the client protocol from the "X-Forwarded-Proto" header
+ * when trusted proxies were set via "setTrustedProxies()".
+ *
+ * The "X-Forwarded-Proto" header must contain the protocol: "https" or "http".
+ *
+ * @return bool
+ */
+ public function isSecure()
+ {
+ if ($this->isFromTrustedProxy() && $proto = $this->getTrustedValues(self::HEADER_X_FORWARDED_PROTO)) {
+ return \in_array(strtolower($proto[0]), ['https', 'on', 'ssl', '1'], true);
+ }
+
+ $https = $this->server->get('HTTPS');
+
+ return !empty($https) && 'off' !== strtolower($https);
+ }
+
+ /**
+ * Returns the host name.
+ *
+ * This method can read the client host name from the "X-Forwarded-Host" header
+ * when trusted proxies were set via "setTrustedProxies()".
+ *
+ * The "X-Forwarded-Host" header must contain the client host name.
+ *
+ * @return string
+ *
+ * @throws SuspiciousOperationException when the host name is invalid or not trusted
+ */
+ public function getHost()
+ {
+ if ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_X_FORWARDED_HOST)) {
+ $host = $host[0];
+ } elseif (!$host = $this->headers->get('HOST')) {
+ if (!$host = $this->server->get('SERVER_NAME')) {
+ $host = $this->server->get('SERVER_ADDR', '');
+ }
+ }
+
+ // trim and remove port number from host
+ // host is lowercase as per RFC 952/2181
+ $host = strtolower(preg_replace('/:\d+$/', '', trim($host)));
+
+ // as the host can come from the user (HTTP_HOST and depending on the configuration, SERVER_NAME too can come from the user)
+ // check that it does not contain forbidden characters (see RFC 952 and RFC 2181)
+ // use preg_replace() instead of preg_match() to prevent DoS attacks with long host names
+ if ($host && '' !== preg_replace('/(?:^\[)?[a-zA-Z0-9-:\]_]+\.?/', '', $host)) {
+ if (!$this->isHostValid) {
+ return '';
+ }
+ $this->isHostValid = false;
+
+ throw new SuspiciousOperationException(sprintf('Invalid Host "%s".', $host));
+ }
+
+ if (\count(self::$trustedHostPatterns) > 0) {
+ // to avoid host header injection attacks, you should provide a list of trusted host patterns
+
+ if (\in_array($host, self::$trustedHosts)) {
+ return $host;
+ }
+
+ foreach (self::$trustedHostPatterns as $pattern) {
+ if (preg_match($pattern, $host)) {
+ self::$trustedHosts[] = $host;
+
+ return $host;
+ }
+ }
+
+ if (!$this->isHostValid) {
+ return '';
+ }
+ $this->isHostValid = false;
+
+ throw new SuspiciousOperationException(sprintf('Untrusted Host "%s".', $host));
+ }
+
+ return $host;
+ }
+
+ /**
+ * Sets the request method.
+ */
+ public function setMethod(string $method)
+ {
+ $this->method = null;
+ $this->server->set('REQUEST_METHOD', $method);
+ }
+
+ /**
+ * Gets the request "intended" method.
+ *
+ * If the X-HTTP-Method-Override header is set, and if the method is a POST,
+ * then it is used to determine the "real" intended HTTP method.
+ *
+ * The _method request parameter can also be used to determine the HTTP method,
+ * but only if enableHttpMethodParameterOverride() has been called.
+ *
+ * The method is always an uppercased string.
+ *
+ * @return string
+ *
+ * @see getRealMethod()
+ */
+ public function getMethod()
+ {
+ if (null !== $this->method) {
+ return $this->method;
+ }
+
+ $this->method = strtoupper($this->server->get('REQUEST_METHOD', 'GET'));
+
+ if ('POST' !== $this->method) {
+ return $this->method;
+ }
+
+ $method = $this->headers->get('X-HTTP-METHOD-OVERRIDE');
+
+ if (!$method && self::$httpMethodParameterOverride) {
+ $method = $this->request->get('_method', $this->query->get('_method', 'POST'));
+ }
+
+ if (!\is_string($method)) {
+ return $this->method;
+ }
+
+ $method = strtoupper($method);
+
+ if (\in_array($method, ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'PATCH', 'PURGE', 'TRACE'], true)) {
+ return $this->method = $method;
+ }
+
+ if (!preg_match('/^[A-Z]++$/D', $method)) {
+ throw new SuspiciousOperationException(sprintf('Invalid method override "%s".', $method));
+ }
+
+ return $this->method = $method;
+ }
+
+ /**
+ * Gets the "real" request method.
+ *
+ * @return string
+ *
+ * @see getMethod()
+ */
+ public function getRealMethod()
+ {
+ return strtoupper($this->server->get('REQUEST_METHOD', 'GET'));
+ }
+
+ /**
+ * Gets the mime type associated with the format.
+ *
+ * @return string|null
+ */
+ public function getMimeType(string $format)
+ {
+ if (null === static::$formats) {
+ static::initializeFormats();
+ }
+
+ return isset(static::$formats[$format]) ? static::$formats[$format][0] : null;
+ }
+
+ /**
+ * Gets the mime types associated with the format.
+ *
+ * @return array
+ */
+ public static function getMimeTypes(string $format)
+ {
+ if (null === static::$formats) {
+ static::initializeFormats();
+ }
+
+ return static::$formats[$format] ?? [];
+ }
+
+ /**
+ * Gets the format associated with the mime type.
+ *
+ * @return string|null
+ */
+ public function getFormat(?string $mimeType)
+ {
+ $canonicalMimeType = null;
+ if ($mimeType && false !== $pos = strpos($mimeType, ';')) {
+ $canonicalMimeType = trim(substr($mimeType, 0, $pos));
+ }
+
+ if (null === static::$formats) {
+ static::initializeFormats();
+ }
+
+ foreach (static::$formats as $format => $mimeTypes) {
+ if (\in_array($mimeType, (array) $mimeTypes)) {
+ return $format;
+ }
+ if (null !== $canonicalMimeType && \in_array($canonicalMimeType, (array) $mimeTypes)) {
+ return $format;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Associates a format with mime types.
+ *
+ * @param string|array $mimeTypes The associated mime types (the preferred one must be the first as it will be used as the content type)
+ */
+ public function setFormat(?string $format, $mimeTypes)
+ {
+ if (null === static::$formats) {
+ static::initializeFormats();
+ }
+
+ static::$formats[$format] = \is_array($mimeTypes) ? $mimeTypes : [$mimeTypes];
+ }
+
+ /**
+ * Gets the request format.
+ *
+ * Here is the process to determine the format:
+ *
+ * * format defined by the user (with setRequestFormat())
+ * * _format request attribute
+ * * $default
+ *
+ * @see getPreferredFormat
+ *
+ * @return string|null
+ */
+ public function getRequestFormat(?string $default = 'html')
+ {
+ if (null === $this->format) {
+ $this->format = $this->attributes->get('_format');
+ }
+
+ return $this->format ?? $default;
+ }
+
+ /**
+ * Sets the request format.
+ */
+ public function setRequestFormat(?string $format)
+ {
+ $this->format = $format;
+ }
+
+ /**
+ * Gets the format associated with the request.
+ *
+ * @return string|null
+ */
+ public function getContentType()
+ {
+ return $this->getFormat($this->headers->get('CONTENT_TYPE', ''));
+ }
+
+ /**
+ * Sets the default locale.
+ */
+ public function setDefaultLocale(string $locale)
+ {
+ $this->defaultLocale = $locale;
+
+ if (null === $this->locale) {
+ $this->setPhpDefaultLocale($locale);
+ }
+ }
+
+ /**
+ * Get the default locale.
+ *
+ * @return string
+ */
+ public function getDefaultLocale()
+ {
+ return $this->defaultLocale;
+ }
+
+ /**
+ * Sets the locale.
+ */
+ public function setLocale(string $locale)
+ {
+ $this->setPhpDefaultLocale($this->locale = $locale);
+ }
+
+ /**
+ * Get the locale.
+ *
+ * @return string
+ */
+ public function getLocale()
+ {
+ return null === $this->locale ? $this->defaultLocale : $this->locale;
+ }
+
+ /**
+ * Checks if the request method is of specified type.
+ *
+ * @param string $method Uppercase request method (GET, POST etc)
+ *
+ * @return bool
+ */
+ public function isMethod(string $method)
+ {
+ return $this->getMethod() === strtoupper($method);
+ }
+
+ /**
+ * Checks whether or not the method is safe.
+ *
+ * @see https://tools.ietf.org/html/rfc7231#section-4.2.1
+ *
+ * @return bool
+ */
+ public function isMethodSafe()
+ {
+ return \in_array($this->getMethod(), ['GET', 'HEAD', 'OPTIONS', 'TRACE']);
+ }
+
+ /**
+ * Checks whether or not the method is idempotent.
+ *
+ * @return bool
+ */
+ public function isMethodIdempotent()
+ {
+ return \in_array($this->getMethod(), ['HEAD', 'GET', 'PUT', 'DELETE', 'TRACE', 'OPTIONS', 'PURGE']);
+ }
+
+ /**
+ * Checks whether the method is cacheable or not.
+ *
+ * @see https://tools.ietf.org/html/rfc7231#section-4.2.3
+ *
+ * @return bool
+ */
+ public function isMethodCacheable()
+ {
+ return \in_array($this->getMethod(), ['GET', 'HEAD']);
+ }
+
+ /**
+ * Returns the protocol version.
+ *
+ * If the application is behind a proxy, the protocol version used in the
+ * requests between the client and the proxy and between the proxy and the
+ * server might be different. This returns the former (from the "Via" header)
+ * if the proxy is trusted (see "setTrustedProxies()"), otherwise it returns
+ * the latter (from the "SERVER_PROTOCOL" server parameter).
+ *
+ * @return string|null
+ */
+ public function getProtocolVersion()
+ {
+ if ($this->isFromTrustedProxy()) {
+ preg_match('~^(HTTP/)?([1-9]\.[0-9]) ~', $this->headers->get('Via') ?? '', $matches);
+
+ if ($matches) {
+ return 'HTTP/'.$matches[2];
+ }
+ }
+
+ return $this->server->get('SERVER_PROTOCOL');
+ }
+
+ /**
+ * Returns the request body content.
+ *
+ * @param bool $asResource If true, a resource will be returned
+ *
+ * @return string|resource
+ */
+ public function getContent(bool $asResource = false)
+ {
+ $currentContentIsResource = \is_resource($this->content);
+
+ if (true === $asResource) {
+ if ($currentContentIsResource) {
+ rewind($this->content);
+
+ return $this->content;
+ }
+
+ // Content passed in parameter (test)
+ if (\is_string($this->content)) {
+ $resource = fopen('php://temp', 'r+');
+ fwrite($resource, $this->content);
+ rewind($resource);
+
+ return $resource;
+ }
+
+ $this->content = false;
+
+ return fopen('php://input', 'r');
+ }
+
+ if ($currentContentIsResource) {
+ rewind($this->content);
+
+ return stream_get_contents($this->content);
+ }
+
+ if (null === $this->content || false === $this->content) {
+ $this->content = file_get_contents('php://input');
+ }
+
+ return $this->content;
+ }
+
+ /**
+ * Gets the request body decoded as array, typically from a JSON payload.
+ *
+ * @throws JsonException When the body cannot be decoded to an array
+ *
+ * @return array
+ */
+ public function toArray()
+ {
+ if ('' === $content = $this->getContent()) {
+ throw new JsonException('Request body is empty.');
+ }
+
+ try {
+ $content = json_decode($content, true, 512, \JSON_BIGINT_AS_STRING | (\PHP_VERSION_ID >= 70300 ? \JSON_THROW_ON_ERROR : 0));
+ } catch (\JsonException $e) {
+ throw new JsonException('Could not decode request body.', $e->getCode(), $e);
+ }
+
+ if (\PHP_VERSION_ID < 70300 && \JSON_ERROR_NONE !== json_last_error()) {
+ throw new JsonException('Could not decode request body: '.json_last_error_msg(), json_last_error());
+ }
+
+ if (!\is_array($content)) {
+ throw new JsonException(sprintf('JSON content was expected to decode to an array, "%s" returned.', get_debug_type($content)));
+ }
+
+ return $content;
+ }
+
+ /**
+ * Gets the Etags.
+ *
+ * @return array
+ */
+ public function getETags()
+ {
+ return preg_split('/\s*,\s*/', $this->headers->get('If-None-Match', ''), -1, \PREG_SPLIT_NO_EMPTY);
+ }
+
+ /**
+ * @return bool
+ */
+ public function isNoCache()
+ {
+ return $this->headers->hasCacheControlDirective('no-cache') || 'no-cache' == $this->headers->get('Pragma');
+ }
+
+ /**
+ * Gets the preferred format for the response by inspecting, in the following order:
+ * * the request format set using setRequestFormat;
+ * * the values of the Accept HTTP header.
+ *
+ * Note that if you use this method, you should send the "Vary: Accept" header
+ * in the response to prevent any issues with intermediary HTTP caches.
+ */
+ public function getPreferredFormat(?string $default = 'html'): ?string
+ {
+ if (null !== $this->preferredFormat || null !== $this->preferredFormat = $this->getRequestFormat(null)) {
+ return $this->preferredFormat;
+ }
+
+ foreach ($this->getAcceptableContentTypes() as $mimeType) {
+ if ($this->preferredFormat = $this->getFormat($mimeType)) {
+ return $this->preferredFormat;
+ }
+ }
+
+ return $default;
+ }
+
+ /**
+ * Returns the preferred language.
+ *
+ * @param string[] $locales An array of ordered available locales
+ *
+ * @return string|null
+ */
+ public function getPreferredLanguage(array $locales = null)
+ {
+ $preferredLanguages = $this->getLanguages();
+
+ if (empty($locales)) {
+ return $preferredLanguages[0] ?? null;
+ }
+
+ if (!$preferredLanguages) {
+ return $locales[0];
+ }
+
+ $extendedPreferredLanguages = [];
+ foreach ($preferredLanguages as $language) {
+ $extendedPreferredLanguages[] = $language;
+ if (false !== $position = strpos($language, '_')) {
+ $superLanguage = substr($language, 0, $position);
+ if (!\in_array($superLanguage, $preferredLanguages)) {
+ $extendedPreferredLanguages[] = $superLanguage;
+ }
+ }
+ }
+
+ $preferredLanguages = array_values(array_intersect($extendedPreferredLanguages, $locales));
+
+ return $preferredLanguages[0] ?? $locales[0];
+ }
+
+ /**
+ * Gets a list of languages acceptable by the client browser ordered in the user browser preferences.
+ *
+ * @return array
+ */
+ public function getLanguages()
+ {
+ if (null !== $this->languages) {
+ return $this->languages;
+ }
+
+ $languages = AcceptHeader::fromString($this->headers->get('Accept-Language'))->all();
+ $this->languages = [];
+ foreach ($languages as $lang => $acceptHeaderItem) {
+ if (str_contains($lang, '-')) {
+ $codes = explode('-', $lang);
+ if ('i' === $codes[0]) {
+ // Language not listed in ISO 639 that are not variants
+ // of any listed language, which can be registered with the
+ // i-prefix, such as i-cherokee
+ if (\count($codes) > 1) {
+ $lang = $codes[1];
+ }
+ } else {
+ for ($i = 0, $max = \count($codes); $i < $max; ++$i) {
+ if (0 === $i) {
+ $lang = strtolower($codes[0]);
+ } else {
+ $lang .= '_'.strtoupper($codes[$i]);
+ }
+ }
+ }
+ }
+
+ $this->languages[] = $lang;
+ }
+
+ return $this->languages;
+ }
+
+ /**
+ * Gets a list of charsets acceptable by the client browser in preferable order.
+ *
+ * @return array
+ */
+ public function getCharsets()
+ {
+ if (null !== $this->charsets) {
+ return $this->charsets;
+ }
+
+ return $this->charsets = array_keys(AcceptHeader::fromString($this->headers->get('Accept-Charset'))->all());
+ }
+
+ /**
+ * Gets a list of encodings acceptable by the client browser in preferable order.
+ *
+ * @return array
+ */
+ public function getEncodings()
+ {
+ if (null !== $this->encodings) {
+ return $this->encodings;
+ }
+
+ return $this->encodings = array_keys(AcceptHeader::fromString($this->headers->get('Accept-Encoding'))->all());
+ }
+
+ /**
+ * Gets a list of content types acceptable by the client browser in preferable order.
+ *
+ * @return array
+ */
+ public function getAcceptableContentTypes()
+ {
+ if (null !== $this->acceptableContentTypes) {
+ return $this->acceptableContentTypes;
+ }
+
+ return $this->acceptableContentTypes = array_keys(AcceptHeader::fromString($this->headers->get('Accept'))->all());
+ }
+
+ /**
+ * Returns true if the request is an XMLHttpRequest.
+ *
+ * It works if your JavaScript library sets an X-Requested-With HTTP header.
+ * It is known to work with common JavaScript frameworks:
+ *
+ * @see https://wikipedia.org/wiki/List_of_Ajax_frameworks#JavaScript
+ *
+ * @return bool
+ */
+ public function isXmlHttpRequest()
+ {
+ return 'XMLHttpRequest' == $this->headers->get('X-Requested-With');
+ }
+
+ /**
+ * Checks whether the client browser prefers safe content or not according to RFC8674.
+ *
+ * @see https://tools.ietf.org/html/rfc8674
+ */
+ public function preferSafeContent(): bool
+ {
+ if (null !== $this->isSafeContentPreferred) {
+ return $this->isSafeContentPreferred;
+ }
+
+ if (!$this->isSecure()) {
+ // see https://tools.ietf.org/html/rfc8674#section-3
+ return $this->isSafeContentPreferred = false;
+ }
+
+ return $this->isSafeContentPreferred = AcceptHeader::fromString($this->headers->get('Prefer'))->has('safe');
+ }
+
+ /*
+ * The following methods are derived from code of the Zend Framework (1.10dev - 2010-01-24)
+ *
+ * Code subject to the new BSD license (https://framework.zend.com/license).
+ *
+ * Copyright (c) 2005-2010 Zend Technologies USA Inc. (https://www.zend.com/)
+ */
+
+ protected function prepareRequestUri()
+ {
+ $requestUri = '';
+
+ if ('1' == $this->server->get('IIS_WasUrlRewritten') && '' != $this->server->get('UNENCODED_URL')) {
+ // IIS7 with URL Rewrite: make sure we get the unencoded URL (double slash problem)
+ $requestUri = $this->server->get('UNENCODED_URL');
+ $this->server->remove('UNENCODED_URL');
+ $this->server->remove('IIS_WasUrlRewritten');
+ } elseif ($this->server->has('REQUEST_URI')) {
+ $requestUri = $this->server->get('REQUEST_URI');
+
+ if ('' !== $requestUri && '/' === $requestUri[0]) {
+ // To only use path and query remove the fragment.
+ if (false !== $pos = strpos($requestUri, '#')) {
+ $requestUri = substr($requestUri, 0, $pos);
+ }
+ } else {
+ // HTTP proxy reqs setup request URI with scheme and host [and port] + the URL path,
+ // only use URL path.
+ $uriComponents = parse_url($requestUri);
+
+ if (isset($uriComponents['path'])) {
+ $requestUri = $uriComponents['path'];
+ }
+
+ if (isset($uriComponents['query'])) {
+ $requestUri .= '?'.$uriComponents['query'];
+ }
+ }
+ } elseif ($this->server->has('ORIG_PATH_INFO')) {
+ // IIS 5.0, PHP as CGI
+ $requestUri = $this->server->get('ORIG_PATH_INFO');
+ if ('' != $this->server->get('QUERY_STRING')) {
+ $requestUri .= '?'.$this->server->get('QUERY_STRING');
+ }
+ $this->server->remove('ORIG_PATH_INFO');
+ }
+
+ // normalize the request URI to ease creating sub-requests from this request
+ $this->server->set('REQUEST_URI', $requestUri);
+
+ return $requestUri;
+ }
+
+ /**
+ * Prepares the base URL.
+ *
+ * @return string
+ */
+ protected function prepareBaseUrl()
+ {
+ $filename = basename($this->server->get('SCRIPT_FILENAME', ''));
+
+ if (basename($this->server->get('SCRIPT_NAME', '')) === $filename) {
+ $baseUrl = $this->server->get('SCRIPT_NAME');
+ } elseif (basename($this->server->get('PHP_SELF', '')) === $filename) {
+ $baseUrl = $this->server->get('PHP_SELF');
+ } elseif (basename($this->server->get('ORIG_SCRIPT_NAME', '')) === $filename) {
+ $baseUrl = $this->server->get('ORIG_SCRIPT_NAME'); // 1and1 shared hosting compatibility
+ } else {
+ // Backtrack up the script_filename to find the portion matching
+ // php_self
+ $path = $this->server->get('PHP_SELF', '');
+ $file = $this->server->get('SCRIPT_FILENAME', '');
+ $segs = explode('/', trim($file, '/'));
+ $segs = array_reverse($segs);
+ $index = 0;
+ $last = \count($segs);
+ $baseUrl = '';
+ do {
+ $seg = $segs[$index];
+ $baseUrl = '/'.$seg.$baseUrl;
+ ++$index;
+ } while ($last > $index && (false !== $pos = strpos($path, $baseUrl)) && 0 != $pos);
+ }
+
+ // Does the baseUrl have anything in common with the request_uri?
+ $requestUri = $this->getRequestUri();
+ if ('' !== $requestUri && '/' !== $requestUri[0]) {
+ $requestUri = '/'.$requestUri;
+ }
+
+ if ($baseUrl && null !== $prefix = $this->getUrlencodedPrefix($requestUri, $baseUrl)) {
+ // full $baseUrl matches
+ return $prefix;
+ }
+
+ if ($baseUrl && null !== $prefix = $this->getUrlencodedPrefix($requestUri, rtrim(\dirname($baseUrl), '/'.\DIRECTORY_SEPARATOR).'/')) {
+ // directory portion of $baseUrl matches
+ return rtrim($prefix, '/'.\DIRECTORY_SEPARATOR);
+ }
+
+ $truncatedRequestUri = $requestUri;
+ if (false !== $pos = strpos($requestUri, '?')) {
+ $truncatedRequestUri = substr($requestUri, 0, $pos);
+ }
+
+ $basename = basename($baseUrl ?? '');
+ if (empty($basename) || !strpos(rawurldecode($truncatedRequestUri), $basename)) {
+ // no match whatsoever; set it blank
+ return '';
+ }
+
+ // If using mod_rewrite or ISAPI_Rewrite strip the script filename
+ // out of baseUrl. $pos !== 0 makes sure it is not matching a value
+ // from PATH_INFO or QUERY_STRING
+ if (\strlen($requestUri) >= \strlen($baseUrl) && (false !== $pos = strpos($requestUri, $baseUrl)) && 0 !== $pos) {
+ $baseUrl = substr($requestUri, 0, $pos + \strlen($baseUrl));
+ }
+
+ return rtrim($baseUrl, '/'.\DIRECTORY_SEPARATOR);
+ }
+
+ /**
+ * Prepares the base path.
+ *
+ * @return string
+ */
+ protected function prepareBasePath()
+ {
+ $baseUrl = $this->getBaseUrl();
+ if (empty($baseUrl)) {
+ return '';
+ }
+
+ $filename = basename($this->server->get('SCRIPT_FILENAME'));
+ if (basename($baseUrl) === $filename) {
+ $basePath = \dirname($baseUrl);
+ } else {
+ $basePath = $baseUrl;
+ }
+
+ if ('\\' === \DIRECTORY_SEPARATOR) {
+ $basePath = str_replace('\\', '/', $basePath);
+ }
+
+ return rtrim($basePath, '/');
+ }
+
+ /**
+ * Prepares the path info.
+ *
+ * @return string
+ */
+ protected function preparePathInfo()
+ {
+ if (null === ($requestUri = $this->getRequestUri())) {
+ return '/';
+ }
+
+ // Remove the query string from REQUEST_URI
+ if (false !== $pos = strpos($requestUri, '?')) {
+ $requestUri = substr($requestUri, 0, $pos);
+ }
+ if ('' !== $requestUri && '/' !== $requestUri[0]) {
+ $requestUri = '/'.$requestUri;
+ }
+
+ if (null === ($baseUrl = $this->getBaseUrlReal())) {
+ return $requestUri;
+ }
+
+ $pathInfo = substr($requestUri, \strlen($baseUrl));
+ if (false === $pathInfo || '' === $pathInfo) {
+ // If substr() returns false then PATH_INFO is set to an empty string
+ return '/';
+ }
+
+ return $pathInfo;
+ }
+
+ /**
+ * Initializes HTTP request formats.
+ */
+ protected static function initializeFormats()
+ {
+ static::$formats = [
+ 'html' => ['text/html', 'application/xhtml+xml'],
+ 'txt' => ['text/plain'],
+ 'js' => ['application/javascript', 'application/x-javascript', 'text/javascript'],
+ 'css' => ['text/css'],
+ 'json' => ['application/json', 'application/x-json'],
+ 'jsonld' => ['application/ld+json'],
+ 'xml' => ['text/xml', 'application/xml', 'application/x-xml'],
+ 'rdf' => ['application/rdf+xml'],
+ 'atom' => ['application/atom+xml'],
+ 'rss' => ['application/rss+xml'],
+ 'form' => ['application/x-www-form-urlencoded', 'multipart/form-data'],
+ ];
+ }
+
+ private function setPhpDefaultLocale(string $locale): void
+ {
+ // if either the class Locale doesn't exist, or an exception is thrown when
+ // setting the default locale, the intl module is not installed, and
+ // the call can be ignored:
+ try {
+ if (class_exists(\Locale::class, false)) {
+ \Locale::setDefault($locale);
+ }
+ } catch (\Exception $e) {
+ }
+ }
+
+ /**
+ * Returns the prefix as encoded in the string when the string starts with
+ * the given prefix, null otherwise.
+ */
+ private function getUrlencodedPrefix(string $string, string $prefix): ?string
+ {
+ if (!str_starts_with(rawurldecode($string), $prefix)) {
+ return null;
+ }
+
+ $len = \strlen($prefix);
+
+ if (preg_match(sprintf('#^(%%[[:xdigit:]]{2}|.){%d}#', $len), $string, $match)) {
+ return $match[0];
+ }
+
+ return null;
+ }
+
+ private static function createRequestFromFactory(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null): self
+ {
+ if (self::$requestFactory) {
+ $request = (self::$requestFactory)($query, $request, $attributes, $cookies, $files, $server, $content);
+
+ if (!$request instanceof self) {
+ throw new \LogicException('The Request factory must return an instance of Symfony\Component\HttpFoundation\Request.');
+ }
+
+ return $request;
+ }
+
+ return new static($query, $request, $attributes, $cookies, $files, $server, $content);
+ }
+
+ /**
+ * Indicates whether this request originated from a trusted proxy.
+ *
+ * This can be useful to determine whether or not to trust the
+ * contents of a proxy-specific header.
+ *
+ * @return bool
+ */
+ public function isFromTrustedProxy()
+ {
+ return self::$trustedProxies && IpUtils::checkIp($this->server->get('REMOTE_ADDR', ''), self::$trustedProxies);
+ }
+
+ private function getTrustedValues(int $type, string $ip = null): array
+ {
+ $clientValues = [];
+ $forwardedValues = [];
+
+ if ((self::$trustedHeaderSet & $type) && $this->headers->has(self::TRUSTED_HEADERS[$type])) {
+ foreach (explode(',', $this->headers->get(self::TRUSTED_HEADERS[$type])) as $v) {
+ $clientValues[] = (self::HEADER_X_FORWARDED_PORT === $type ? '0.0.0.0:' : '').trim($v);
+ }
+ }
+
+ if ((self::$trustedHeaderSet & self::HEADER_FORWARDED) && (isset(self::FORWARDED_PARAMS[$type])) && $this->headers->has(self::TRUSTED_HEADERS[self::HEADER_FORWARDED])) {
+ $forwarded = $this->headers->get(self::TRUSTED_HEADERS[self::HEADER_FORWARDED]);
+ $parts = HeaderUtils::split($forwarded, ',;=');
+ $forwardedValues = [];
+ $param = self::FORWARDED_PARAMS[$type];
+ foreach ($parts as $subParts) {
+ if (null === $v = HeaderUtils::combine($subParts)[$param] ?? null) {
+ continue;
+ }
+ if (self::HEADER_X_FORWARDED_PORT === $type) {
+ if (str_ends_with($v, ']') || false === $v = strrchr($v, ':')) {
+ $v = $this->isSecure() ? ':443' : ':80';
+ }
+ $v = '0.0.0.0'.$v;
+ }
+ $forwardedValues[] = $v;
+ }
+ }
+
+ if (null !== $ip) {
+ $clientValues = $this->normalizeAndFilterClientIps($clientValues, $ip);
+ $forwardedValues = $this->normalizeAndFilterClientIps($forwardedValues, $ip);
+ }
+
+ if ($forwardedValues === $clientValues || !$clientValues) {
+ return $forwardedValues;
+ }
+
+ if (!$forwardedValues) {
+ return $clientValues;
+ }
+
+ if (!$this->isForwardedValid) {
+ return null !== $ip ? ['0.0.0.0', $ip] : [];
+ }
+ $this->isForwardedValid = false;
+
+ throw new ConflictingHeadersException(sprintf('The request has both a trusted "%s" header and a trusted "%s" header, conflicting with each other. You should either configure your proxy to remove one of them, or configure your project to distrust the offending one.', self::TRUSTED_HEADERS[self::HEADER_FORWARDED], self::TRUSTED_HEADERS[$type]));
+ }
+
+ private function normalizeAndFilterClientIps(array $clientIps, string $ip): array
+ {
+ if (!$clientIps) {
+ return [];
+ }
+ $clientIps[] = $ip; // Complete the IP chain with the IP the request actually came from
+ $firstTrustedIp = null;
+
+ foreach ($clientIps as $key => $clientIp) {
+ if (strpos($clientIp, '.')) {
+ // Strip :port from IPv4 addresses. This is allowed in Forwarded
+ // and may occur in X-Forwarded-For.
+ $i = strpos($clientIp, ':');
+ if ($i) {
+ $clientIps[$key] = $clientIp = substr($clientIp, 0, $i);
+ }
+ } elseif (str_starts_with($clientIp, '[')) {
+ // Strip brackets and :port from IPv6 addresses.
+ $i = strpos($clientIp, ']', 1);
+ $clientIps[$key] = $clientIp = substr($clientIp, 1, $i - 1);
+ }
+
+ if (!filter_var($clientIp, \FILTER_VALIDATE_IP)) {
+ unset($clientIps[$key]);
+
+ continue;
+ }
+
+ if (IpUtils::checkIp($clientIp, self::$trustedProxies)) {
+ unset($clientIps[$key]);
+
+ // Fallback to this when the client IP falls into the range of trusted proxies
+ if (null === $firstTrustedIp) {
+ $firstTrustedIp = $clientIp;
+ }
+ }
+ }
+
+ // Now the IP chain contains only untrusted proxies and the client IP
+ return $clientIps ? array_reverse($clientIps) : [$firstTrustedIp];
+ }
+}
diff --git a/symfony/http-foundation/RequestMatcher.php b/symfony/http-foundation/RequestMatcher.php
new file mode 100644
index 00000000..f2645f9a
--- /dev/null
+++ b/symfony/http-foundation/RequestMatcher.php
@@ -0,0 +1,196 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation;
+
+/**
+ * RequestMatcher compares a pre-defined set of checks against a Request instance.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class RequestMatcher implements RequestMatcherInterface
+{
+ /**
+ * @var string|null
+ */
+ private $path;
+
+ /**
+ * @var string|null
+ */
+ private $host;
+
+ /**
+ * @var int|null
+ */
+ private $port;
+
+ /**
+ * @var string[]
+ */
+ private $methods = [];
+
+ /**
+ * @var string[]
+ */
+ private $ips = [];
+
+ /**
+ * @var array
+ */
+ private $attributes = [];
+
+ /**
+ * @var string[]
+ */
+ private $schemes = [];
+
+ /**
+ * @param string|string[]|null $methods
+ * @param string|string[]|null $ips
+ * @param string|string[]|null $schemes
+ */
+ public function __construct(string $path = null, string $host = null, $methods = null, $ips = null, array $attributes = [], $schemes = null, int $port = null)
+ {
+ $this->matchPath($path);
+ $this->matchHost($host);
+ $this->matchMethod($methods);
+ $this->matchIps($ips);
+ $this->matchScheme($schemes);
+ $this->matchPort($port);
+
+ foreach ($attributes as $k => $v) {
+ $this->matchAttribute($k, $v);
+ }
+ }
+
+ /**
+ * Adds a check for the HTTP scheme.
+ *
+ * @param string|string[]|null $scheme An HTTP scheme or an array of HTTP schemes
+ */
+ public function matchScheme($scheme)
+ {
+ $this->schemes = null !== $scheme ? array_map('strtolower', (array) $scheme) : [];
+ }
+
+ /**
+ * Adds a check for the URL host name.
+ */
+ public function matchHost(?string $regexp)
+ {
+ $this->host = $regexp;
+ }
+
+ /**
+ * Adds a check for the the URL port.
+ *
+ * @param int|null $port The port number to connect to
+ */
+ public function matchPort(?int $port)
+ {
+ $this->port = $port;
+ }
+
+ /**
+ * Adds a check for the URL path info.
+ */
+ public function matchPath(?string $regexp)
+ {
+ $this->path = $regexp;
+ }
+
+ /**
+ * Adds a check for the client IP.
+ *
+ * @param string $ip A specific IP address or a range specified using IP/netmask like 192.168.1.0/24
+ */
+ public function matchIp(string $ip)
+ {
+ $this->matchIps($ip);
+ }
+
+ /**
+ * Adds a check for the client IP.
+ *
+ * @param string|string[]|null $ips A specific IP address or a range specified using IP/netmask like 192.168.1.0/24
+ */
+ public function matchIps($ips)
+ {
+ $ips = null !== $ips ? (array) $ips : [];
+
+ $this->ips = array_reduce($ips, static function (array $ips, string $ip) {
+ return array_merge($ips, preg_split('/\s*,\s*/', $ip));
+ }, []);
+ }
+
+ /**
+ * Adds a check for the HTTP method.
+ *
+ * @param string|string[]|null $method An HTTP method or an array of HTTP methods
+ */
+ public function matchMethod($method)
+ {
+ $this->methods = null !== $method ? array_map('strtoupper', (array) $method) : [];
+ }
+
+ /**
+ * Adds a check for request attribute.
+ */
+ public function matchAttribute(string $key, string $regexp)
+ {
+ $this->attributes[$key] = $regexp;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function matches(Request $request)
+ {
+ if ($this->schemes && !\in_array($request->getScheme(), $this->schemes, true)) {
+ return false;
+ }
+
+ if ($this->methods && !\in_array($request->getMethod(), $this->methods, true)) {
+ return false;
+ }
+
+ foreach ($this->attributes as $key => $pattern) {
+ $requestAttribute = $request->attributes->get($key);
+ if (!\is_string($requestAttribute)) {
+ return false;
+ }
+ if (!preg_match('{'.$pattern.'}', $requestAttribute)) {
+ return false;
+ }
+ }
+
+ if (null !== $this->path && !preg_match('{'.$this->path.'}', rawurldecode($request->getPathInfo()))) {
+ return false;
+ }
+
+ if (null !== $this->host && !preg_match('{'.$this->host.'}i', $request->getHost())) {
+ return false;
+ }
+
+ if (null !== $this->port && 0 < $this->port && $request->getPort() !== $this->port) {
+ return false;
+ }
+
+ if (IpUtils::checkIp($request->getClientIp() ?? '', $this->ips)) {
+ return true;
+ }
+
+ // Note to future implementors: add additional checks above the
+ // foreach above or else your check might not be run!
+ return 0 === \count($this->ips);
+ }
+}
diff --git a/symfony/http-foundation/RequestMatcherInterface.php b/symfony/http-foundation/RequestMatcherInterface.php
new file mode 100644
index 00000000..c2e14785
--- /dev/null
+++ b/symfony/http-foundation/RequestMatcherInterface.php
@@ -0,0 +1,27 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation;
+
+/**
+ * RequestMatcherInterface is an interface for strategies to match a Request.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+interface RequestMatcherInterface
+{
+ /**
+ * Decides whether the rule(s) implemented by the strategy matches the supplied request.
+ *
+ * @return bool
+ */
+ public function matches(Request $request);
+}
diff --git a/symfony/http-foundation/RequestStack.php b/symfony/http-foundation/RequestStack.php
new file mode 100644
index 00000000..855b5181
--- /dev/null
+++ b/symfony/http-foundation/RequestStack.php
@@ -0,0 +1,128 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation;
+
+use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException;
+use Symfony\Component\HttpFoundation\Session\SessionInterface;
+
+/**
+ * Request stack that controls the lifecycle of requests.
+ *
+ * @author Benjamin Eberlei <kontakt@beberlei.de>
+ */
+class RequestStack
+{
+ /**
+ * @var Request[]
+ */
+ private $requests = [];
+
+ /**
+ * Pushes a Request on the stack.
+ *
+ * This method should generally not be called directly as the stack
+ * management should be taken care of by the application itself.
+ */
+ public function push(Request $request)
+ {
+ $this->requests[] = $request;
+ }
+
+ /**
+ * Pops the current request from the stack.
+ *
+ * This operation lets the current request go out of scope.
+ *
+ * This method should generally not be called directly as the stack
+ * management should be taken care of by the application itself.
+ *
+ * @return Request|null
+ */
+ public function pop()
+ {
+ if (!$this->requests) {
+ return null;
+ }
+
+ return array_pop($this->requests);
+ }
+
+ /**
+ * @return Request|null
+ */
+ public function getCurrentRequest()
+ {
+ return end($this->requests) ?: null;
+ }
+
+ /**
+ * Gets the main request.
+ *
+ * Be warned that making your code aware of the main request
+ * might make it un-compatible with other features of your framework
+ * like ESI support.
+ */
+ public function getMainRequest(): ?Request
+ {
+ if (!$this->requests) {
+ return null;
+ }
+
+ return $this->requests[0];
+ }
+
+ /**
+ * Gets the master request.
+ *
+ * @return Request|null
+ *
+ * @deprecated since symfony/http-foundation 5.3, use getMainRequest() instead
+ */
+ public function getMasterRequest()
+ {
+ trigger_deprecation('symfony/http-foundation', '5.3', '"%s()" is deprecated, use "getMainRequest()" instead.', __METHOD__);
+
+ return $this->getMainRequest();
+ }
+
+ /**
+ * Returns the parent request of the current.
+ *
+ * Be warned that making your code aware of the parent request
+ * might make it un-compatible with other features of your framework
+ * like ESI support.
+ *
+ * If current Request is the main request, it returns null.
+ *
+ * @return Request|null
+ */
+ public function getParentRequest()
+ {
+ $pos = \count($this->requests) - 2;
+
+ return $this->requests[$pos] ?? null;
+ }
+
+ /**
+ * Gets the current session.
+ *
+ * @throws SessionNotFoundException
+ */
+ public function getSession(): SessionInterface
+ {
+ if ((null !== $request = end($this->requests) ?: null) && $request->hasSession()) {
+ return $request->getSession();
+ }
+
+ throw new SessionNotFoundException();
+ }
+}
diff --git a/symfony/http-foundation/Response.php b/symfony/http-foundation/Response.php
new file mode 100644
index 00000000..def7f8e7
--- /dev/null
+++ b/symfony/http-foundation/Response.php
@@ -0,0 +1,1285 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation;
+
+// Help opcache.preload discover always-needed symbols
+class_exists(ResponseHeaderBag::class);
+
+/**
+ * Response represents an HTTP response.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class Response
+{
+ public const HTTP_CONTINUE = 100;
+ public const HTTP_SWITCHING_PROTOCOLS = 101;
+ public const HTTP_PROCESSING = 102; // RFC2518
+ public const HTTP_EARLY_HINTS = 103; // RFC8297
+ public const HTTP_OK = 200;
+ public const HTTP_CREATED = 201;
+ public const HTTP_ACCEPTED = 202;
+ public const HTTP_NON_AUTHORITATIVE_INFORMATION = 203;
+ public const HTTP_NO_CONTENT = 204;
+ public const HTTP_RESET_CONTENT = 205;
+ public const HTTP_PARTIAL_CONTENT = 206;
+ public const HTTP_MULTI_STATUS = 207; // RFC4918
+ public const HTTP_ALREADY_REPORTED = 208; // RFC5842
+ public const HTTP_IM_USED = 226; // RFC3229
+ public const HTTP_MULTIPLE_CHOICES = 300;
+ public const HTTP_MOVED_PERMANENTLY = 301;
+ public const HTTP_FOUND = 302;
+ public const HTTP_SEE_OTHER = 303;
+ public const HTTP_NOT_MODIFIED = 304;
+ public const HTTP_USE_PROXY = 305;
+ public const HTTP_RESERVED = 306;
+ public const HTTP_TEMPORARY_REDIRECT = 307;
+ public const HTTP_PERMANENTLY_REDIRECT = 308; // RFC7238
+ public const HTTP_BAD_REQUEST = 400;
+ public const HTTP_UNAUTHORIZED = 401;
+ public const HTTP_PAYMENT_REQUIRED = 402;
+ public const HTTP_FORBIDDEN = 403;
+ public const HTTP_NOT_FOUND = 404;
+ public const HTTP_METHOD_NOT_ALLOWED = 405;
+ public const HTTP_NOT_ACCEPTABLE = 406;
+ public const HTTP_PROXY_AUTHENTICATION_REQUIRED = 407;
+ public const HTTP_REQUEST_TIMEOUT = 408;
+ public const HTTP_CONFLICT = 409;
+ public const HTTP_GONE = 410;
+ public const HTTP_LENGTH_REQUIRED = 411;
+ public const HTTP_PRECONDITION_FAILED = 412;
+ public const HTTP_REQUEST_ENTITY_TOO_LARGE = 413;
+ public const HTTP_REQUEST_URI_TOO_LONG = 414;
+ public const HTTP_UNSUPPORTED_MEDIA_TYPE = 415;
+ public const HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
+ public const HTTP_EXPECTATION_FAILED = 417;
+ public const HTTP_I_AM_A_TEAPOT = 418; // RFC2324
+ public const HTTP_MISDIRECTED_REQUEST = 421; // RFC7540
+ public const HTTP_UNPROCESSABLE_ENTITY = 422; // RFC4918
+ public const HTTP_LOCKED = 423; // RFC4918
+ public const HTTP_FAILED_DEPENDENCY = 424; // RFC4918
+ public const HTTP_TOO_EARLY = 425; // RFC-ietf-httpbis-replay-04
+ public const HTTP_UPGRADE_REQUIRED = 426; // RFC2817
+ public const HTTP_PRECONDITION_REQUIRED = 428; // RFC6585
+ public const HTTP_TOO_MANY_REQUESTS = 429; // RFC6585
+ public const HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE = 431; // RFC6585
+ public const HTTP_UNAVAILABLE_FOR_LEGAL_REASONS = 451;
+ public const HTTP_INTERNAL_SERVER_ERROR = 500;
+ public const HTTP_NOT_IMPLEMENTED = 501;
+ public const HTTP_BAD_GATEWAY = 502;
+ public const HTTP_SERVICE_UNAVAILABLE = 503;
+ public const HTTP_GATEWAY_TIMEOUT = 504;
+ public const HTTP_VERSION_NOT_SUPPORTED = 505;
+ public const HTTP_VARIANT_ALSO_NEGOTIATES_EXPERIMENTAL = 506; // RFC2295
+ public const HTTP_INSUFFICIENT_STORAGE = 507; // RFC4918
+ public const HTTP_LOOP_DETECTED = 508; // RFC5842
+ public const HTTP_NOT_EXTENDED = 510; // RFC2774
+ public const HTTP_NETWORK_AUTHENTICATION_REQUIRED = 511; // RFC6585
+
+ /**
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
+ */
+ private const HTTP_RESPONSE_CACHE_CONTROL_DIRECTIVES = [
+ 'must_revalidate' => false,
+ 'no_cache' => false,
+ 'no_store' => false,
+ 'no_transform' => false,
+ 'public' => false,
+ 'private' => false,
+ 'proxy_revalidate' => false,
+ 'max_age' => true,
+ 's_maxage' => true,
+ 'immutable' => false,
+ 'last_modified' => true,
+ 'etag' => true,
+ ];
+
+ /**
+ * @var ResponseHeaderBag
+ */
+ public $headers;
+
+ /**
+ * @var string
+ */
+ protected $content;
+
+ /**
+ * @var string
+ */
+ protected $version;
+
+ /**
+ * @var int
+ */
+ protected $statusCode;
+
+ /**
+ * @var string
+ */
+ protected $statusText;
+
+ /**
+ * @var string
+ */
+ protected $charset;
+
+ /**
+ * Status codes translation table.
+ *
+ * The list of codes is complete according to the
+ * {@link https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml Hypertext Transfer Protocol (HTTP) Status Code Registry}
+ * (last updated 2021-10-01).
+ *
+ * Unless otherwise noted, the status code is defined in RFC2616.
+ *
+ * @var array
+ */
+ public static $statusTexts = [
+ 100 => 'Continue',
+ 101 => 'Switching Protocols',
+ 102 => 'Processing', // RFC2518
+ 103 => 'Early Hints',
+ 200 => 'OK',
+ 201 => 'Created',
+ 202 => 'Accepted',
+ 203 => 'Non-Authoritative Information',
+ 204 => 'No Content',
+ 205 => 'Reset Content',
+ 206 => 'Partial Content',
+ 207 => 'Multi-Status', // RFC4918
+ 208 => 'Already Reported', // RFC5842
+ 226 => 'IM Used', // RFC3229
+ 300 => 'Multiple Choices',
+ 301 => 'Moved Permanently',
+ 302 => 'Found',
+ 303 => 'See Other',
+ 304 => 'Not Modified',
+ 305 => 'Use Proxy',
+ 307 => 'Temporary Redirect',
+ 308 => 'Permanent Redirect', // RFC7238
+ 400 => 'Bad Request',
+ 401 => 'Unauthorized',
+ 402 => 'Payment Required',
+ 403 => 'Forbidden',
+ 404 => 'Not Found',
+ 405 => 'Method Not Allowed',
+ 406 => 'Not Acceptable',
+ 407 => 'Proxy Authentication Required',
+ 408 => 'Request Timeout',
+ 409 => 'Conflict',
+ 410 => 'Gone',
+ 411 => 'Length Required',
+ 412 => 'Precondition Failed',
+ 413 => 'Content Too Large', // RFC-ietf-httpbis-semantics
+ 414 => 'URI Too Long',
+ 415 => 'Unsupported Media Type',
+ 416 => 'Range Not Satisfiable',
+ 417 => 'Expectation Failed',
+ 418 => 'I\'m a teapot', // RFC2324
+ 421 => 'Misdirected Request', // RFC7540
+ 422 => 'Unprocessable Content', // RFC-ietf-httpbis-semantics
+ 423 => 'Locked', // RFC4918
+ 424 => 'Failed Dependency', // RFC4918
+ 425 => 'Too Early', // RFC-ietf-httpbis-replay-04
+ 426 => 'Upgrade Required', // RFC2817
+ 428 => 'Precondition Required', // RFC6585
+ 429 => 'Too Many Requests', // RFC6585
+ 431 => 'Request Header Fields Too Large', // RFC6585
+ 451 => 'Unavailable For Legal Reasons', // RFC7725
+ 500 => 'Internal Server Error',
+ 501 => 'Not Implemented',
+ 502 => 'Bad Gateway',
+ 503 => 'Service Unavailable',
+ 504 => 'Gateway Timeout',
+ 505 => 'HTTP Version Not Supported',
+ 506 => 'Variant Also Negotiates', // RFC2295
+ 507 => 'Insufficient Storage', // RFC4918
+ 508 => 'Loop Detected', // RFC5842
+ 510 => 'Not Extended', // RFC2774
+ 511 => 'Network Authentication Required', // RFC6585
+ ];
+
+ /**
+ * @throws \InvalidArgumentException When the HTTP status code is not valid
+ */
+ public function __construct(?string $content = '', int $status = 200, array $headers = [])
+ {
+ $this->headers = new ResponseHeaderBag($headers);
+ $this->setContent($content);
+ $this->setStatusCode($status);
+ $this->setProtocolVersion('1.0');
+ }
+
+ /**
+ * Factory method for chainability.
+ *
+ * Example:
+ *
+ * return Response::create($body, 200)
+ * ->setSharedMaxAge(300);
+ *
+ * @return static
+ *
+ * @deprecated since Symfony 5.1, use __construct() instead.
+ */
+ public static function create(?string $content = '', int $status = 200, array $headers = [])
+ {
+ trigger_deprecation('symfony/http-foundation', '5.1', 'The "%s()" method is deprecated, use "new %s()" instead.', __METHOD__, static::class);
+
+ return new static($content, $status, $headers);
+ }
+
+ /**
+ * Returns the Response as an HTTP string.
+ *
+ * The string representation of the Response is the same as the
+ * one that will be sent to the client only if the prepare() method
+ * has been called before.
+ *
+ * @return string
+ *
+ * @see prepare()
+ */
+ public function __toString()
+ {
+ return
+ sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)."\r\n".
+ $this->headers."\r\n".
+ $this->getContent();
+ }
+
+ /**
+ * Clones the current Response instance.
+ */
+ public function __clone()
+ {
+ $this->headers = clone $this->headers;
+ }
+
+ /**
+ * Prepares the Response before it is sent to the client.
+ *
+ * This method tweaks the Response to ensure that it is
+ * compliant with RFC 2616. Most of the changes are based on
+ * the Request that is "associated" with this Response.
+ *
+ * @return $this
+ */
+ public function prepare(Request $request)
+ {
+ $headers = $this->headers;
+
+ if ($this->isInformational() || $this->isEmpty()) {
+ $this->setContent(null);
+ $headers->remove('Content-Type');
+ $headers->remove('Content-Length');
+ // prevent PHP from sending the Content-Type header based on default_mimetype
+ ini_set('default_mimetype', '');
+ } else {
+ // Content-type based on the Request
+ if (!$headers->has('Content-Type')) {
+ $format = $request->getRequestFormat(null);
+ if (null !== $format && $mimeType = $request->getMimeType($format)) {
+ $headers->set('Content-Type', $mimeType);
+ }
+ }
+
+ // Fix Content-Type
+ $charset = $this->charset ?: 'UTF-8';
+ if (!$headers->has('Content-Type')) {
+ $headers->set('Content-Type', 'text/html; charset='.$charset);
+ } elseif (0 === stripos($headers->get('Content-Type'), 'text/') && false === stripos($headers->get('Content-Type'), 'charset')) {
+ // add the charset
+ $headers->set('Content-Type', $headers->get('Content-Type').'; charset='.$charset);
+ }
+
+ // Fix Content-Length
+ if ($headers->has('Transfer-Encoding')) {
+ $headers->remove('Content-Length');
+ }
+
+ if ($request->isMethod('HEAD')) {
+ // cf. RFC2616 14.13
+ $length = $headers->get('Content-Length');
+ $this->setContent(null);
+ if ($length) {
+ $headers->set('Content-Length', $length);
+ }
+ }
+ }
+
+ // Fix protocol
+ if ('HTTP/1.0' != $request->server->get('SERVER_PROTOCOL')) {
+ $this->setProtocolVersion('1.1');
+ }
+
+ // Check if we need to send extra expire info headers
+ if ('1.0' == $this->getProtocolVersion() && str_contains($headers->get('Cache-Control', ''), 'no-cache')) {
+ $headers->set('pragma', 'no-cache');
+ $headers->set('expires', -1);
+ }
+
+ $this->ensureIEOverSSLCompatibility($request);
+
+ if ($request->isSecure()) {
+ foreach ($headers->getCookies() as $cookie) {
+ $cookie->setSecureDefault(true);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Sends HTTP headers.
+ *
+ * @return $this
+ */
+ public function sendHeaders()
+ {
+ // headers have already been sent by the developer
+ if (headers_sent()) {
+ return $this;
+ }
+
+ // headers
+ foreach ($this->headers->allPreserveCaseWithoutCookies() as $name => $values) {
+ $replace = 0 === strcasecmp($name, 'Content-Type');
+ foreach ($values as $value) {
+ header($name.': '.$value, $replace, $this->statusCode);
+ }
+ }
+
+ // cookies
+ foreach ($this->headers->getCookies() as $cookie) {
+ header('Set-Cookie: '.$cookie, false, $this->statusCode);
+ }
+
+ // status
+ header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode);
+
+ return $this;
+ }
+
+ /**
+ * Sends content for the current web response.
+ *
+ * @return $this
+ */
+ public function sendContent()
+ {
+ echo $this->content;
+
+ return $this;
+ }
+
+ /**
+ * Sends HTTP headers and content.
+ *
+ * @return $this
+ */
+ public function send()
+ {
+ $this->sendHeaders();
+ $this->sendContent();
+
+ if (\function_exists('fastcgi_finish_request')) {
+ fastcgi_finish_request();
+ } elseif (\function_exists('litespeed_finish_request')) {
+ litespeed_finish_request();
+ } elseif (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) {
+ static::closeOutputBuffers(0, true);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Sets the response content.
+ *
+ * @return $this
+ */
+ public function setContent(?string $content)
+ {
+ $this->content = $content ?? '';
+
+ return $this;
+ }
+
+ /**
+ * Gets the current response content.
+ *
+ * @return string|false
+ */
+ public function getContent()
+ {
+ return $this->content;
+ }
+
+ /**
+ * Sets the HTTP protocol version (1.0 or 1.1).
+ *
+ * @return $this
+ *
+ * @final
+ */
+ public function setProtocolVersion(string $version): object
+ {
+ $this->version = $version;
+
+ return $this;
+ }
+
+ /**
+ * Gets the HTTP protocol version.
+ *
+ * @final
+ */
+ public function getProtocolVersion(): string
+ {
+ return $this->version;
+ }
+
+ /**
+ * Sets the response status code.
+ *
+ * If the status text is null it will be automatically populated for the known
+ * status codes and left empty otherwise.
+ *
+ * @return $this
+ *
+ * @throws \InvalidArgumentException When the HTTP status code is not valid
+ *
+ * @final
+ */
+ public function setStatusCode(int $code, string $text = null): object
+ {
+ $this->statusCode = $code;
+ if ($this->isInvalid()) {
+ throw new \InvalidArgumentException(sprintf('The HTTP status code "%s" is not valid.', $code));
+ }
+
+ if (null === $text) {
+ $this->statusText = self::$statusTexts[$code] ?? 'unknown status';
+
+ return $this;
+ }
+
+ if (false === $text) {
+ $this->statusText = '';
+
+ return $this;
+ }
+
+ $this->statusText = $text;
+
+ return $this;
+ }
+
+ /**
+ * Retrieves the status code for the current web response.
+ *
+ * @final
+ */
+ public function getStatusCode(): int
+ {
+ return $this->statusCode;
+ }
+
+ /**
+ * Sets the response charset.
+ *
+ * @return $this
+ *
+ * @final
+ */
+ public function setCharset(string $charset): object
+ {
+ $this->charset = $charset;
+
+ return $this;
+ }
+
+ /**
+ * Retrieves the response charset.
+ *
+ * @final
+ */
+ public function getCharset(): ?string
+ {
+ return $this->charset;
+ }
+
+ /**
+ * Returns true if the response may safely be kept in a shared (surrogate) cache.
+ *
+ * Responses marked "private" with an explicit Cache-Control directive are
+ * considered uncacheable.
+ *
+ * Responses with neither a freshness lifetime (Expires, max-age) nor cache
+ * validator (Last-Modified, ETag) are considered uncacheable because there is
+ * no way to tell when or how to remove them from the cache.
+ *
+ * Note that RFC 7231 and RFC 7234 possibly allow for a more permissive implementation,
+ * for example "status codes that are defined as cacheable by default [...]
+ * can be reused by a cache with heuristic expiration unless otherwise indicated"
+ * (https://tools.ietf.org/html/rfc7231#section-6.1)
+ *
+ * @final
+ */
+ public function isCacheable(): bool
+ {
+ if (!\in_array($this->statusCode, [200, 203, 300, 301, 302, 404, 410])) {
+ return false;
+ }
+
+ if ($this->headers->hasCacheControlDirective('no-store') || $this->headers->getCacheControlDirective('private')) {
+ return false;
+ }
+
+ return $this->isValidateable() || $this->isFresh();
+ }
+
+ /**
+ * Returns true if the response is "fresh".
+ *
+ * Fresh responses may be served from cache without any interaction with the
+ * origin. A response is considered fresh when it includes a Cache-Control/max-age
+ * indicator or Expires header and the calculated age is less than the freshness lifetime.
+ *
+ * @final
+ */
+ public function isFresh(): bool
+ {
+ return $this->getTtl() > 0;
+ }
+
+ /**
+ * Returns true if the response includes headers that can be used to validate
+ * the response with the origin server using a conditional GET request.
+ *
+ * @final
+ */
+ public function isValidateable(): bool
+ {
+ return $this->headers->has('Last-Modified') || $this->headers->has('ETag');
+ }
+
+ /**
+ * Marks the response as "private".
+ *
+ * It makes the response ineligible for serving other clients.
+ *
+ * @return $this
+ *
+ * @final
+ */
+ public function setPrivate(): object
+ {
+ $this->headers->removeCacheControlDirective('public');
+ $this->headers->addCacheControlDirective('private');
+
+ return $this;
+ }
+
+ /**
+ * Marks the response as "public".
+ *
+ * It makes the response eligible for serving other clients.
+ *
+ * @return $this
+ *
+ * @final
+ */
+ public function setPublic(): object
+ {
+ $this->headers->addCacheControlDirective('public');
+ $this->headers->removeCacheControlDirective('private');
+
+ return $this;
+ }
+
+ /**
+ * Marks the response as "immutable".
+ *
+ * @return $this
+ *
+ * @final
+ */
+ public function setImmutable(bool $immutable = true): object
+ {
+ if ($immutable) {
+ $this->headers->addCacheControlDirective('immutable');
+ } else {
+ $this->headers->removeCacheControlDirective('immutable');
+ }
+
+ return $this;
+ }
+
+ /**
+ * Returns true if the response is marked as "immutable".
+ *
+ * @final
+ */
+ public function isImmutable(): bool
+ {
+ return $this->headers->hasCacheControlDirective('immutable');
+ }
+
+ /**
+ * Returns true if the response must be revalidated by shared caches once it has become stale.
+ *
+ * This method indicates that the response must not be served stale by a
+ * cache in any circumstance without first revalidating with the origin.
+ * When present, the TTL of the response should not be overridden to be
+ * greater than the value provided by the origin.
+ *
+ * @final
+ */
+ public function mustRevalidate(): bool
+ {
+ return $this->headers->hasCacheControlDirective('must-revalidate') || $this->headers->hasCacheControlDirective('proxy-revalidate');
+ }
+
+ /**
+ * Returns the Date header as a DateTime instance.
+ *
+ * @throws \RuntimeException When the header is not parseable
+ *
+ * @final
+ */
+ public function getDate(): ?\DateTimeInterface
+ {
+ return $this->headers->getDate('Date');
+ }
+
+ /**
+ * Sets the Date header.
+ *
+ * @return $this
+ *
+ * @final
+ */
+ public function setDate(\DateTimeInterface $date): object
+ {
+ if ($date instanceof \DateTime) {
+ $date = \DateTimeImmutable::createFromMutable($date);
+ }
+
+ $date = $date->setTimezone(new \DateTimeZone('UTC'));
+ $this->headers->set('Date', $date->format('D, d M Y H:i:s').' GMT');
+
+ return $this;
+ }
+
+ /**
+ * Returns the age of the response in seconds.
+ *
+ * @final
+ */
+ public function getAge(): int
+ {
+ if (null !== $age = $this->headers->get('Age')) {
+ return (int) $age;
+ }
+
+ return max(time() - (int) $this->getDate()->format('U'), 0);
+ }
+
+ /**
+ * Marks the response stale by setting the Age header to be equal to the maximum age of the response.
+ *
+ * @return $this
+ */
+ public function expire()
+ {
+ if ($this->isFresh()) {
+ $this->headers->set('Age', $this->getMaxAge());
+ $this->headers->remove('Expires');
+ }
+
+ return $this;
+ }
+
+ /**
+ * Returns the value of the Expires header as a DateTime instance.
+ *
+ * @final
+ */
+ public function getExpires(): ?\DateTimeInterface
+ {
+ try {
+ return $this->headers->getDate('Expires');
+ } catch (\RuntimeException $e) {
+ // according to RFC 2616 invalid date formats (e.g. "0" and "-1") must be treated as in the past
+ return \DateTime::createFromFormat('U', time() - 172800);
+ }
+ }
+
+ /**
+ * Sets the Expires HTTP header with a DateTime instance.
+ *
+ * Passing null as value will remove the header.
+ *
+ * @return $this
+ *
+ * @final
+ */
+ public function setExpires(\DateTimeInterface $date = null): object
+ {
+ if (null === $date) {
+ $this->headers->remove('Expires');
+
+ return $this;
+ }
+
+ if ($date instanceof \DateTime) {
+ $date = \DateTimeImmutable::createFromMutable($date);
+ }
+
+ $date = $date->setTimezone(new \DateTimeZone('UTC'));
+ $this->headers->set('Expires', $date->format('D, d M Y H:i:s').' GMT');
+
+ return $this;
+ }
+
+ /**
+ * Returns the number of seconds after the time specified in the response's Date
+ * header when the response should no longer be considered fresh.
+ *
+ * First, it checks for a s-maxage directive, then a max-age directive, and then it falls
+ * back on an expires header. It returns null when no maximum age can be established.
+ *
+ * @final
+ */
+ public function getMaxAge(): ?int
+ {
+ if ($this->headers->hasCacheControlDirective('s-maxage')) {
+ return (int) $this->headers->getCacheControlDirective('s-maxage');
+ }
+
+ if ($this->headers->hasCacheControlDirective('max-age')) {
+ return (int) $this->headers->getCacheControlDirective('max-age');
+ }
+
+ if (null !== $this->getExpires()) {
+ return (int) $this->getExpires()->format('U') - (int) $this->getDate()->format('U');
+ }
+
+ return null;
+ }
+
+ /**
+ * Sets the number of seconds after which the response should no longer be considered fresh.
+ *
+ * This methods sets the Cache-Control max-age directive.
+ *
+ * @return $this
+ *
+ * @final
+ */
+ public function setMaxAge(int $value): object
+ {
+ $this->headers->addCacheControlDirective('max-age', $value);
+
+ return $this;
+ }
+
+ /**
+ * Sets the number of seconds after which the response should no longer be considered fresh by shared caches.
+ *
+ * This methods sets the Cache-Control s-maxage directive.
+ *
+ * @return $this
+ *
+ * @final
+ */
+ public function setSharedMaxAge(int $value): object
+ {
+ $this->setPublic();
+ $this->headers->addCacheControlDirective('s-maxage', $value);
+
+ return $this;
+ }
+
+ /**
+ * Returns the response's time-to-live in seconds.
+ *
+ * It returns null when no freshness information is present in the response.
+ *
+ * When the responses TTL is <= 0, the response may not be served from cache without first
+ * revalidating with the origin.
+ *
+ * @final
+ */
+ public function getTtl(): ?int
+ {
+ $maxAge = $this->getMaxAge();
+
+ return null !== $maxAge ? $maxAge - $this->getAge() : null;
+ }
+
+ /**
+ * Sets the response's time-to-live for shared caches in seconds.
+ *
+ * This method adjusts the Cache-Control/s-maxage directive.
+ *
+ * @return $this
+ *
+ * @final
+ */
+ public function setTtl(int $seconds): object
+ {
+ $this->setSharedMaxAge($this->getAge() + $seconds);
+
+ return $this;
+ }
+
+ /**
+ * Sets the response's time-to-live for private/client caches in seconds.
+ *
+ * This method adjusts the Cache-Control/max-age directive.
+ *
+ * @return $this
+ *
+ * @final
+ */
+ public function setClientTtl(int $seconds): object
+ {
+ $this->setMaxAge($this->getAge() + $seconds);
+
+ return $this;
+ }
+
+ /**
+ * Returns the Last-Modified HTTP header as a DateTime instance.
+ *
+ * @throws \RuntimeException When the HTTP header is not parseable
+ *
+ * @final
+ */
+ public function getLastModified(): ?\DateTimeInterface
+ {
+ return $this->headers->getDate('Last-Modified');
+ }
+
+ /**
+ * Sets the Last-Modified HTTP header with a DateTime instance.
+ *
+ * Passing null as value will remove the header.
+ *
+ * @return $this
+ *
+ * @final
+ */
+ public function setLastModified(\DateTimeInterface $date = null): object
+ {
+ if (null === $date) {
+ $this->headers->remove('Last-Modified');
+
+ return $this;
+ }
+
+ if ($date instanceof \DateTime) {
+ $date = \DateTimeImmutable::createFromMutable($date);
+ }
+
+ $date = $date->setTimezone(new \DateTimeZone('UTC'));
+ $this->headers->set('Last-Modified', $date->format('D, d M Y H:i:s').' GMT');
+
+ return $this;
+ }
+
+ /**
+ * Returns the literal value of the ETag HTTP header.
+ *
+ * @final
+ */
+ public function getEtag(): ?string
+ {
+ return $this->headers->get('ETag');
+ }
+
+ /**
+ * Sets the ETag value.
+ *
+ * @param string|null $etag The ETag unique identifier or null to remove the header
+ * @param bool $weak Whether you want a weak ETag or not
+ *
+ * @return $this
+ *
+ * @final
+ */
+ public function setEtag(string $etag = null, bool $weak = false): object
+ {
+ if (null === $etag) {
+ $this->headers->remove('Etag');
+ } else {
+ if (!str_starts_with($etag, '"')) {
+ $etag = '"'.$etag.'"';
+ }
+
+ $this->headers->set('ETag', (true === $weak ? 'W/' : '').$etag);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Sets the response's cache headers (validation and/or expiration).
+ *
+ * Available options are: must_revalidate, no_cache, no_store, no_transform, public, private, proxy_revalidate, max_age, s_maxage, immutable, last_modified and etag.
+ *
+ * @return $this
+ *
+ * @throws \InvalidArgumentException
+ *
+ * @final
+ */
+ public function setCache(array $options): object
+ {
+ if ($diff = array_diff(array_keys($options), array_keys(self::HTTP_RESPONSE_CACHE_CONTROL_DIRECTIVES))) {
+ throw new \InvalidArgumentException(sprintf('Response does not support the following options: "%s".', implode('", "', $diff)));
+ }
+
+ if (isset($options['etag'])) {
+ $this->setEtag($options['etag']);
+ }
+
+ if (isset($options['last_modified'])) {
+ $this->setLastModified($options['last_modified']);
+ }
+
+ if (isset($options['max_age'])) {
+ $this->setMaxAge($options['max_age']);
+ }
+
+ if (isset($options['s_maxage'])) {
+ $this->setSharedMaxAge($options['s_maxage']);
+ }
+
+ foreach (self::HTTP_RESPONSE_CACHE_CONTROL_DIRECTIVES as $directive => $hasValue) {
+ if (!$hasValue && isset($options[$directive])) {
+ if ($options[$directive]) {
+ $this->headers->addCacheControlDirective(str_replace('_', '-', $directive));
+ } else {
+ $this->headers->removeCacheControlDirective(str_replace('_', '-', $directive));
+ }
+ }
+ }
+
+ if (isset($options['public'])) {
+ if ($options['public']) {
+ $this->setPublic();
+ } else {
+ $this->setPrivate();
+ }
+ }
+
+ if (isset($options['private'])) {
+ if ($options['private']) {
+ $this->setPrivate();
+ } else {
+ $this->setPublic();
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Modifies the response so that it conforms to the rules defined for a 304 status code.
+ *
+ * This sets the status, removes the body, and discards any headers
+ * that MUST NOT be included in 304 responses.
+ *
+ * @return $this
+ *
+ * @see https://tools.ietf.org/html/rfc2616#section-10.3.5
+ *
+ * @final
+ */
+ public function setNotModified(): object
+ {
+ $this->setStatusCode(304);
+ $this->setContent(null);
+
+ // remove headers that MUST NOT be included with 304 Not Modified responses
+ foreach (['Allow', 'Content-Encoding', 'Content-Language', 'Content-Length', 'Content-MD5', 'Content-Type', 'Last-Modified'] as $header) {
+ $this->headers->remove($header);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Returns true if the response includes a Vary header.
+ *
+ * @final
+ */
+ public function hasVary(): bool
+ {
+ return null !== $this->headers->get('Vary');
+ }
+
+ /**
+ * Returns an array of header names given in the Vary header.
+ *
+ * @final
+ */
+ public function getVary(): array
+ {
+ if (!$vary = $this->headers->all('Vary')) {
+ return [];
+ }
+
+ $ret = [];
+ foreach ($vary as $item) {
+ $ret[] = preg_split('/[\s,]+/', $item);
+ }
+
+ return array_merge([], ...$ret);
+ }
+
+ /**
+ * Sets the Vary header.
+ *
+ * @param string|array $headers
+ * @param bool $replace Whether to replace the actual value or not (true by default)
+ *
+ * @return $this
+ *
+ * @final
+ */
+ public function setVary($headers, bool $replace = true): object
+ {
+ $this->headers->set('Vary', $headers, $replace);
+
+ return $this;
+ }
+
+ /**
+ * Determines if the Response validators (ETag, Last-Modified) match
+ * a conditional value specified in the Request.
+ *
+ * If the Response is not modified, it sets the status code to 304 and
+ * removes the actual content by calling the setNotModified() method.
+ *
+ * @final
+ */
+ public function isNotModified(Request $request): bool
+ {
+ if (!$request->isMethodCacheable()) {
+ return false;
+ }
+
+ $notModified = false;
+ $lastModified = $this->headers->get('Last-Modified');
+ $modifiedSince = $request->headers->get('If-Modified-Since');
+
+ if (($ifNoneMatchEtags = $request->getETags()) && (null !== $etag = $this->getEtag())) {
+ if (0 == strncmp($etag, 'W/', 2)) {
+ $etag = substr($etag, 2);
+ }
+
+ // Use weak comparison as per https://tools.ietf.org/html/rfc7232#section-3.2.
+ foreach ($ifNoneMatchEtags as $ifNoneMatchEtag) {
+ if (0 == strncmp($ifNoneMatchEtag, 'W/', 2)) {
+ $ifNoneMatchEtag = substr($ifNoneMatchEtag, 2);
+ }
+
+ if ($ifNoneMatchEtag === $etag || '*' === $ifNoneMatchEtag) {
+ $notModified = true;
+ break;
+ }
+ }
+ }
+ // Only do If-Modified-Since date comparison when If-None-Match is not present as per https://tools.ietf.org/html/rfc7232#section-3.3.
+ elseif ($modifiedSince && $lastModified) {
+ $notModified = strtotime($modifiedSince) >= strtotime($lastModified);
+ }
+
+ if ($notModified) {
+ $this->setNotModified();
+ }
+
+ return $notModified;
+ }
+
+ /**
+ * Is response invalid?
+ *
+ * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
+ *
+ * @final
+ */
+ public function isInvalid(): bool
+ {
+ return $this->statusCode < 100 || $this->statusCode >= 600;
+ }
+
+ /**
+ * Is response informative?
+ *
+ * @final
+ */
+ public function isInformational(): bool
+ {
+ return $this->statusCode >= 100 && $this->statusCode < 200;
+ }
+
+ /**
+ * Is response successful?
+ *
+ * @final
+ */
+ public function isSuccessful(): bool
+ {
+ return $this->statusCode >= 200 && $this->statusCode < 300;
+ }
+
+ /**
+ * Is the response a redirect?
+ *
+ * @final
+ */
+ public function isRedirection(): bool
+ {
+ return $this->statusCode >= 300 && $this->statusCode < 400;
+ }
+
+ /**
+ * Is there a client error?
+ *
+ * @final
+ */
+ public function isClientError(): bool
+ {
+ return $this->statusCode >= 400 && $this->statusCode < 500;
+ }
+
+ /**
+ * Was there a server side error?
+ *
+ * @final
+ */
+ public function isServerError(): bool
+ {
+ return $this->statusCode >= 500 && $this->statusCode < 600;
+ }
+
+ /**
+ * Is the response OK?
+ *
+ * @final
+ */
+ public function isOk(): bool
+ {
+ return 200 === $this->statusCode;
+ }
+
+ /**
+ * Is the response forbidden?
+ *
+ * @final
+ */
+ public function isForbidden(): bool
+ {
+ return 403 === $this->statusCode;
+ }
+
+ /**
+ * Is the response a not found error?
+ *
+ * @final
+ */
+ public function isNotFound(): bool
+ {
+ return 404 === $this->statusCode;
+ }
+
+ /**
+ * Is the response a redirect of some form?
+ *
+ * @final
+ */
+ public function isRedirect(string $location = null): bool
+ {
+ return \in_array($this->statusCode, [201, 301, 302, 303, 307, 308]) && (null === $location ?: $location == $this->headers->get('Location'));
+ }
+
+ /**
+ * Is the response empty?
+ *
+ * @final
+ */
+ public function isEmpty(): bool
+ {
+ return \in_array($this->statusCode, [204, 304]);
+ }
+
+ /**
+ * Cleans or flushes output buffers up to target level.
+ *
+ * Resulting level can be greater than target level if a non-removable buffer has been encountered.
+ *
+ * @final
+ */
+ public static function closeOutputBuffers(int $targetLevel, bool $flush): void
+ {
+ $status = ob_get_status(true);
+ $level = \count($status);
+ $flags = \PHP_OUTPUT_HANDLER_REMOVABLE | ($flush ? \PHP_OUTPUT_HANDLER_FLUSHABLE : \PHP_OUTPUT_HANDLER_CLEANABLE);
+
+ while ($level-- > $targetLevel && ($s = $status[$level]) && (!isset($s['del']) ? !isset($s['flags']) || ($s['flags'] & $flags) === $flags : $s['del'])) {
+ if ($flush) {
+ ob_end_flush();
+ } else {
+ ob_end_clean();
+ }
+ }
+ }
+
+ /**
+ * Marks a response as safe according to RFC8674.
+ *
+ * @see https://tools.ietf.org/html/rfc8674
+ */
+ public function setContentSafe(bool $safe = true): void
+ {
+ if ($safe) {
+ $this->headers->set('Preference-Applied', 'safe');
+ } elseif ('safe' === $this->headers->get('Preference-Applied')) {
+ $this->headers->remove('Preference-Applied');
+ }
+
+ $this->setVary('Prefer', false);
+ }
+
+ /**
+ * Checks if we need to remove Cache-Control for SSL encrypted downloads when using IE < 9.
+ *
+ * @see http://support.microsoft.com/kb/323308
+ *
+ * @final
+ */
+ protected function ensureIEOverSSLCompatibility(Request $request): void
+ {
+ if (false !== stripos($this->headers->get('Content-Disposition') ?? '', 'attachment') && 1 == preg_match('/MSIE (.*?);/i', $request->server->get('HTTP_USER_AGENT') ?? '', $match) && true === $request->isSecure()) {
+ if ((int) preg_replace('/(MSIE )(.*?);/', '$2', $match[0]) < 9) {
+ $this->headers->remove('Cache-Control');
+ }
+ }
+ }
+}
diff --git a/symfony/http-foundation/ResponseHeaderBag.php b/symfony/http-foundation/ResponseHeaderBag.php
new file mode 100644
index 00000000..1df13fa2
--- /dev/null
+++ b/symfony/http-foundation/ResponseHeaderBag.php
@@ -0,0 +1,291 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation;
+
+/**
+ * ResponseHeaderBag is a container for Response HTTP headers.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class ResponseHeaderBag extends HeaderBag
+{
+ public const COOKIES_FLAT = 'flat';
+ public const COOKIES_ARRAY = 'array';
+
+ public const DISPOSITION_ATTACHMENT = 'attachment';
+ public const DISPOSITION_INLINE = 'inline';
+
+ protected $computedCacheControl = [];
+ protected $cookies = [];
+ protected $headerNames = [];
+
+ public function __construct(array $headers = [])
+ {
+ parent::__construct($headers);
+
+ if (!isset($this->headers['cache-control'])) {
+ $this->set('Cache-Control', '');
+ }
+
+ /* RFC2616 - 14.18 says all Responses need to have a Date */
+ if (!isset($this->headers['date'])) {
+ $this->initDate();
+ }
+ }
+
+ /**
+ * Returns the headers, with original capitalizations.
+ *
+ * @return array
+ */
+ public function allPreserveCase()
+ {
+ $headers = [];
+ foreach ($this->all() as $name => $value) {
+ $headers[$this->headerNames[$name] ?? $name] = $value;
+ }
+
+ return $headers;
+ }
+
+ public function allPreserveCaseWithoutCookies()
+ {
+ $headers = $this->allPreserveCase();
+ if (isset($this->headerNames['set-cookie'])) {
+ unset($headers[$this->headerNames['set-cookie']]);
+ }
+
+ return $headers;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function replace(array $headers = [])
+ {
+ $this->headerNames = [];
+
+ parent::replace($headers);
+
+ if (!isset($this->headers['cache-control'])) {
+ $this->set('Cache-Control', '');
+ }
+
+ if (!isset($this->headers['date'])) {
+ $this->initDate();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function all(string $key = null)
+ {
+ $headers = parent::all();
+
+ if (null !== $key) {
+ $key = strtr($key, self::UPPER, self::LOWER);
+
+ return 'set-cookie' !== $key ? $headers[$key] ?? [] : array_map('strval', $this->getCookies());
+ }
+
+ foreach ($this->getCookies() as $cookie) {
+ $headers['set-cookie'][] = (string) $cookie;
+ }
+
+ return $headers;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set(string $key, $values, bool $replace = true)
+ {
+ $uniqueKey = strtr($key, self::UPPER, self::LOWER);
+
+ if ('set-cookie' === $uniqueKey) {
+ if ($replace) {
+ $this->cookies = [];
+ }
+ foreach ((array) $values as $cookie) {
+ $this->setCookie(Cookie::fromString($cookie));
+ }
+ $this->headerNames[$uniqueKey] = $key;
+
+ return;
+ }
+
+ $this->headerNames[$uniqueKey] = $key;
+
+ parent::set($key, $values, $replace);
+
+ // ensure the cache-control header has sensible defaults
+ if (\in_array($uniqueKey, ['cache-control', 'etag', 'last-modified', 'expires'], true) && '' !== $computed = $this->computeCacheControlValue()) {
+ $this->headers['cache-control'] = [$computed];
+ $this->headerNames['cache-control'] = 'Cache-Control';
+ $this->computedCacheControl = $this->parseCacheControl($computed);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function remove(string $key)
+ {
+ $uniqueKey = strtr($key, self::UPPER, self::LOWER);
+ unset($this->headerNames[$uniqueKey]);
+
+ if ('set-cookie' === $uniqueKey) {
+ $this->cookies = [];
+
+ return;
+ }
+
+ parent::remove($key);
+
+ if ('cache-control' === $uniqueKey) {
+ $this->computedCacheControl = [];
+ }
+
+ if ('date' === $uniqueKey) {
+ $this->initDate();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasCacheControlDirective(string $key)
+ {
+ return \array_key_exists($key, $this->computedCacheControl);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCacheControlDirective(string $key)
+ {
+ return $this->computedCacheControl[$key] ?? null;
+ }
+
+ public function setCookie(Cookie $cookie)
+ {
+ $this->cookies[$cookie->getDomain()][$cookie->getPath()][$cookie->getName()] = $cookie;
+ $this->headerNames['set-cookie'] = 'Set-Cookie';
+ }
+
+ /**
+ * Removes a cookie from the array, but does not unset it in the browser.
+ */
+ public function removeCookie(string $name, ?string $path = '/', string $domain = null)
+ {
+ if (null === $path) {
+ $path = '/';
+ }
+
+ unset($this->cookies[$domain][$path][$name]);
+
+ if (empty($this->cookies[$domain][$path])) {
+ unset($this->cookies[$domain][$path]);
+
+ if (empty($this->cookies[$domain])) {
+ unset($this->cookies[$domain]);
+ }
+ }
+
+ if (empty($this->cookies)) {
+ unset($this->headerNames['set-cookie']);
+ }
+ }
+
+ /**
+ * Returns an array with all cookies.
+ *
+ * @return Cookie[]
+ *
+ * @throws \InvalidArgumentException When the $format is invalid
+ */
+ public function getCookies(string $format = self::COOKIES_FLAT)
+ {
+ if (!\in_array($format, [self::COOKIES_FLAT, self::COOKIES_ARRAY])) {
+ throw new \InvalidArgumentException(sprintf('Format "%s" invalid (%s).', $format, implode(', ', [self::COOKIES_FLAT, self::COOKIES_ARRAY])));
+ }
+
+ if (self::COOKIES_ARRAY === $format) {
+ return $this->cookies;
+ }
+
+ $flattenedCookies = [];
+ foreach ($this->cookies as $path) {
+ foreach ($path as $cookies) {
+ foreach ($cookies as $cookie) {
+ $flattenedCookies[] = $cookie;
+ }
+ }
+ }
+
+ return $flattenedCookies;
+ }
+
+ /**
+ * Clears a cookie in the browser.
+ */
+ public function clearCookie(string $name, ?string $path = '/', string $domain = null, bool $secure = false, bool $httpOnly = true, string $sameSite = null)
+ {
+ $this->setCookie(new Cookie($name, null, 1, $path, $domain, $secure, $httpOnly, false, $sameSite));
+ }
+
+ /**
+ * @see HeaderUtils::makeDisposition()
+ */
+ public function makeDisposition(string $disposition, string $filename, string $filenameFallback = '')
+ {
+ return HeaderUtils::makeDisposition($disposition, $filename, $filenameFallback);
+ }
+
+ /**
+ * Returns the calculated value of the cache-control header.
+ *
+ * This considers several other headers and calculates or modifies the
+ * cache-control header to a sensible, conservative value.
+ *
+ * @return string
+ */
+ protected function computeCacheControlValue()
+ {
+ if (!$this->cacheControl) {
+ if ($this->has('Last-Modified') || $this->has('Expires')) {
+ return 'private, must-revalidate'; // allows for heuristic expiration (RFC 7234 Section 4.2.2) in the case of "Last-Modified"
+ }
+
+ // conservative by default
+ return 'no-cache, private';
+ }
+
+ $header = $this->getCacheControlHeader();
+ if (isset($this->cacheControl['public']) || isset($this->cacheControl['private'])) {
+ return $header;
+ }
+
+ // public if s-maxage is defined, private otherwise
+ if (!isset($this->cacheControl['s-maxage'])) {
+ return $header.', private';
+ }
+
+ return $header;
+ }
+
+ private function initDate(): void
+ {
+ $this->set('Date', gmdate('D, d M Y H:i:s').' GMT');
+ }
+}
diff --git a/symfony/http-foundation/ServerBag.php b/symfony/http-foundation/ServerBag.php
new file mode 100644
index 00000000..25688d52
--- /dev/null
+++ b/symfony/http-foundation/ServerBag.php
@@ -0,0 +1,99 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation;
+
+/**
+ * ServerBag is a container for HTTP headers from the $_SERVER variable.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Bulat Shakirzyanov <mallluhuct@gmail.com>
+ * @author Robert Kiss <kepten@gmail.com>
+ */
+class ServerBag extends ParameterBag
+{
+ /**
+ * Gets the HTTP headers.
+ *
+ * @return array
+ */
+ public function getHeaders()
+ {
+ $headers = [];
+ foreach ($this->parameters as $key => $value) {
+ if (str_starts_with($key, 'HTTP_')) {
+ $headers[substr($key, 5)] = $value;
+ } elseif (\in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH', 'CONTENT_MD5'], true)) {
+ $headers[$key] = $value;
+ }
+ }
+
+ if (isset($this->parameters['PHP_AUTH_USER'])) {
+ $headers['PHP_AUTH_USER'] = $this->parameters['PHP_AUTH_USER'];
+ $headers['PHP_AUTH_PW'] = $this->parameters['PHP_AUTH_PW'] ?? '';
+ } else {
+ /*
+ * php-cgi under Apache does not pass HTTP Basic user/pass to PHP by default
+ * For this workaround to work, add these lines to your .htaccess file:
+ * RewriteCond %{HTTP:Authorization} .+
+ * RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0]
+ *
+ * A sample .htaccess file:
+ * RewriteEngine On
+ * RewriteCond %{HTTP:Authorization} .+
+ * RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0]
+ * RewriteCond %{REQUEST_FILENAME} !-f
+ * RewriteRule ^(.*)$ app.php [QSA,L]
+ */
+
+ $authorizationHeader = null;
+ if (isset($this->parameters['HTTP_AUTHORIZATION'])) {
+ $authorizationHeader = $this->parameters['HTTP_AUTHORIZATION'];
+ } elseif (isset($this->parameters['REDIRECT_HTTP_AUTHORIZATION'])) {
+ $authorizationHeader = $this->parameters['REDIRECT_HTTP_AUTHORIZATION'];
+ }
+
+ if (null !== $authorizationHeader) {
+ if (0 === stripos($authorizationHeader, 'basic ')) {
+ // Decode AUTHORIZATION header into PHP_AUTH_USER and PHP_AUTH_PW when authorization header is basic
+ $exploded = explode(':', base64_decode(substr($authorizationHeader, 6)), 2);
+ if (2 == \count($exploded)) {
+ [$headers['PHP_AUTH_USER'], $headers['PHP_AUTH_PW']] = $exploded;
+ }
+ } elseif (empty($this->parameters['PHP_AUTH_DIGEST']) && (0 === stripos($authorizationHeader, 'digest '))) {
+ // In some circumstances PHP_AUTH_DIGEST needs to be set
+ $headers['PHP_AUTH_DIGEST'] = $authorizationHeader;
+ $this->parameters['PHP_AUTH_DIGEST'] = $authorizationHeader;
+ } elseif (0 === stripos($authorizationHeader, 'bearer ')) {
+ /*
+ * XXX: Since there is no PHP_AUTH_BEARER in PHP predefined variables,
+ * I'll just set $headers['AUTHORIZATION'] here.
+ * https://php.net/reserved.variables.server
+ */
+ $headers['AUTHORIZATION'] = $authorizationHeader;
+ }
+ }
+ }
+
+ if (isset($headers['AUTHORIZATION'])) {
+ return $headers;
+ }
+
+ // PHP_AUTH_USER/PHP_AUTH_PW
+ if (isset($headers['PHP_AUTH_USER'])) {
+ $headers['AUTHORIZATION'] = 'Basic '.base64_encode($headers['PHP_AUTH_USER'].':'.($headers['PHP_AUTH_PW'] ?? ''));
+ } elseif (isset($headers['PHP_AUTH_DIGEST'])) {
+ $headers['AUTHORIZATION'] = $headers['PHP_AUTH_DIGEST'];
+ }
+
+ return $headers;
+ }
+}
diff --git a/symfony/http-foundation/Session/Attribute/AttributeBag.php b/symfony/http-foundation/Session/Attribute/AttributeBag.php
new file mode 100644
index 00000000..f4f051c7
--- /dev/null
+++ b/symfony/http-foundation/Session/Attribute/AttributeBag.php
@@ -0,0 +1,152 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Attribute;
+
+/**
+ * This class relates to session attribute storage.
+ *
+ * @implements \IteratorAggregate<string, mixed>
+ */
+class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Countable
+{
+ private $name = 'attributes';
+ private $storageKey;
+
+ protected $attributes = [];
+
+ /**
+ * @param string $storageKey The key used to store attributes in the session
+ */
+ public function __construct(string $storageKey = '_sf2_attributes')
+ {
+ $this->storageKey = $storageKey;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ public function setName(string $name)
+ {
+ $this->name = $name;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function initialize(array &$attributes)
+ {
+ $this->attributes = &$attributes;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStorageKey()
+ {
+ return $this->storageKey;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function has(string $name)
+ {
+ return \array_key_exists($name, $this->attributes);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get(string $name, $default = null)
+ {
+ return \array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set(string $name, $value)
+ {
+ $this->attributes[$name] = $value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function all()
+ {
+ return $this->attributes;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function replace(array $attributes)
+ {
+ $this->attributes = [];
+ foreach ($attributes as $key => $value) {
+ $this->set($key, $value);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function remove(string $name)
+ {
+ $retval = null;
+ if (\array_key_exists($name, $this->attributes)) {
+ $retval = $this->attributes[$name];
+ unset($this->attributes[$name]);
+ }
+
+ return $retval;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function clear()
+ {
+ $return = $this->attributes;
+ $this->attributes = [];
+
+ return $return;
+ }
+
+ /**
+ * Returns an iterator for attributes.
+ *
+ * @return \ArrayIterator<string, mixed>
+ */
+ #[\ReturnTypeWillChange]
+ public function getIterator()
+ {
+ return new \ArrayIterator($this->attributes);
+ }
+
+ /**
+ * Returns the number of attributes.
+ *
+ * @return int
+ */
+ #[\ReturnTypeWillChange]
+ public function count()
+ {
+ return \count($this->attributes);
+ }
+}
diff --git a/symfony/http-foundation/Session/Attribute/AttributeBagInterface.php b/symfony/http-foundation/Session/Attribute/AttributeBagInterface.php
new file mode 100644
index 00000000..cb506968
--- /dev/null
+++ b/symfony/http-foundation/Session/Attribute/AttributeBagInterface.php
@@ -0,0 +1,61 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Attribute;
+
+use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
+
+/**
+ * Attributes store.
+ *
+ * @author Drak <drak@zikula.org>
+ */
+interface AttributeBagInterface extends SessionBagInterface
+{
+ /**
+ * Checks if an attribute is defined.
+ *
+ * @return bool
+ */
+ public function has(string $name);
+
+ /**
+ * Returns an attribute.
+ *
+ * @param mixed $default The default value if not found
+ *
+ * @return mixed
+ */
+ public function get(string $name, $default = null);
+
+ /**
+ * Sets an attribute.
+ *
+ * @param mixed $value
+ */
+ public function set(string $name, $value);
+
+ /**
+ * Returns attributes.
+ *
+ * @return array<string, mixed>
+ */
+ public function all();
+
+ public function replace(array $attributes);
+
+ /**
+ * Removes an attribute.
+ *
+ * @return mixed The removed value or null when it does not exist
+ */
+ public function remove(string $name);
+}
diff --git a/symfony/http-foundation/Session/Attribute/NamespacedAttributeBag.php b/symfony/http-foundation/Session/Attribute/NamespacedAttributeBag.php
new file mode 100644
index 00000000..864b35fb
--- /dev/null
+++ b/symfony/http-foundation/Session/Attribute/NamespacedAttributeBag.php
@@ -0,0 +1,161 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Attribute;
+
+trigger_deprecation('symfony/http-foundation', '5.3', 'The "%s" class is deprecated.', NamespacedAttributeBag::class);
+
+/**
+ * This class provides structured storage of session attributes using
+ * a name spacing character in the key.
+ *
+ * @author Drak <drak@zikula.org>
+ *
+ * @deprecated since Symfony 5.3
+ */
+class NamespacedAttributeBag extends AttributeBag
+{
+ private $namespaceCharacter;
+
+ /**
+ * @param string $storageKey Session storage key
+ * @param string $namespaceCharacter Namespace character to use in keys
+ */
+ public function __construct(string $storageKey = '_sf2_attributes', string $namespaceCharacter = '/')
+ {
+ $this->namespaceCharacter = $namespaceCharacter;
+ parent::__construct($storageKey);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function has(string $name)
+ {
+ // reference mismatch: if fixed, re-introduced in array_key_exists; keep as it is
+ $attributes = $this->resolveAttributePath($name);
+ $name = $this->resolveKey($name);
+
+ if (null === $attributes) {
+ return false;
+ }
+
+ return \array_key_exists($name, $attributes);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get(string $name, $default = null)
+ {
+ // reference mismatch: if fixed, re-introduced in array_key_exists; keep as it is
+ $attributes = $this->resolveAttributePath($name);
+ $name = $this->resolveKey($name);
+
+ if (null === $attributes) {
+ return $default;
+ }
+
+ return \array_key_exists($name, $attributes) ? $attributes[$name] : $default;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set(string $name, $value)
+ {
+ $attributes = &$this->resolveAttributePath($name, true);
+ $name = $this->resolveKey($name);
+ $attributes[$name] = $value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function remove(string $name)
+ {
+ $retval = null;
+ $attributes = &$this->resolveAttributePath($name);
+ $name = $this->resolveKey($name);
+ if (null !== $attributes && \array_key_exists($name, $attributes)) {
+ $retval = $attributes[$name];
+ unset($attributes[$name]);
+ }
+
+ return $retval;
+ }
+
+ /**
+ * Resolves a path in attributes property and returns it as a reference.
+ *
+ * This method allows structured namespacing of session attributes.
+ *
+ * @param string $name Key name
+ * @param bool $writeContext Write context, default false
+ *
+ * @return array|null
+ */
+ protected function &resolveAttributePath(string $name, bool $writeContext = false)
+ {
+ $array = &$this->attributes;
+ $name = (str_starts_with($name, $this->namespaceCharacter)) ? substr($name, 1) : $name;
+
+ // Check if there is anything to do, else return
+ if (!$name) {
+ return $array;
+ }
+
+ $parts = explode($this->namespaceCharacter, $name);
+ if (\count($parts) < 2) {
+ if (!$writeContext) {
+ return $array;
+ }
+
+ $array[$parts[0]] = [];
+
+ return $array;
+ }
+
+ unset($parts[\count($parts) - 1]);
+
+ foreach ($parts as $part) {
+ if (null !== $array && !\array_key_exists($part, $array)) {
+ if (!$writeContext) {
+ $null = null;
+
+ return $null;
+ }
+
+ $array[$part] = [];
+ }
+
+ $array = &$array[$part];
+ }
+
+ return $array;
+ }
+
+ /**
+ * Resolves the key from the name.
+ *
+ * This is the last part in a dot separated string.
+ *
+ * @return string
+ */
+ protected function resolveKey(string $name)
+ {
+ if (false !== $pos = strrpos($name, $this->namespaceCharacter)) {
+ $name = substr($name, $pos + 1);
+ }
+
+ return $name;
+ }
+}
diff --git a/symfony/http-foundation/Session/Flash/AutoExpireFlashBag.php b/symfony/http-foundation/Session/Flash/AutoExpireFlashBag.php
new file mode 100644
index 00000000..8aab3a12
--- /dev/null
+++ b/symfony/http-foundation/Session/Flash/AutoExpireFlashBag.php
@@ -0,0 +1,161 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Flash;
+
+/**
+ * AutoExpireFlashBag flash message container.
+ *
+ * @author Drak <drak@zikula.org>
+ */
+class AutoExpireFlashBag implements FlashBagInterface
+{
+ private $name = 'flashes';
+ private $flashes = ['display' => [], 'new' => []];
+ private $storageKey;
+
+ /**
+ * @param string $storageKey The key used to store flashes in the session
+ */
+ public function __construct(string $storageKey = '_symfony_flashes')
+ {
+ $this->storageKey = $storageKey;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ public function setName(string $name)
+ {
+ $this->name = $name;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function initialize(array &$flashes)
+ {
+ $this->flashes = &$flashes;
+
+ // The logic: messages from the last request will be stored in new, so we move them to previous
+ // This request we will show what is in 'display'. What is placed into 'new' this time round will
+ // be moved to display next time round.
+ $this->flashes['display'] = \array_key_exists('new', $this->flashes) ? $this->flashes['new'] : [];
+ $this->flashes['new'] = [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function add(string $type, $message)
+ {
+ $this->flashes['new'][$type][] = $message;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function peek(string $type, array $default = [])
+ {
+ return $this->has($type) ? $this->flashes['display'][$type] : $default;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function peekAll()
+ {
+ return \array_key_exists('display', $this->flashes) ? $this->flashes['display'] : [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get(string $type, array $default = [])
+ {
+ $return = $default;
+
+ if (!$this->has($type)) {
+ return $return;
+ }
+
+ if (isset($this->flashes['display'][$type])) {
+ $return = $this->flashes['display'][$type];
+ unset($this->flashes['display'][$type]);
+ }
+
+ return $return;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function all()
+ {
+ $return = $this->flashes['display'];
+ $this->flashes['display'] = [];
+
+ return $return;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setAll(array $messages)
+ {
+ $this->flashes['new'] = $messages;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set(string $type, $messages)
+ {
+ $this->flashes['new'][$type] = (array) $messages;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function has(string $type)
+ {
+ return \array_key_exists($type, $this->flashes['display']) && $this->flashes['display'][$type];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function keys()
+ {
+ return array_keys($this->flashes['display']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStorageKey()
+ {
+ return $this->storageKey;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function clear()
+ {
+ return $this->all();
+ }
+}
diff --git a/symfony/http-foundation/Session/Flash/FlashBag.php b/symfony/http-foundation/Session/Flash/FlashBag.php
new file mode 100644
index 00000000..88df7508
--- /dev/null
+++ b/symfony/http-foundation/Session/Flash/FlashBag.php
@@ -0,0 +1,152 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Flash;
+
+/**
+ * FlashBag flash message container.
+ *
+ * @author Drak <drak@zikula.org>
+ */
+class FlashBag implements FlashBagInterface
+{
+ private $name = 'flashes';
+ private $flashes = [];
+ private $storageKey;
+
+ /**
+ * @param string $storageKey The key used to store flashes in the session
+ */
+ public function __construct(string $storageKey = '_symfony_flashes')
+ {
+ $this->storageKey = $storageKey;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ public function setName(string $name)
+ {
+ $this->name = $name;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function initialize(array &$flashes)
+ {
+ $this->flashes = &$flashes;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function add(string $type, $message)
+ {
+ $this->flashes[$type][] = $message;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function peek(string $type, array $default = [])
+ {
+ return $this->has($type) ? $this->flashes[$type] : $default;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function peekAll()
+ {
+ return $this->flashes;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get(string $type, array $default = [])
+ {
+ if (!$this->has($type)) {
+ return $default;
+ }
+
+ $return = $this->flashes[$type];
+
+ unset($this->flashes[$type]);
+
+ return $return;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function all()
+ {
+ $return = $this->peekAll();
+ $this->flashes = [];
+
+ return $return;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set(string $type, $messages)
+ {
+ $this->flashes[$type] = (array) $messages;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setAll(array $messages)
+ {
+ $this->flashes = $messages;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function has(string $type)
+ {
+ return \array_key_exists($type, $this->flashes) && $this->flashes[$type];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function keys()
+ {
+ return array_keys($this->flashes);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStorageKey()
+ {
+ return $this->storageKey;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function clear()
+ {
+ return $this->all();
+ }
+}
diff --git a/symfony/http-foundation/Session/Flash/FlashBagInterface.php b/symfony/http-foundation/Session/Flash/FlashBagInterface.php
new file mode 100644
index 00000000..8713e71d
--- /dev/null
+++ b/symfony/http-foundation/Session/Flash/FlashBagInterface.php
@@ -0,0 +1,88 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Flash;
+
+use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
+
+/**
+ * FlashBagInterface.
+ *
+ * @author Drak <drak@zikula.org>
+ */
+interface FlashBagInterface extends SessionBagInterface
+{
+ /**
+ * Adds a flash message for the given type.
+ *
+ * @param mixed $message
+ */
+ public function add(string $type, $message);
+
+ /**
+ * Registers one or more messages for a given type.
+ *
+ * @param string|array $messages
+ */
+ public function set(string $type, $messages);
+
+ /**
+ * Gets flash messages for a given type.
+ *
+ * @param string $type Message category type
+ * @param array $default Default value if $type does not exist
+ *
+ * @return array
+ */
+ public function peek(string $type, array $default = []);
+
+ /**
+ * Gets all flash messages.
+ *
+ * @return array
+ */
+ public function peekAll();
+
+ /**
+ * Gets and clears flash from the stack.
+ *
+ * @param array $default Default value if $type does not exist
+ *
+ * @return array
+ */
+ public function get(string $type, array $default = []);
+
+ /**
+ * Gets and clears flashes from the stack.
+ *
+ * @return array
+ */
+ public function all();
+
+ /**
+ * Sets all flash messages.
+ */
+ public function setAll(array $messages);
+
+ /**
+ * Has flash messages for a given type?
+ *
+ * @return bool
+ */
+ public function has(string $type);
+
+ /**
+ * Returns a list of all defined types.
+ *
+ * @return array
+ */
+ public function keys();
+}
diff --git a/symfony/http-foundation/Session/Session.php b/symfony/http-foundation/Session/Session.php
new file mode 100644
index 00000000..022e3986
--- /dev/null
+++ b/symfony/http-foundation/Session/Session.php
@@ -0,0 +1,285 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session;
+
+use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag;
+use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface;
+use Symfony\Component\HttpFoundation\Session\Flash\FlashBag;
+use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
+use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
+use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface;
+
+// Help opcache.preload discover always-needed symbols
+class_exists(AttributeBag::class);
+class_exists(FlashBag::class);
+class_exists(SessionBagProxy::class);
+
+/**
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Drak <drak@zikula.org>
+ *
+ * @implements \IteratorAggregate<string, mixed>
+ */
+class Session implements SessionInterface, \IteratorAggregate, \Countable
+{
+ protected $storage;
+
+ private $flashName;
+ private $attributeName;
+ private $data = [];
+ private $usageIndex = 0;
+ private $usageReporter;
+
+ public function __construct(SessionStorageInterface $storage = null, AttributeBagInterface $attributes = null, FlashBagInterface $flashes = null, callable $usageReporter = null)
+ {
+ $this->storage = $storage ?? new NativeSessionStorage();
+ $this->usageReporter = $usageReporter;
+
+ $attributes = $attributes ?? new AttributeBag();
+ $this->attributeName = $attributes->getName();
+ $this->registerBag($attributes);
+
+ $flashes = $flashes ?? new FlashBag();
+ $this->flashName = $flashes->getName();
+ $this->registerBag($flashes);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function start()
+ {
+ return $this->storage->start();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function has(string $name)
+ {
+ return $this->getAttributeBag()->has($name);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get(string $name, $default = null)
+ {
+ return $this->getAttributeBag()->get($name, $default);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set(string $name, $value)
+ {
+ $this->getAttributeBag()->set($name, $value);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function all()
+ {
+ return $this->getAttributeBag()->all();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function replace(array $attributes)
+ {
+ $this->getAttributeBag()->replace($attributes);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function remove(string $name)
+ {
+ return $this->getAttributeBag()->remove($name);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function clear()
+ {
+ $this->getAttributeBag()->clear();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isStarted()
+ {
+ return $this->storage->isStarted();
+ }
+
+ /**
+ * Returns an iterator for attributes.
+ *
+ * @return \ArrayIterator<string, mixed>
+ */
+ #[\ReturnTypeWillChange]
+ public function getIterator()
+ {
+ return new \ArrayIterator($this->getAttributeBag()->all());
+ }
+
+ /**
+ * Returns the number of attributes.
+ *
+ * @return int
+ */
+ #[\ReturnTypeWillChange]
+ public function count()
+ {
+ return \count($this->getAttributeBag()->all());
+ }
+
+ public function &getUsageIndex(): int
+ {
+ return $this->usageIndex;
+ }
+
+ /**
+ * @internal
+ */
+ public function isEmpty(): bool
+ {
+ if ($this->isStarted()) {
+ ++$this->usageIndex;
+ if ($this->usageReporter && 0 <= $this->usageIndex) {
+ ($this->usageReporter)();
+ }
+ }
+ foreach ($this->data as &$data) {
+ if (!empty($data)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function invalidate(int $lifetime = null)
+ {
+ $this->storage->clear();
+
+ return $this->migrate(true, $lifetime);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function migrate(bool $destroy = false, int $lifetime = null)
+ {
+ return $this->storage->regenerate($destroy, $lifetime);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save()
+ {
+ $this->storage->save();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return $this->storage->getId();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setId(string $id)
+ {
+ if ($this->storage->getId() !== $id) {
+ $this->storage->setId($id);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return $this->storage->getName();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setName(string $name)
+ {
+ $this->storage->setName($name);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getMetadataBag()
+ {
+ ++$this->usageIndex;
+ if ($this->usageReporter && 0 <= $this->usageIndex) {
+ ($this->usageReporter)();
+ }
+
+ return $this->storage->getMetadataBag();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function registerBag(SessionBagInterface $bag)
+ {
+ $this->storage->registerBag(new SessionBagProxy($bag, $this->data, $this->usageIndex, $this->usageReporter));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getBag(string $name)
+ {
+ $bag = $this->storage->getBag($name);
+
+ return method_exists($bag, 'getBag') ? $bag->getBag() : $bag;
+ }
+
+ /**
+ * Gets the flashbag interface.
+ *
+ * @return FlashBagInterface
+ */
+ public function getFlashBag()
+ {
+ return $this->getBag($this->flashName);
+ }
+
+ /**
+ * Gets the attributebag interface.
+ *
+ * Note that this method was added to help with IDE autocompletion.
+ */
+ private function getAttributeBag(): AttributeBagInterface
+ {
+ return $this->getBag($this->attributeName);
+ }
+}
diff --git a/symfony/http-foundation/Session/SessionBagInterface.php b/symfony/http-foundation/Session/SessionBagInterface.php
new file mode 100644
index 00000000..8e37d06d
--- /dev/null
+++ b/symfony/http-foundation/Session/SessionBagInterface.php
@@ -0,0 +1,46 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session;
+
+/**
+ * Session Bag store.
+ *
+ * @author Drak <drak@zikula.org>
+ */
+interface SessionBagInterface
+{
+ /**
+ * Gets this bag's name.
+ *
+ * @return string
+ */
+ public function getName();
+
+ /**
+ * Initializes the Bag.
+ */
+ public function initialize(array &$array);
+
+ /**
+ * Gets the storage key for this bag.
+ *
+ * @return string
+ */
+ public function getStorageKey();
+
+ /**
+ * Clears out data from bag.
+ *
+ * @return mixed Whatever data was contained
+ */
+ public function clear();
+}
diff --git a/symfony/http-foundation/Session/SessionBagProxy.php b/symfony/http-foundation/Session/SessionBagProxy.php
new file mode 100644
index 00000000..90aa010c
--- /dev/null
+++ b/symfony/http-foundation/Session/SessionBagProxy.php
@@ -0,0 +1,95 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session;
+
+/**
+ * @author Nicolas Grekas <p@tchwork.com>
+ *
+ * @internal
+ */
+final class SessionBagProxy implements SessionBagInterface
+{
+ private $bag;
+ private $data;
+ private $usageIndex;
+ private $usageReporter;
+
+ public function __construct(SessionBagInterface $bag, array &$data, ?int &$usageIndex, ?callable $usageReporter)
+ {
+ $this->bag = $bag;
+ $this->data = &$data;
+ $this->usageIndex = &$usageIndex;
+ $this->usageReporter = $usageReporter;
+ }
+
+ public function getBag(): SessionBagInterface
+ {
+ ++$this->usageIndex;
+ if ($this->usageReporter && 0 <= $this->usageIndex) {
+ ($this->usageReporter)();
+ }
+
+ return $this->bag;
+ }
+
+ public function isEmpty(): bool
+ {
+ if (!isset($this->data[$this->bag->getStorageKey()])) {
+ return true;
+ }
+ ++$this->usageIndex;
+ if ($this->usageReporter && 0 <= $this->usageIndex) {
+ ($this->usageReporter)();
+ }
+
+ return empty($this->data[$this->bag->getStorageKey()]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName(): string
+ {
+ return $this->bag->getName();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function initialize(array &$array): void
+ {
+ ++$this->usageIndex;
+ if ($this->usageReporter && 0 <= $this->usageIndex) {
+ ($this->usageReporter)();
+ }
+
+ $this->data[$this->bag->getStorageKey()] = &$array;
+
+ $this->bag->initialize($array);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStorageKey(): string
+ {
+ return $this->bag->getStorageKey();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function clear()
+ {
+ return $this->bag->clear();
+ }
+}
diff --git a/symfony/http-foundation/Session/SessionFactory.php b/symfony/http-foundation/Session/SessionFactory.php
new file mode 100644
index 00000000..04c4b06a
--- /dev/null
+++ b/symfony/http-foundation/Session/SessionFactory.php
@@ -0,0 +1,40 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session;
+
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageFactoryInterface;
+
+// Help opcache.preload discover always-needed symbols
+class_exists(Session::class);
+
+/**
+ * @author Jérémy Derussé <jeremy@derusse.com>
+ */
+class SessionFactory implements SessionFactoryInterface
+{
+ private $requestStack;
+ private $storageFactory;
+ private $usageReporter;
+
+ public function __construct(RequestStack $requestStack, SessionStorageFactoryInterface $storageFactory, callable $usageReporter = null)
+ {
+ $this->requestStack = $requestStack;
+ $this->storageFactory = $storageFactory;
+ $this->usageReporter = $usageReporter;
+ }
+
+ public function createSession(): SessionInterface
+ {
+ return new Session($this->storageFactory->createStorage($this->requestStack->getMainRequest()), null, null, $this->usageReporter);
+ }
+}
diff --git a/symfony/http-foundation/Session/SessionFactoryInterface.php b/symfony/http-foundation/Session/SessionFactoryInterface.php
new file mode 100644
index 00000000..b24fdc49
--- /dev/null
+++ b/symfony/http-foundation/Session/SessionFactoryInterface.php
@@ -0,0 +1,20 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session;
+
+/**
+ * @author Kevin Bond <kevinbond@gmail.com>
+ */
+interface SessionFactoryInterface
+{
+ public function createSession(): SessionInterface;
+}
diff --git a/symfony/http-foundation/Session/SessionInterface.php b/symfony/http-foundation/Session/SessionInterface.php
new file mode 100644
index 00000000..b2f09fd0
--- /dev/null
+++ b/symfony/http-foundation/Session/SessionInterface.php
@@ -0,0 +1,166 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session;
+
+use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag;
+
+/**
+ * Interface for the session.
+ *
+ * @author Drak <drak@zikula.org>
+ */
+interface SessionInterface
+{
+ /**
+ * Starts the session storage.
+ *
+ * @return bool
+ *
+ * @throws \RuntimeException if session fails to start
+ */
+ public function start();
+
+ /**
+ * Returns the session ID.
+ *
+ * @return string
+ */
+ public function getId();
+
+ /**
+ * Sets the session ID.
+ */
+ public function setId(string $id);
+
+ /**
+ * Returns the session name.
+ *
+ * @return string
+ */
+ public function getName();
+
+ /**
+ * Sets the session name.
+ */
+ public function setName(string $name);
+
+ /**
+ * Invalidates the current session.
+ *
+ * Clears all session attributes and flashes and regenerates the
+ * session and deletes the old session from persistence.
+ *
+ * @param int $lifetime Sets the cookie lifetime for the session cookie. A null value
+ * will leave the system settings unchanged, 0 sets the cookie
+ * to expire with browser session. Time is in seconds, and is
+ * not a Unix timestamp.
+ *
+ * @return bool
+ */
+ public function invalidate(int $lifetime = null);
+
+ /**
+ * Migrates the current session to a new session id while maintaining all
+ * session attributes.
+ *
+ * @param bool $destroy Whether to delete the old session or leave it to garbage collection
+ * @param int $lifetime Sets the cookie lifetime for the session cookie. A null value
+ * will leave the system settings unchanged, 0 sets the cookie
+ * to expire with browser session. Time is in seconds, and is
+ * not a Unix timestamp.
+ *
+ * @return bool
+ */
+ public function migrate(bool $destroy = false, int $lifetime = null);
+
+ /**
+ * Force the session to be saved and closed.
+ *
+ * This method is generally not required for real sessions as
+ * the session will be automatically saved at the end of
+ * code execution.
+ */
+ public function save();
+
+ /**
+ * Checks if an attribute is defined.
+ *
+ * @return bool
+ */
+ public function has(string $name);
+
+ /**
+ * Returns an attribute.
+ *
+ * @param mixed $default The default value if not found
+ *
+ * @return mixed
+ */
+ public function get(string $name, $default = null);
+
+ /**
+ * Sets an attribute.
+ *
+ * @param mixed $value
+ */
+ public function set(string $name, $value);
+
+ /**
+ * Returns attributes.
+ *
+ * @return array
+ */
+ public function all();
+
+ /**
+ * Sets attributes.
+ */
+ public function replace(array $attributes);
+
+ /**
+ * Removes an attribute.
+ *
+ * @return mixed The removed value or null when it does not exist
+ */
+ public function remove(string $name);
+
+ /**
+ * Clears all attributes.
+ */
+ public function clear();
+
+ /**
+ * Checks if the session was started.
+ *
+ * @return bool
+ */
+ public function isStarted();
+
+ /**
+ * Registers a SessionBagInterface with the session.
+ */
+ public function registerBag(SessionBagInterface $bag);
+
+ /**
+ * Gets a bag instance by name.
+ *
+ * @return SessionBagInterface
+ */
+ public function getBag(string $name);
+
+ /**
+ * Gets session meta.
+ *
+ * @return MetadataBag
+ */
+ public function getMetadataBag();
+}
diff --git a/symfony/http-foundation/Session/SessionUtils.php b/symfony/http-foundation/Session/SessionUtils.php
new file mode 100644
index 00000000..b5bce4a8
--- /dev/null
+++ b/symfony/http-foundation/Session/SessionUtils.php
@@ -0,0 +1,59 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session;
+
+/**
+ * Session utility functions.
+ *
+ * @author Nicolas Grekas <p@tchwork.com>
+ * @author Rémon van de Kamp <rpkamp@gmail.com>
+ *
+ * @internal
+ */
+final class SessionUtils
+{
+ /**
+ * Finds the session header amongst the headers that are to be sent, removes it, and returns
+ * it so the caller can process it further.
+ */
+ public static function popSessionCookie(string $sessionName, string $sessionId): ?string
+ {
+ $sessionCookie = null;
+ $sessionCookiePrefix = sprintf(' %s=', urlencode($sessionName));
+ $sessionCookieWithId = sprintf('%s%s;', $sessionCookiePrefix, urlencode($sessionId));
+ $otherCookies = [];
+ foreach (headers_list() as $h) {
+ if (0 !== stripos($h, 'Set-Cookie:')) {
+ continue;
+ }
+ if (11 === strpos($h, $sessionCookiePrefix, 11)) {
+ $sessionCookie = $h;
+
+ if (11 !== strpos($h, $sessionCookieWithId, 11)) {
+ $otherCookies[] = $h;
+ }
+ } else {
+ $otherCookies[] = $h;
+ }
+ }
+ if (null === $sessionCookie) {
+ return null;
+ }
+
+ header_remove('Set-Cookie');
+ foreach ($otherCookies as $h) {
+ header($h, false);
+ }
+
+ return $sessionCookie;
+ }
+}
diff --git a/symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php b/symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php
new file mode 100644
index 00000000..bc7b944f
--- /dev/null
+++ b/symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php
@@ -0,0 +1,155 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
+
+use Symfony\Component\HttpFoundation\Session\SessionUtils;
+
+/**
+ * This abstract session handler provides a generic implementation
+ * of the PHP 7.0 SessionUpdateTimestampHandlerInterface,
+ * enabling strict and lazy session handling.
+ *
+ * @author Nicolas Grekas <p@tchwork.com>
+ */
+abstract class AbstractSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface
+{
+ private $sessionName;
+ private $prefetchId;
+ private $prefetchData;
+ private $newSessionId;
+ private $igbinaryEmptyData;
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function open($savePath, $sessionName)
+ {
+ $this->sessionName = $sessionName;
+ if (!headers_sent() && !ini_get('session.cache_limiter') && '0' !== ini_get('session.cache_limiter')) {
+ header(sprintf('Cache-Control: max-age=%d, private, must-revalidate', 60 * (int) ini_get('session.cache_expire')));
+ }
+
+ return true;
+ }
+
+ /**
+ * @return string
+ */
+ abstract protected function doRead(string $sessionId);
+
+ /**
+ * @return bool
+ */
+ abstract protected function doWrite(string $sessionId, string $data);
+
+ /**
+ * @return bool
+ */
+ abstract protected function doDestroy(string $sessionId);
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function validateId($sessionId)
+ {
+ $this->prefetchData = $this->read($sessionId);
+ $this->prefetchId = $sessionId;
+
+ if (\PHP_VERSION_ID < 70317 || (70400 <= \PHP_VERSION_ID && \PHP_VERSION_ID < 70405)) {
+ // work around https://bugs.php.net/79413
+ foreach (debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS) as $frame) {
+ if (!isset($frame['class']) && isset($frame['function']) && \in_array($frame['function'], ['session_regenerate_id', 'session_create_id'], true)) {
+ return '' === $this->prefetchData;
+ }
+ }
+ }
+
+ return '' !== $this->prefetchData;
+ }
+
+ /**
+ * @return string
+ */
+ #[\ReturnTypeWillChange]
+ public function read($sessionId)
+ {
+ if (null !== $this->prefetchId) {
+ $prefetchId = $this->prefetchId;
+ $prefetchData = $this->prefetchData;
+ $this->prefetchId = $this->prefetchData = null;
+
+ if ($prefetchId === $sessionId || '' === $prefetchData) {
+ $this->newSessionId = '' === $prefetchData ? $sessionId : null;
+
+ return $prefetchData;
+ }
+ }
+
+ $data = $this->doRead($sessionId);
+ $this->newSessionId = '' === $data ? $sessionId : null;
+
+ return $data;
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function write($sessionId, $data)
+ {
+ if (null === $this->igbinaryEmptyData) {
+ // see https://github.com/igbinary/igbinary/issues/146
+ $this->igbinaryEmptyData = \function_exists('igbinary_serialize') ? igbinary_serialize([]) : '';
+ }
+ if ('' === $data || $this->igbinaryEmptyData === $data) {
+ return $this->destroy($sessionId);
+ }
+ $this->newSessionId = null;
+
+ return $this->doWrite($sessionId, $data);
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function destroy($sessionId)
+ {
+ if (!headers_sent() && filter_var(ini_get('session.use_cookies'), \FILTER_VALIDATE_BOOLEAN)) {
+ if (!$this->sessionName) {
+ throw new \LogicException(sprintf('Session name cannot be empty, did you forget to call "parent::open()" in "%s"?.', static::class));
+ }
+ $cookie = SessionUtils::popSessionCookie($this->sessionName, $sessionId);
+
+ /*
+ * We send an invalidation Set-Cookie header (zero lifetime)
+ * when either the session was started or a cookie with
+ * the session name was sent by the client (in which case
+ * we know it's invalid as a valid session cookie would've
+ * started the session).
+ */
+ if (null === $cookie || isset($_COOKIE[$this->sessionName])) {
+ if (\PHP_VERSION_ID < 70300) {
+ setcookie($this->sessionName, '', 0, ini_get('session.cookie_path'), ini_get('session.cookie_domain'), filter_var(ini_get('session.cookie_secure'), \FILTER_VALIDATE_BOOLEAN), filter_var(ini_get('session.cookie_httponly'), \FILTER_VALIDATE_BOOLEAN));
+ } else {
+ $params = session_get_cookie_params();
+ unset($params['lifetime']);
+ setcookie($this->sessionName, '', $params);
+ }
+ }
+ }
+
+ return $this->newSessionId === $sessionId || $this->doDestroy($sessionId);
+ }
+}
diff --git a/symfony/http-foundation/Session/Storage/Handler/IdentityMarshaller.php b/symfony/http-foundation/Session/Storage/Handler/IdentityMarshaller.php
new file mode 100644
index 00000000..bea3a323
--- /dev/null
+++ b/symfony/http-foundation/Session/Storage/Handler/IdentityMarshaller.php
@@ -0,0 +1,42 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
+
+use Symfony\Component\Cache\Marshaller\MarshallerInterface;
+
+/**
+ * @author Ahmed TAILOULOUTE <ahmed.tailouloute@gmail.com>
+ */
+class IdentityMarshaller implements MarshallerInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function marshall(array $values, ?array &$failed): array
+ {
+ foreach ($values as $key => $value) {
+ if (!\is_string($value)) {
+ throw new \LogicException(sprintf('%s accepts only string as data.', __METHOD__));
+ }
+ }
+
+ return $values;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function unmarshall(string $value): string
+ {
+ return $value;
+ }
+}
diff --git a/symfony/http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php b/symfony/http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php
new file mode 100644
index 00000000..c321c8c9
--- /dev/null
+++ b/symfony/http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php
@@ -0,0 +1,108 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
+
+use Symfony\Component\Cache\Marshaller\MarshallerInterface;
+
+/**
+ * @author Ahmed TAILOULOUTE <ahmed.tailouloute@gmail.com>
+ */
+class MarshallingSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface
+{
+ private $handler;
+ private $marshaller;
+
+ public function __construct(AbstractSessionHandler $handler, MarshallerInterface $marshaller)
+ {
+ $this->handler = $handler;
+ $this->marshaller = $marshaller;
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function open($savePath, $name)
+ {
+ return $this->handler->open($savePath, $name);
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function close()
+ {
+ return $this->handler->close();
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function destroy($sessionId)
+ {
+ return $this->handler->destroy($sessionId);
+ }
+
+ /**
+ * @return int|false
+ */
+ #[\ReturnTypeWillChange]
+ public function gc($maxlifetime)
+ {
+ return $this->handler->gc($maxlifetime);
+ }
+
+ /**
+ * @return string
+ */
+ #[\ReturnTypeWillChange]
+ public function read($sessionId)
+ {
+ return $this->marshaller->unmarshall($this->handler->read($sessionId));
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function write($sessionId, $data)
+ {
+ $failed = [];
+ $marshalledData = $this->marshaller->marshall(['data' => $data], $failed);
+
+ if (isset($failed['data'])) {
+ return false;
+ }
+
+ return $this->handler->write($sessionId, $marshalledData['data']);
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function validateId($sessionId)
+ {
+ return $this->handler->validateId($sessionId);
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function updateTimestamp($sessionId, $data)
+ {
+ return $this->handler->updateTimestamp($sessionId, $data);
+ }
+}
diff --git a/symfony/http-foundation/Session/Storage/Handler/MemcachedSessionHandler.php b/symfony/http-foundation/Session/Storage/Handler/MemcachedSessionHandler.php
new file mode 100644
index 00000000..a5a78eb9
--- /dev/null
+++ b/symfony/http-foundation/Session/Storage/Handler/MemcachedSessionHandler.php
@@ -0,0 +1,122 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
+
+/**
+ * Memcached based session storage handler based on the Memcached class
+ * provided by the PHP memcached extension.
+ *
+ * @see https://php.net/memcached
+ *
+ * @author Drak <drak@zikula.org>
+ */
+class MemcachedSessionHandler extends AbstractSessionHandler
+{
+ private $memcached;
+
+ /**
+ * @var int Time to live in seconds
+ */
+ private $ttl;
+
+ /**
+ * @var string Key prefix for shared environments
+ */
+ private $prefix;
+
+ /**
+ * Constructor.
+ *
+ * List of available options:
+ * * prefix: The prefix to use for the memcached keys in order to avoid collision
+ * * ttl: The time to live in seconds.
+ *
+ * @throws \InvalidArgumentException When unsupported options are passed
+ */
+ public function __construct(\Memcached $memcached, array $options = [])
+ {
+ $this->memcached = $memcached;
+
+ if ($diff = array_diff(array_keys($options), ['prefix', 'expiretime', 'ttl'])) {
+ throw new \InvalidArgumentException(sprintf('The following options are not supported "%s".', implode(', ', $diff)));
+ }
+
+ $this->ttl = $options['expiretime'] ?? $options['ttl'] ?? null;
+ $this->prefix = $options['prefix'] ?? 'sf2s';
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function close()
+ {
+ return $this->memcached->quit();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doRead(string $sessionId)
+ {
+ return $this->memcached->get($this->prefix.$sessionId) ?: '';
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function updateTimestamp($sessionId, $data)
+ {
+ $this->memcached->touch($this->prefix.$sessionId, time() + (int) ($this->ttl ?? ini_get('session.gc_maxlifetime')));
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doWrite(string $sessionId, string $data)
+ {
+ return $this->memcached->set($this->prefix.$sessionId, $data, time() + (int) ($this->ttl ?? ini_get('session.gc_maxlifetime')));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doDestroy(string $sessionId)
+ {
+ $result = $this->memcached->delete($this->prefix.$sessionId);
+
+ return $result || \Memcached::RES_NOTFOUND == $this->memcached->getResultCode();
+ }
+
+ /**
+ * @return int|false
+ */
+ #[\ReturnTypeWillChange]
+ public function gc($maxlifetime)
+ {
+ // not required here because memcached will auto expire the records anyhow.
+ return 0;
+ }
+
+ /**
+ * Return a Memcached instance.
+ *
+ * @return \Memcached
+ */
+ protected function getMemcached()
+ {
+ return $this->memcached;
+ }
+}
diff --git a/symfony/http-foundation/Session/Storage/Handler/MigratingSessionHandler.php b/symfony/http-foundation/Session/Storage/Handler/MigratingSessionHandler.php
new file mode 100644
index 00000000..bf27ca6c
--- /dev/null
+++ b/symfony/http-foundation/Session/Storage/Handler/MigratingSessionHandler.php
@@ -0,0 +1,139 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
+
+/**
+ * Migrating session handler for migrating from one handler to another. It reads
+ * from the current handler and writes both the current and new ones.
+ *
+ * It ignores errors from the new handler.
+ *
+ * @author Ross Motley <ross.motley@amara.com>
+ * @author Oliver Radwell <oliver.radwell@amara.com>
+ */
+class MigratingSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface
+{
+ /**
+ * @var \SessionHandlerInterface&\SessionUpdateTimestampHandlerInterface
+ */
+ private $currentHandler;
+
+ /**
+ * @var \SessionHandlerInterface&\SessionUpdateTimestampHandlerInterface
+ */
+ private $writeOnlyHandler;
+
+ public function __construct(\SessionHandlerInterface $currentHandler, \SessionHandlerInterface $writeOnlyHandler)
+ {
+ if (!$currentHandler instanceof \SessionUpdateTimestampHandlerInterface) {
+ $currentHandler = new StrictSessionHandler($currentHandler);
+ }
+ if (!$writeOnlyHandler instanceof \SessionUpdateTimestampHandlerInterface) {
+ $writeOnlyHandler = new StrictSessionHandler($writeOnlyHandler);
+ }
+
+ $this->currentHandler = $currentHandler;
+ $this->writeOnlyHandler = $writeOnlyHandler;
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function close()
+ {
+ $result = $this->currentHandler->close();
+ $this->writeOnlyHandler->close();
+
+ return $result;
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function destroy($sessionId)
+ {
+ $result = $this->currentHandler->destroy($sessionId);
+ $this->writeOnlyHandler->destroy($sessionId);
+
+ return $result;
+ }
+
+ /**
+ * @return int|false
+ */
+ #[\ReturnTypeWillChange]
+ public function gc($maxlifetime)
+ {
+ $result = $this->currentHandler->gc($maxlifetime);
+ $this->writeOnlyHandler->gc($maxlifetime);
+
+ return $result;
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function open($savePath, $sessionName)
+ {
+ $result = $this->currentHandler->open($savePath, $sessionName);
+ $this->writeOnlyHandler->open($savePath, $sessionName);
+
+ return $result;
+ }
+
+ /**
+ * @return string
+ */
+ #[\ReturnTypeWillChange]
+ public function read($sessionId)
+ {
+ // No reading from new handler until switch-over
+ return $this->currentHandler->read($sessionId);
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function write($sessionId, $sessionData)
+ {
+ $result = $this->currentHandler->write($sessionId, $sessionData);
+ $this->writeOnlyHandler->write($sessionId, $sessionData);
+
+ return $result;
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function validateId($sessionId)
+ {
+ // No reading from new handler until switch-over
+ return $this->currentHandler->validateId($sessionId);
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function updateTimestamp($sessionId, $sessionData)
+ {
+ $result = $this->currentHandler->updateTimestamp($sessionId, $sessionData);
+ $this->writeOnlyHandler->updateTimestamp($sessionId, $sessionData);
+
+ return $result;
+ }
+}
diff --git a/symfony/http-foundation/Session/Storage/Handler/MongoDbSessionHandler.php b/symfony/http-foundation/Session/Storage/Handler/MongoDbSessionHandler.php
new file mode 100644
index 00000000..8384e79d
--- /dev/null
+++ b/symfony/http-foundation/Session/Storage/Handler/MongoDbSessionHandler.php
@@ -0,0 +1,193 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
+
+use MongoDB\BSON\Binary;
+use MongoDB\BSON\UTCDateTime;
+use MongoDB\Client;
+use MongoDB\Collection;
+
+/**
+ * Session handler using the mongodb/mongodb package and MongoDB driver extension.
+ *
+ * @author Markus Bachmann <markus.bachmann@bachi.biz>
+ *
+ * @see https://packagist.org/packages/mongodb/mongodb
+ * @see https://php.net/mongodb
+ */
+class MongoDbSessionHandler extends AbstractSessionHandler
+{
+ private $mongo;
+
+ /**
+ * @var Collection
+ */
+ private $collection;
+
+ /**
+ * @var array
+ */
+ private $options;
+
+ /**
+ * Constructor.
+ *
+ * List of available options:
+ * * database: The name of the database [required]
+ * * collection: The name of the collection [required]
+ * * id_field: The field name for storing the session id [default: _id]
+ * * data_field: The field name for storing the session data [default: data]
+ * * time_field: The field name for storing the timestamp [default: time]
+ * * expiry_field: The field name for storing the expiry-timestamp [default: expires_at].
+ *
+ * It is strongly recommended to put an index on the `expiry_field` for
+ * garbage-collection. Alternatively it's possible to automatically expire
+ * the sessions in the database as described below:
+ *
+ * A TTL collections can be used on MongoDB 2.2+ to cleanup expired sessions
+ * automatically. Such an index can for example look like this:
+ *
+ * db.<session-collection>.createIndex(
+ * { "<expiry-field>": 1 },
+ * { "expireAfterSeconds": 0 }
+ * )
+ *
+ * More details on: https://docs.mongodb.org/manual/tutorial/expire-data/
+ *
+ * If you use such an index, you can drop `gc_probability` to 0 since
+ * no garbage-collection is required.
+ *
+ * @throws \InvalidArgumentException When "database" or "collection" not provided
+ */
+ public function __construct(Client $mongo, array $options)
+ {
+ if (!isset($options['database']) || !isset($options['collection'])) {
+ throw new \InvalidArgumentException('You must provide the "database" and "collection" option for MongoDBSessionHandler.');
+ }
+
+ $this->mongo = $mongo;
+
+ $this->options = array_merge([
+ 'id_field' => '_id',
+ 'data_field' => 'data',
+ 'time_field' => 'time',
+ 'expiry_field' => 'expires_at',
+ ], $options);
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function close()
+ {
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doDestroy(string $sessionId)
+ {
+ $this->getCollection()->deleteOne([
+ $this->options['id_field'] => $sessionId,
+ ]);
+
+ return true;
+ }
+
+ /**
+ * @return int|false
+ */
+ #[\ReturnTypeWillChange]
+ public function gc($maxlifetime)
+ {
+ return $this->getCollection()->deleteMany([
+ $this->options['expiry_field'] => ['$lt' => new UTCDateTime()],
+ ])->getDeletedCount();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doWrite(string $sessionId, string $data)
+ {
+ $expiry = new UTCDateTime((time() + (int) ini_get('session.gc_maxlifetime')) * 1000);
+
+ $fields = [
+ $this->options['time_field'] => new UTCDateTime(),
+ $this->options['expiry_field'] => $expiry,
+ $this->options['data_field'] => new Binary($data, Binary::TYPE_OLD_BINARY),
+ ];
+
+ $this->getCollection()->updateOne(
+ [$this->options['id_field'] => $sessionId],
+ ['$set' => $fields],
+ ['upsert' => true]
+ );
+
+ return true;
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function updateTimestamp($sessionId, $data)
+ {
+ $expiry = new UTCDateTime((time() + (int) ini_get('session.gc_maxlifetime')) * 1000);
+
+ $this->getCollection()->updateOne(
+ [$this->options['id_field'] => $sessionId],
+ ['$set' => [
+ $this->options['time_field'] => new UTCDateTime(),
+ $this->options['expiry_field'] => $expiry,
+ ]]
+ );
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doRead(string $sessionId)
+ {
+ $dbData = $this->getCollection()->findOne([
+ $this->options['id_field'] => $sessionId,
+ $this->options['expiry_field'] => ['$gte' => new UTCDateTime()],
+ ]);
+
+ if (null === $dbData) {
+ return '';
+ }
+
+ return $dbData[$this->options['data_field']]->getData();
+ }
+
+ private function getCollection(): Collection
+ {
+ if (null === $this->collection) {
+ $this->collection = $this->mongo->selectCollection($this->options['database'], $this->options['collection']);
+ }
+
+ return $this->collection;
+ }
+
+ /**
+ * @return Client
+ */
+ protected function getMongo()
+ {
+ return $this->mongo;
+ }
+}
diff --git a/symfony/http-foundation/Session/Storage/Handler/NativeFileSessionHandler.php b/symfony/http-foundation/Session/Storage/Handler/NativeFileSessionHandler.php
new file mode 100644
index 00000000..effc9db5
--- /dev/null
+++ b/symfony/http-foundation/Session/Storage/Handler/NativeFileSessionHandler.php
@@ -0,0 +1,55 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
+
+/**
+ * Native session handler using PHP's built in file storage.
+ *
+ * @author Drak <drak@zikula.org>
+ */
+class NativeFileSessionHandler extends \SessionHandler
+{
+ /**
+ * @param string $savePath Path of directory to save session files
+ * Default null will leave setting as defined by PHP.
+ * '/path', 'N;/path', or 'N;octal-mode;/path
+ *
+ * @see https://php.net/session.configuration#ini.session.save-path for further details.
+ *
+ * @throws \InvalidArgumentException On invalid $savePath
+ * @throws \RuntimeException When failing to create the save directory
+ */
+ public function __construct(string $savePath = null)
+ {
+ if (null === $savePath) {
+ $savePath = ini_get('session.save_path');
+ }
+
+ $baseDir = $savePath;
+
+ if ($count = substr_count($savePath, ';')) {
+ if ($count > 2) {
+ throw new \InvalidArgumentException(sprintf('Invalid argument $savePath \'%s\'.', $savePath));
+ }
+
+ // characters after last ';' are the path
+ $baseDir = ltrim(strrchr($savePath, ';'), ';');
+ }
+
+ if ($baseDir && !is_dir($baseDir) && !@mkdir($baseDir, 0777, true) && !is_dir($baseDir)) {
+ throw new \RuntimeException(sprintf('Session Storage was not able to create directory "%s".', $baseDir));
+ }
+
+ ini_set('session.save_path', $savePath);
+ ini_set('session.save_handler', 'files');
+ }
+}
diff --git a/symfony/http-foundation/Session/Storage/Handler/NullSessionHandler.php b/symfony/http-foundation/Session/Storage/Handler/NullSessionHandler.php
new file mode 100644
index 00000000..4331dbe5
--- /dev/null
+++ b/symfony/http-foundation/Session/Storage/Handler/NullSessionHandler.php
@@ -0,0 +1,80 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
+
+/**
+ * Can be used in unit testing or in a situations where persisted sessions are not desired.
+ *
+ * @author Drak <drak@zikula.org>
+ */
+class NullSessionHandler extends AbstractSessionHandler
+{
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function close()
+ {
+ return true;
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function validateId($sessionId)
+ {
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doRead(string $sessionId)
+ {
+ return '';
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function updateTimestamp($sessionId, $data)
+ {
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doWrite(string $sessionId, string $data)
+ {
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doDestroy(string $sessionId)
+ {
+ return true;
+ }
+
+ /**
+ * @return int|false
+ */
+ #[\ReturnTypeWillChange]
+ public function gc($maxlifetime)
+ {
+ return 0;
+ }
+}
diff --git a/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php b/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php
new file mode 100644
index 00000000..067bfcb3
--- /dev/null
+++ b/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php
@@ -0,0 +1,943 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
+
+/**
+ * Session handler using a PDO connection to read and write data.
+ *
+ * It works with MySQL, PostgreSQL, Oracle, SQL Server and SQLite and implements
+ * different locking strategies to handle concurrent access to the same session.
+ * Locking is necessary to prevent loss of data due to race conditions and to keep
+ * the session data consistent between read() and write(). With locking, requests
+ * for the same session will wait until the other one finished writing. For this
+ * reason it's best practice to close a session as early as possible to improve
+ * concurrency. PHPs internal files session handler also implements locking.
+ *
+ * Attention: Since SQLite does not support row level locks but locks the whole database,
+ * it means only one session can be accessed at a time. Even different sessions would wait
+ * for another to finish. So saving session in SQLite should only be considered for
+ * development or prototypes.
+ *
+ * Session data is a binary string that can contain non-printable characters like the null byte.
+ * For this reason it must be saved in a binary column in the database like BLOB in MySQL.
+ * Saving it in a character column could corrupt the data. You can use createTable()
+ * to initialize a correctly defined table.
+ *
+ * @see https://php.net/sessionhandlerinterface
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Michael Williams <michael.williams@funsational.com>
+ * @author Tobias Schultze <http://tobion.de>
+ */
+class PdoSessionHandler extends AbstractSessionHandler
+{
+ /**
+ * No locking is done. This means sessions are prone to loss of data due to
+ * race conditions of concurrent requests to the same session. The last session
+ * write will win in this case. It might be useful when you implement your own
+ * logic to deal with this like an optimistic approach.
+ */
+ public const LOCK_NONE = 0;
+
+ /**
+ * Creates an application-level lock on a session. The disadvantage is that the
+ * lock is not enforced by the database and thus other, unaware parts of the
+ * application could still concurrently modify the session. The advantage is it
+ * does not require a transaction.
+ * This mode is not available for SQLite and not yet implemented for oci and sqlsrv.
+ */
+ public const LOCK_ADVISORY = 1;
+
+ /**
+ * Issues a real row lock. Since it uses a transaction between opening and
+ * closing a session, you have to be careful when you use same database connection
+ * that you also use for your application logic. This mode is the default because
+ * it's the only reliable solution across DBMSs.
+ */
+ public const LOCK_TRANSACTIONAL = 2;
+
+ private const MAX_LIFETIME = 315576000;
+
+ /**
+ * @var \PDO|null PDO instance or null when not connected yet
+ */
+ private $pdo;
+
+ /**
+ * DSN string or null for session.save_path or false when lazy connection disabled.
+ *
+ * @var string|false|null
+ */
+ private $dsn = false;
+
+ /**
+ * @var string|null
+ */
+ private $driver;
+
+ /**
+ * @var string
+ */
+ private $table = 'sessions';
+
+ /**
+ * @var string
+ */
+ private $idCol = 'sess_id';
+
+ /**
+ * @var string
+ */
+ private $dataCol = 'sess_data';
+
+ /**
+ * @var string
+ */
+ private $lifetimeCol = 'sess_lifetime';
+
+ /**
+ * @var string
+ */
+ private $timeCol = 'sess_time';
+
+ /**
+ * Username when lazy-connect.
+ *
+ * @var string
+ */
+ private $username = '';
+
+ /**
+ * Password when lazy-connect.
+ *
+ * @var string
+ */
+ private $password = '';
+
+ /**
+ * Connection options when lazy-connect.
+ *
+ * @var array
+ */
+ private $connectionOptions = [];
+
+ /**
+ * The strategy for locking, see constants.
+ *
+ * @var int
+ */
+ private $lockMode = self::LOCK_TRANSACTIONAL;
+
+ /**
+ * It's an array to support multiple reads before closing which is manual, non-standard usage.
+ *
+ * @var \PDOStatement[] An array of statements to release advisory locks
+ */
+ private $unlockStatements = [];
+
+ /**
+ * True when the current session exists but expired according to session.gc_maxlifetime.
+ *
+ * @var bool
+ */
+ private $sessionExpired = false;
+
+ /**
+ * Whether a transaction is active.
+ *
+ * @var bool
+ */
+ private $inTransaction = false;
+
+ /**
+ * Whether gc() has been called.
+ *
+ * @var bool
+ */
+ private $gcCalled = false;
+
+ /**
+ * You can either pass an existing database connection as PDO instance or
+ * pass a DSN string that will be used to lazy-connect to the database
+ * when the session is actually used. Furthermore it's possible to pass null
+ * which will then use the session.save_path ini setting as PDO DSN parameter.
+ *
+ * List of available options:
+ * * db_table: The name of the table [default: sessions]
+ * * db_id_col: The column where to store the session id [default: sess_id]
+ * * db_data_col: The column where to store the session data [default: sess_data]
+ * * db_lifetime_col: The column where to store the lifetime [default: sess_lifetime]
+ * * db_time_col: The column where to store the timestamp [default: sess_time]
+ * * db_username: The username when lazy-connect [default: '']
+ * * db_password: The password when lazy-connect [default: '']
+ * * db_connection_options: An array of driver-specific connection options [default: []]
+ * * lock_mode: The strategy for locking, see constants [default: LOCK_TRANSACTIONAL]
+ *
+ * @param \PDO|string|null $pdoOrDsn A \PDO instance or DSN string or URL string or null
+ *
+ * @throws \InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION
+ */
+ public function __construct($pdoOrDsn = null, array $options = [])
+ {
+ if ($pdoOrDsn instanceof \PDO) {
+ if (\PDO::ERRMODE_EXCEPTION !== $pdoOrDsn->getAttribute(\PDO::ATTR_ERRMODE)) {
+ throw new \InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION)).', __CLASS__));
+ }
+
+ $this->pdo = $pdoOrDsn;
+ $this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
+ } elseif (\is_string($pdoOrDsn) && str_contains($pdoOrDsn, '://')) {
+ $this->dsn = $this->buildDsnFromUrl($pdoOrDsn);
+ } else {
+ $this->dsn = $pdoOrDsn;
+ }
+
+ $this->table = $options['db_table'] ?? $this->table;
+ $this->idCol = $options['db_id_col'] ?? $this->idCol;
+ $this->dataCol = $options['db_data_col'] ?? $this->dataCol;
+ $this->lifetimeCol = $options['db_lifetime_col'] ?? $this->lifetimeCol;
+ $this->timeCol = $options['db_time_col'] ?? $this->timeCol;
+ $this->username = $options['db_username'] ?? $this->username;
+ $this->password = $options['db_password'] ?? $this->password;
+ $this->connectionOptions = $options['db_connection_options'] ?? $this->connectionOptions;
+ $this->lockMode = $options['lock_mode'] ?? $this->lockMode;
+ }
+
+ /**
+ * Creates the table to store sessions which can be called once for setup.
+ *
+ * Session ID is saved in a column of maximum length 128 because that is enough even
+ * for a 512 bit configured session.hash_function like Whirlpool. Session data is
+ * saved in a BLOB. One could also use a shorter inlined varbinary column
+ * if one was sure the data fits into it.
+ *
+ * @throws \PDOException When the table already exists
+ * @throws \DomainException When an unsupported PDO driver is used
+ */
+ public function createTable()
+ {
+ // connect if we are not yet
+ $this->getConnection();
+
+ switch ($this->driver) {
+ case 'mysql':
+ // We use varbinary for the ID column because it prevents unwanted conversions:
+ // - character set conversions between server and client
+ // - trailing space removal
+ // - case-insensitivity
+ // - language processing like é == e
+ $sql = "CREATE TABLE $this->table ($this->idCol VARBINARY(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER UNSIGNED NOT NULL, $this->timeCol INTEGER UNSIGNED NOT NULL) COLLATE utf8mb4_bin, ENGINE = InnoDB";
+ break;
+ case 'sqlite':
+ $sql = "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)";
+ break;
+ case 'pgsql':
+ $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(128) NOT NULL PRIMARY KEY, $this->dataCol BYTEA NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)";
+ break;
+ case 'oci':
+ $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR2(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)";
+ break;
+ case 'sqlsrv':
+ $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(128) NOT NULL PRIMARY KEY, $this->dataCol VARBINARY(MAX) NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)";
+ break;
+ default:
+ throw new \DomainException(sprintf('Creating the session table is currently not implemented for PDO driver "%s".', $this->driver));
+ }
+
+ try {
+ $this->pdo->exec($sql);
+ $this->pdo->exec("CREATE INDEX EXPIRY ON $this->table ($this->lifetimeCol)");
+ } catch (\PDOException $e) {
+ $this->rollback();
+
+ throw $e;
+ }
+ }
+
+ /**
+ * Returns true when the current session exists but expired according to session.gc_maxlifetime.
+ *
+ * Can be used to distinguish between a new session and one that expired due to inactivity.
+ *
+ * @return bool
+ */
+ public function isSessionExpired()
+ {
+ return $this->sessionExpired;
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function open($savePath, $sessionName)
+ {
+ $this->sessionExpired = false;
+
+ if (null === $this->pdo) {
+ $this->connect($this->dsn ?: $savePath);
+ }
+
+ return parent::open($savePath, $sessionName);
+ }
+
+ /**
+ * @return string
+ */
+ #[\ReturnTypeWillChange]
+ public function read($sessionId)
+ {
+ try {
+ return parent::read($sessionId);
+ } catch (\PDOException $e) {
+ $this->rollback();
+
+ throw $e;
+ }
+ }
+
+ /**
+ * @return int|false
+ */
+ #[\ReturnTypeWillChange]
+ public function gc($maxlifetime)
+ {
+ // We delay gc() to close() so that it is executed outside the transactional and blocking read-write process.
+ // This way, pruning expired sessions does not block them from being started while the current session is used.
+ $this->gcCalled = true;
+
+ return 0;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doDestroy(string $sessionId)
+ {
+ // delete the record associated with this id
+ $sql = "DELETE FROM $this->table WHERE $this->idCol = :id";
+
+ try {
+ $stmt = $this->pdo->prepare($sql);
+ $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
+ $stmt->execute();
+ } catch (\PDOException $e) {
+ $this->rollback();
+
+ throw $e;
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doWrite(string $sessionId, string $data)
+ {
+ $maxlifetime = (int) ini_get('session.gc_maxlifetime');
+
+ try {
+ // We use a single MERGE SQL query when supported by the database.
+ $mergeStmt = $this->getMergeStatement($sessionId, $data, $maxlifetime);
+ if (null !== $mergeStmt) {
+ $mergeStmt->execute();
+
+ return true;
+ }
+
+ $updateStmt = $this->getUpdateStatement($sessionId, $data, $maxlifetime);
+ $updateStmt->execute();
+
+ // When MERGE is not supported, like in Postgres < 9.5, we have to use this approach that can result in
+ // duplicate key errors when the same session is written simultaneously (given the LOCK_NONE behavior).
+ // We can just catch such an error and re-execute the update. This is similar to a serializable
+ // transaction with retry logic on serialization failures but without the overhead and without possible
+ // false positives due to longer gap locking.
+ if (!$updateStmt->rowCount()) {
+ try {
+ $insertStmt = $this->getInsertStatement($sessionId, $data, $maxlifetime);
+ $insertStmt->execute();
+ } catch (\PDOException $e) {
+ // Handle integrity violation SQLSTATE 23000 (or a subclass like 23505 in Postgres) for duplicate keys
+ if (str_starts_with($e->getCode(), '23')) {
+ $updateStmt->execute();
+ } else {
+ throw $e;
+ }
+ }
+ }
+ } catch (\PDOException $e) {
+ $this->rollback();
+
+ throw $e;
+ }
+
+ return true;
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function updateTimestamp($sessionId, $data)
+ {
+ $expiry = time() + (int) ini_get('session.gc_maxlifetime');
+
+ try {
+ $updateStmt = $this->pdo->prepare(
+ "UPDATE $this->table SET $this->lifetimeCol = :expiry, $this->timeCol = :time WHERE $this->idCol = :id"
+ );
+ $updateStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
+ $updateStmt->bindParam(':expiry', $expiry, \PDO::PARAM_INT);
+ $updateStmt->bindValue(':time', time(), \PDO::PARAM_INT);
+ $updateStmt->execute();
+ } catch (\PDOException $e) {
+ $this->rollback();
+
+ throw $e;
+ }
+
+ return true;
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function close()
+ {
+ $this->commit();
+
+ while ($unlockStmt = array_shift($this->unlockStatements)) {
+ $unlockStmt->execute();
+ }
+
+ if ($this->gcCalled) {
+ $this->gcCalled = false;
+
+ // delete the session records that have expired
+ $sql = "DELETE FROM $this->table WHERE $this->lifetimeCol < :time AND $this->lifetimeCol > :min";
+ $stmt = $this->pdo->prepare($sql);
+ $stmt->bindValue(':time', time(), \PDO::PARAM_INT);
+ $stmt->bindValue(':min', self::MAX_LIFETIME, \PDO::PARAM_INT);
+ $stmt->execute();
+ // to be removed in 6.0
+ if ('mysql' === $this->driver) {
+ $legacySql = "DELETE FROM $this->table WHERE $this->lifetimeCol <= :min AND $this->lifetimeCol + $this->timeCol < :time";
+ } else {
+ $legacySql = "DELETE FROM $this->table WHERE $this->lifetimeCol <= :min AND $this->lifetimeCol < :time - $this->timeCol";
+ }
+
+ $stmt = $this->pdo->prepare($legacySql);
+ $stmt->bindValue(':time', time(), \PDO::PARAM_INT);
+ $stmt->bindValue(':min', self::MAX_LIFETIME, \PDO::PARAM_INT);
+ $stmt->execute();
+ }
+
+ if (false !== $this->dsn) {
+ $this->pdo = null; // only close lazy-connection
+ $this->driver = null;
+ }
+
+ return true;
+ }
+
+ /**
+ * Lazy-connects to the database.
+ */
+ private function connect(string $dsn): void
+ {
+ $this->pdo = new \PDO($dsn, $this->username, $this->password, $this->connectionOptions);
+ $this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
+ $this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
+ }
+
+ /**
+ * Builds a PDO DSN from a URL-like connection string.
+ *
+ * @todo implement missing support for oci DSN (which look totally different from other PDO ones)
+ */
+ private function buildDsnFromUrl(string $dsnOrUrl): string
+ {
+ // (pdo_)?sqlite3?:///... => (pdo_)?sqlite3?://localhost/... or else the URL will be invalid
+ $url = preg_replace('#^((?:pdo_)?sqlite3?):///#', '$1://localhost/', $dsnOrUrl);
+
+ $params = parse_url($url);
+
+ if (false === $params) {
+ return $dsnOrUrl; // If the URL is not valid, let's assume it might be a DSN already.
+ }
+
+ $params = array_map('rawurldecode', $params);
+
+ // Override the default username and password. Values passed through options will still win over these in the constructor.
+ if (isset($params['user'])) {
+ $this->username = $params['user'];
+ }
+
+ if (isset($params['pass'])) {
+ $this->password = $params['pass'];
+ }
+
+ if (!isset($params['scheme'])) {
+ throw new \InvalidArgumentException('URLs without scheme are not supported to configure the PdoSessionHandler.');
+ }
+
+ $driverAliasMap = [
+ 'mssql' => 'sqlsrv',
+ 'mysql2' => 'mysql', // Amazon RDS, for some weird reason
+ 'postgres' => 'pgsql',
+ 'postgresql' => 'pgsql',
+ 'sqlite3' => 'sqlite',
+ ];
+
+ $driver = $driverAliasMap[$params['scheme']] ?? $params['scheme'];
+
+ // Doctrine DBAL supports passing its internal pdo_* driver names directly too (allowing both dashes and underscores). This allows supporting the same here.
+ if (str_starts_with($driver, 'pdo_') || str_starts_with($driver, 'pdo-')) {
+ $driver = substr($driver, 4);
+ }
+
+ $dsn = null;
+ switch ($driver) {
+ case 'mysql':
+ $dsn = 'mysql:';
+ if ('' !== ($params['query'] ?? '')) {
+ $queryParams = [];
+ parse_str($params['query'], $queryParams);
+ if ('' !== ($queryParams['charset'] ?? '')) {
+ $dsn .= 'charset='.$queryParams['charset'].';';
+ }
+
+ if ('' !== ($queryParams['unix_socket'] ?? '')) {
+ $dsn .= 'unix_socket='.$queryParams['unix_socket'].';';
+
+ if (isset($params['path'])) {
+ $dbName = substr($params['path'], 1); // Remove the leading slash
+ $dsn .= 'dbname='.$dbName.';';
+ }
+
+ return $dsn;
+ }
+ }
+ // If "unix_socket" is not in the query, we continue with the same process as pgsql
+ // no break
+ case 'pgsql':
+ $dsn ?? $dsn = 'pgsql:';
+
+ if (isset($params['host']) && '' !== $params['host']) {
+ $dsn .= 'host='.$params['host'].';';
+ }
+
+ if (isset($params['port']) && '' !== $params['port']) {
+ $dsn .= 'port='.$params['port'].';';
+ }
+
+ if (isset($params['path'])) {
+ $dbName = substr($params['path'], 1); // Remove the leading slash
+ $dsn .= 'dbname='.$dbName.';';
+ }
+
+ return $dsn;
+
+ case 'sqlite':
+ return 'sqlite:'.substr($params['path'], 1);
+
+ case 'sqlsrv':
+ $dsn = 'sqlsrv:server=';
+
+ if (isset($params['host'])) {
+ $dsn .= $params['host'];
+ }
+
+ if (isset($params['port']) && '' !== $params['port']) {
+ $dsn .= ','.$params['port'];
+ }
+
+ if (isset($params['path'])) {
+ $dbName = substr($params['path'], 1); // Remove the leading slash
+ $dsn .= ';Database='.$dbName;
+ }
+
+ return $dsn;
+
+ default:
+ throw new \InvalidArgumentException(sprintf('The scheme "%s" is not supported by the PdoSessionHandler URL configuration. Pass a PDO DSN directly.', $params['scheme']));
+ }
+ }
+
+ /**
+ * Helper method to begin a transaction.
+ *
+ * Since SQLite does not support row level locks, we have to acquire a reserved lock
+ * on the database immediately. Because of https://bugs.php.net/42766 we have to create
+ * such a transaction manually which also means we cannot use PDO::commit or
+ * PDO::rollback or PDO::inTransaction for SQLite.
+ *
+ * Also MySQLs default isolation, REPEATABLE READ, causes deadlock for different sessions
+ * due to https://percona.com/blog/2013/12/12/one-more-innodb-gap-lock-to-avoid/ .
+ * So we change it to READ COMMITTED.
+ */
+ private function beginTransaction(): void
+ {
+ if (!$this->inTransaction) {
+ if ('sqlite' === $this->driver) {
+ $this->pdo->exec('BEGIN IMMEDIATE TRANSACTION');
+ } else {
+ if ('mysql' === $this->driver) {
+ $this->pdo->exec('SET TRANSACTION ISOLATION LEVEL READ COMMITTED');
+ }
+ $this->pdo->beginTransaction();
+ }
+ $this->inTransaction = true;
+ }
+ }
+
+ /**
+ * Helper method to commit a transaction.
+ */
+ private function commit(): void
+ {
+ if ($this->inTransaction) {
+ try {
+ // commit read-write transaction which also releases the lock
+ if ('sqlite' === $this->driver) {
+ $this->pdo->exec('COMMIT');
+ } else {
+ $this->pdo->commit();
+ }
+ $this->inTransaction = false;
+ } catch (\PDOException $e) {
+ $this->rollback();
+
+ throw $e;
+ }
+ }
+ }
+
+ /**
+ * Helper method to rollback a transaction.
+ */
+ private function rollback(): void
+ {
+ // We only need to rollback if we are in a transaction. Otherwise the resulting
+ // error would hide the real problem why rollback was called. We might not be
+ // in a transaction when not using the transactional locking behavior or when
+ // two callbacks (e.g. destroy and write) are invoked that both fail.
+ if ($this->inTransaction) {
+ if ('sqlite' === $this->driver) {
+ $this->pdo->exec('ROLLBACK');
+ } else {
+ $this->pdo->rollBack();
+ }
+ $this->inTransaction = false;
+ }
+ }
+
+ /**
+ * Reads the session data in respect to the different locking strategies.
+ *
+ * We need to make sure we do not return session data that is already considered garbage according
+ * to the session.gc_maxlifetime setting because gc() is called after read() and only sometimes.
+ *
+ * @return string
+ */
+ protected function doRead(string $sessionId)
+ {
+ if (self::LOCK_ADVISORY === $this->lockMode) {
+ $this->unlockStatements[] = $this->doAdvisoryLock($sessionId);
+ }
+
+ $selectSql = $this->getSelectSql();
+ $selectStmt = $this->pdo->prepare($selectSql);
+ $selectStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
+ $insertStmt = null;
+
+ while (true) {
+ $selectStmt->execute();
+ $sessionRows = $selectStmt->fetchAll(\PDO::FETCH_NUM);
+
+ if ($sessionRows) {
+ $expiry = (int) $sessionRows[0][1];
+ if ($expiry <= self::MAX_LIFETIME) {
+ $expiry += $sessionRows[0][2];
+ }
+
+ if ($expiry < time()) {
+ $this->sessionExpired = true;
+
+ return '';
+ }
+
+ return \is_resource($sessionRows[0][0]) ? stream_get_contents($sessionRows[0][0]) : $sessionRows[0][0];
+ }
+
+ if (null !== $insertStmt) {
+ $this->rollback();
+ throw new \RuntimeException('Failed to read session: INSERT reported a duplicate id but next SELECT did not return any data.');
+ }
+
+ if (!filter_var(ini_get('session.use_strict_mode'), \FILTER_VALIDATE_BOOLEAN) && self::LOCK_TRANSACTIONAL === $this->lockMode && 'sqlite' !== $this->driver) {
+ // In strict mode, session fixation is not possible: new sessions always start with a unique
+ // random id, so that concurrency is not possible and this code path can be skipped.
+ // Exclusive-reading of non-existent rows does not block, so we need to do an insert to block
+ // until other connections to the session are committed.
+ try {
+ $insertStmt = $this->getInsertStatement($sessionId, '', 0);
+ $insertStmt->execute();
+ } catch (\PDOException $e) {
+ // Catch duplicate key error because other connection created the session already.
+ // It would only not be the case when the other connection destroyed the session.
+ if (str_starts_with($e->getCode(), '23')) {
+ // Retrieve finished session data written by concurrent connection by restarting the loop.
+ // We have to start a new transaction as a failed query will mark the current transaction as
+ // aborted in PostgreSQL and disallow further queries within it.
+ $this->rollback();
+ $this->beginTransaction();
+ continue;
+ }
+
+ throw $e;
+ }
+ }
+
+ return '';
+ }
+ }
+
+ /**
+ * Executes an application-level lock on the database.
+ *
+ * @return \PDOStatement The statement that needs to be executed later to release the lock
+ *
+ * @throws \DomainException When an unsupported PDO driver is used
+ *
+ * @todo implement missing advisory locks
+ * - for oci using DBMS_LOCK.REQUEST
+ * - for sqlsrv using sp_getapplock with LockOwner = Session
+ */
+ private function doAdvisoryLock(string $sessionId): \PDOStatement
+ {
+ switch ($this->driver) {
+ case 'mysql':
+ // MySQL 5.7.5 and later enforces a maximum length on lock names of 64 characters. Previously, no limit was enforced.
+ $lockId = substr($sessionId, 0, 64);
+ // should we handle the return value? 0 on timeout, null on error
+ // we use a timeout of 50 seconds which is also the default for innodb_lock_wait_timeout
+ $stmt = $this->pdo->prepare('SELECT GET_LOCK(:key, 50)');
+ $stmt->bindValue(':key', $lockId, \PDO::PARAM_STR);
+ $stmt->execute();
+
+ $releaseStmt = $this->pdo->prepare('DO RELEASE_LOCK(:key)');
+ $releaseStmt->bindValue(':key', $lockId, \PDO::PARAM_STR);
+
+ return $releaseStmt;
+ case 'pgsql':
+ // Obtaining an exclusive session level advisory lock requires an integer key.
+ // When session.sid_bits_per_character > 4, the session id can contain non-hex-characters.
+ // So we cannot just use hexdec().
+ if (4 === \PHP_INT_SIZE) {
+ $sessionInt1 = $this->convertStringToInt($sessionId);
+ $sessionInt2 = $this->convertStringToInt(substr($sessionId, 4, 4));
+
+ $stmt = $this->pdo->prepare('SELECT pg_advisory_lock(:key1, :key2)');
+ $stmt->bindValue(':key1', $sessionInt1, \PDO::PARAM_INT);
+ $stmt->bindValue(':key2', $sessionInt2, \PDO::PARAM_INT);
+ $stmt->execute();
+
+ $releaseStmt = $this->pdo->prepare('SELECT pg_advisory_unlock(:key1, :key2)');
+ $releaseStmt->bindValue(':key1', $sessionInt1, \PDO::PARAM_INT);
+ $releaseStmt->bindValue(':key2', $sessionInt2, \PDO::PARAM_INT);
+ } else {
+ $sessionBigInt = $this->convertStringToInt($sessionId);
+
+ $stmt = $this->pdo->prepare('SELECT pg_advisory_lock(:key)');
+ $stmt->bindValue(':key', $sessionBigInt, \PDO::PARAM_INT);
+ $stmt->execute();
+
+ $releaseStmt = $this->pdo->prepare('SELECT pg_advisory_unlock(:key)');
+ $releaseStmt->bindValue(':key', $sessionBigInt, \PDO::PARAM_INT);
+ }
+
+ return $releaseStmt;
+ case 'sqlite':
+ throw new \DomainException('SQLite does not support advisory locks.');
+ default:
+ throw new \DomainException(sprintf('Advisory locks are currently not implemented for PDO driver "%s".', $this->driver));
+ }
+ }
+
+ /**
+ * Encodes the first 4 (when PHP_INT_SIZE == 4) or 8 characters of the string as an integer.
+ *
+ * Keep in mind, PHP integers are signed.
+ */
+ private function convertStringToInt(string $string): int
+ {
+ if (4 === \PHP_INT_SIZE) {
+ return (\ord($string[3]) << 24) + (\ord($string[2]) << 16) + (\ord($string[1]) << 8) + \ord($string[0]);
+ }
+
+ $int1 = (\ord($string[7]) << 24) + (\ord($string[6]) << 16) + (\ord($string[5]) << 8) + \ord($string[4]);
+ $int2 = (\ord($string[3]) << 24) + (\ord($string[2]) << 16) + (\ord($string[1]) << 8) + \ord($string[0]);
+
+ return $int2 + ($int1 << 32);
+ }
+
+ /**
+ * Return a locking or nonlocking SQL query to read session information.
+ *
+ * @throws \DomainException When an unsupported PDO driver is used
+ */
+ private function getSelectSql(): string
+ {
+ if (self::LOCK_TRANSACTIONAL === $this->lockMode) {
+ $this->beginTransaction();
+
+ // selecting the time column should be removed in 6.0
+ switch ($this->driver) {
+ case 'mysql':
+ case 'oci':
+ case 'pgsql':
+ return "SELECT $this->dataCol, $this->lifetimeCol, $this->timeCol FROM $this->table WHERE $this->idCol = :id FOR UPDATE";
+ case 'sqlsrv':
+ return "SELECT $this->dataCol, $this->lifetimeCol, $this->timeCol FROM $this->table WITH (UPDLOCK, ROWLOCK) WHERE $this->idCol = :id";
+ case 'sqlite':
+ // we already locked when starting transaction
+ break;
+ default:
+ throw new \DomainException(sprintf('Transactional locks are currently not implemented for PDO driver "%s".', $this->driver));
+ }
+ }
+
+ return "SELECT $this->dataCol, $this->lifetimeCol, $this->timeCol FROM $this->table WHERE $this->idCol = :id";
+ }
+
+ /**
+ * Returns an insert statement supported by the database for writing session data.
+ */
+ private function getInsertStatement(string $sessionId, string $sessionData, int $maxlifetime): \PDOStatement
+ {
+ switch ($this->driver) {
+ case 'oci':
+ $data = fopen('php://memory', 'r+');
+ fwrite($data, $sessionData);
+ rewind($data);
+ $sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, EMPTY_BLOB(), :expiry, :time) RETURNING $this->dataCol into :data";
+ break;
+ default:
+ $data = $sessionData;
+ $sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :expiry, :time)";
+ break;
+ }
+
+ $stmt = $this->pdo->prepare($sql);
+ $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
+ $stmt->bindParam(':data', $data, \PDO::PARAM_LOB);
+ $stmt->bindValue(':expiry', time() + $maxlifetime, \PDO::PARAM_INT);
+ $stmt->bindValue(':time', time(), \PDO::PARAM_INT);
+
+ return $stmt;
+ }
+
+ /**
+ * Returns an update statement supported by the database for writing session data.
+ */
+ private function getUpdateStatement(string $sessionId, string $sessionData, int $maxlifetime): \PDOStatement
+ {
+ switch ($this->driver) {
+ case 'oci':
+ $data = fopen('php://memory', 'r+');
+ fwrite($data, $sessionData);
+ rewind($data);
+ $sql = "UPDATE $this->table SET $this->dataCol = EMPTY_BLOB(), $this->lifetimeCol = :expiry, $this->timeCol = :time WHERE $this->idCol = :id RETURNING $this->dataCol into :data";
+ break;
+ default:
+ $data = $sessionData;
+ $sql = "UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :expiry, $this->timeCol = :time WHERE $this->idCol = :id";
+ break;
+ }
+
+ $stmt = $this->pdo->prepare($sql);
+ $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
+ $stmt->bindParam(':data', $data, \PDO::PARAM_LOB);
+ $stmt->bindValue(':expiry', time() + $maxlifetime, \PDO::PARAM_INT);
+ $stmt->bindValue(':time', time(), \PDO::PARAM_INT);
+
+ return $stmt;
+ }
+
+ /**
+ * Returns a merge/upsert (i.e. insert or update) statement when supported by the database for writing session data.
+ */
+ private function getMergeStatement(string $sessionId, string $data, int $maxlifetime): ?\PDOStatement
+ {
+ switch (true) {
+ case 'mysql' === $this->driver:
+ $mergeSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :expiry, :time) ".
+ "ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)";
+ break;
+ case 'sqlsrv' === $this->driver && version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '10', '>='):
+ // MERGE is only available since SQL Server 2008 and must be terminated by semicolon
+ // It also requires HOLDLOCK according to https://weblogs.sqlteam.com/dang/2009/01/31/upsert-race-condition-with-merge/
+ $mergeSql = "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = ?) ".
+ "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ".
+ "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?;";
+ break;
+ case 'sqlite' === $this->driver:
+ $mergeSql = "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :expiry, :time)";
+ break;
+ case 'pgsql' === $this->driver && version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '9.5', '>='):
+ $mergeSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :expiry, :time) ".
+ "ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->lifetimeCol, $this->timeCol) = (EXCLUDED.$this->dataCol, EXCLUDED.$this->lifetimeCol, EXCLUDED.$this->timeCol)";
+ break;
+ default:
+ // MERGE is not supported with LOBs: https://oracle.com/technetwork/articles/fuecks-lobs-095315.html
+ return null;
+ }
+
+ $mergeStmt = $this->pdo->prepare($mergeSql);
+
+ if ('sqlsrv' === $this->driver) {
+ $mergeStmt->bindParam(1, $sessionId, \PDO::PARAM_STR);
+ $mergeStmt->bindParam(2, $sessionId, \PDO::PARAM_STR);
+ $mergeStmt->bindParam(3, $data, \PDO::PARAM_LOB);
+ $mergeStmt->bindValue(4, time() + $maxlifetime, \PDO::PARAM_INT);
+ $mergeStmt->bindValue(5, time(), \PDO::PARAM_INT);
+ $mergeStmt->bindParam(6, $data, \PDO::PARAM_LOB);
+ $mergeStmt->bindValue(7, time() + $maxlifetime, \PDO::PARAM_INT);
+ $mergeStmt->bindValue(8, time(), \PDO::PARAM_INT);
+ } else {
+ $mergeStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
+ $mergeStmt->bindParam(':data', $data, \PDO::PARAM_LOB);
+ $mergeStmt->bindValue(':expiry', time() + $maxlifetime, \PDO::PARAM_INT);
+ $mergeStmt->bindValue(':time', time(), \PDO::PARAM_INT);
+ }
+
+ return $mergeStmt;
+ }
+
+ /**
+ * Return a PDO instance.
+ *
+ * @return \PDO
+ */
+ protected function getConnection()
+ {
+ if (null === $this->pdo) {
+ $this->connect($this->dsn ?: ini_get('session.save_path'));
+ }
+
+ return $this->pdo;
+ }
+}
diff --git a/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php b/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php
new file mode 100644
index 00000000..9573b311
--- /dev/null
+++ b/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php
@@ -0,0 +1,137 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
+
+use Predis\Response\ErrorInterface;
+use Symfony\Component\Cache\Traits\RedisClusterProxy;
+use Symfony\Component\Cache\Traits\RedisProxy;
+
+/**
+ * Redis based session storage handler based on the Redis class
+ * provided by the PHP redis extension.
+ *
+ * @author Dalibor Karlović <dalibor@flexolabs.io>
+ */
+class RedisSessionHandler extends AbstractSessionHandler
+{
+ private $redis;
+
+ /**
+ * @var string Key prefix for shared environments
+ */
+ private $prefix;
+
+ /**
+ * @var int Time to live in seconds
+ */
+ private $ttl;
+
+ /**
+ * List of available options:
+ * * prefix: The prefix to use for the keys in order to avoid collision on the Redis server
+ * * ttl: The time to live in seconds.
+ *
+ * @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy $redis
+ *
+ * @throws \InvalidArgumentException When unsupported client or options are passed
+ */
+ public function __construct($redis, array $options = [])
+ {
+ if (
+ !$redis instanceof \Redis &&
+ !$redis instanceof \RedisArray &&
+ !$redis instanceof \RedisCluster &&
+ !$redis instanceof \Predis\ClientInterface &&
+ !$redis instanceof RedisProxy &&
+ !$redis instanceof RedisClusterProxy
+ ) {
+ throw new \InvalidArgumentException(sprintf('"%s()" expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\ClientInterface, "%s" given.', __METHOD__, get_debug_type($redis)));
+ }
+
+ if ($diff = array_diff(array_keys($options), ['prefix', 'ttl'])) {
+ throw new \InvalidArgumentException(sprintf('The following options are not supported "%s".', implode(', ', $diff)));
+ }
+
+ $this->redis = $redis;
+ $this->prefix = $options['prefix'] ?? 'sf_s';
+ $this->ttl = $options['ttl'] ?? null;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doRead(string $sessionId): string
+ {
+ return $this->redis->get($this->prefix.$sessionId) ?: '';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doWrite(string $sessionId, string $data): bool
+ {
+ $result = $this->redis->setEx($this->prefix.$sessionId, (int) ($this->ttl ?? ini_get('session.gc_maxlifetime')), $data);
+
+ return $result && !$result instanceof ErrorInterface;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doDestroy(string $sessionId): bool
+ {
+ static $unlink = true;
+
+ if ($unlink) {
+ try {
+ $unlink = false !== $this->redis->unlink($this->prefix.$sessionId);
+ } catch (\Throwable $e) {
+ $unlink = false;
+ }
+ }
+
+ if (!$unlink) {
+ $this->redis->del($this->prefix.$sessionId);
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ #[\ReturnTypeWillChange]
+ public function close(): bool
+ {
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return int|false
+ */
+ #[\ReturnTypeWillChange]
+ public function gc($maxlifetime)
+ {
+ return 0;
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function updateTimestamp($sessionId, $data)
+ {
+ return (bool) $this->redis->expire($this->prefix.$sessionId, (int) ($this->ttl ?? ini_get('session.gc_maxlifetime')));
+ }
+}
diff --git a/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php b/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php
new file mode 100644
index 00000000..f3f7b201
--- /dev/null
+++ b/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php
@@ -0,0 +1,91 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
+
+use Doctrine\DBAL\DriverManager;
+use Symfony\Component\Cache\Adapter\AbstractAdapter;
+use Symfony\Component\Cache\Traits\RedisClusterProxy;
+use Symfony\Component\Cache\Traits\RedisProxy;
+
+/**
+ * @author Nicolas Grekas <p@tchwork.com>
+ */
+class SessionHandlerFactory
+{
+ /**
+ * @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy|\Memcached|\PDO|string $connection Connection or DSN
+ */
+ public static function createHandler($connection): AbstractSessionHandler
+ {
+ if (!\is_string($connection) && !\is_object($connection)) {
+ throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be a string or a connection object, "%s" given.', __METHOD__, get_debug_type($connection)));
+ }
+
+ if ($options = \is_string($connection) ? parse_url($connection) : false) {
+ parse_str($options['query'] ?? '', $options);
+ }
+
+ switch (true) {
+ case $connection instanceof \Redis:
+ case $connection instanceof \RedisArray:
+ case $connection instanceof \RedisCluster:
+ case $connection instanceof \Predis\ClientInterface:
+ case $connection instanceof RedisProxy:
+ case $connection instanceof RedisClusterProxy:
+ return new RedisSessionHandler($connection);
+
+ case $connection instanceof \Memcached:
+ return new MemcachedSessionHandler($connection);
+
+ case $connection instanceof \PDO:
+ return new PdoSessionHandler($connection);
+
+ case !\is_string($connection):
+ throw new \InvalidArgumentException(sprintf('Unsupported Connection: "%s".', get_debug_type($connection)));
+ case str_starts_with($connection, 'file://'):
+ $savePath = substr($connection, 7);
+
+ return new StrictSessionHandler(new NativeFileSessionHandler('' === $savePath ? null : $savePath));
+
+ case str_starts_with($connection, 'redis:'):
+ case str_starts_with($connection, 'rediss:'):
+ case str_starts_with($connection, 'memcached:'):
+ if (!class_exists(AbstractAdapter::class)) {
+ throw new \InvalidArgumentException(sprintf('Unsupported DSN "%s". Try running "composer require symfony/cache".', $connection));
+ }
+ $handlerClass = str_starts_with($connection, 'memcached:') ? MemcachedSessionHandler::class : RedisSessionHandler::class;
+ $connection = AbstractAdapter::createConnection($connection, ['lazy' => true]);
+
+ return new $handlerClass($connection, array_intersect_key($options ?: [], ['prefix' => 1, 'ttl' => 1]));
+
+ case str_starts_with($connection, 'pdo_oci://'):
+ if (!class_exists(DriverManager::class)) {
+ throw new \InvalidArgumentException(sprintf('Unsupported DSN "%s". Try running "composer require doctrine/dbal".', $connection));
+ }
+ $connection = DriverManager::getConnection(['url' => $connection])->getWrappedConnection();
+ // no break;
+
+ case str_starts_with($connection, 'mssql://'):
+ case str_starts_with($connection, 'mysql://'):
+ case str_starts_with($connection, 'mysql2://'):
+ case str_starts_with($connection, 'pgsql://'):
+ case str_starts_with($connection, 'postgres://'):
+ case str_starts_with($connection, 'postgresql://'):
+ case str_starts_with($connection, 'sqlsrv://'):
+ case str_starts_with($connection, 'sqlite://'):
+ case str_starts_with($connection, 'sqlite3://'):
+ return new PdoSessionHandler($connection, $options ?: []);
+ }
+
+ throw new \InvalidArgumentException(sprintf('Unsupported Connection: "%s".', $connection));
+ }
+}
diff --git a/symfony/http-foundation/Session/Storage/Handler/StrictSessionHandler.php b/symfony/http-foundation/Session/Storage/Handler/StrictSessionHandler.php
new file mode 100644
index 00000000..0461e997
--- /dev/null
+++ b/symfony/http-foundation/Session/Storage/Handler/StrictSessionHandler.php
@@ -0,0 +1,108 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
+
+/**
+ * Adds basic `SessionUpdateTimestampHandlerInterface` behaviors to another `SessionHandlerInterface`.
+ *
+ * @author Nicolas Grekas <p@tchwork.com>
+ */
+class StrictSessionHandler extends AbstractSessionHandler
+{
+ private $handler;
+ private $doDestroy;
+
+ public function __construct(\SessionHandlerInterface $handler)
+ {
+ if ($handler instanceof \SessionUpdateTimestampHandlerInterface) {
+ throw new \LogicException(sprintf('"%s" is already an instance of "SessionUpdateTimestampHandlerInterface", you cannot wrap it with "%s".', get_debug_type($handler), self::class));
+ }
+
+ $this->handler = $handler;
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function open($savePath, $sessionName)
+ {
+ parent::open($savePath, $sessionName);
+
+ return $this->handler->open($savePath, $sessionName);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doRead(string $sessionId)
+ {
+ return $this->handler->read($sessionId);
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function updateTimestamp($sessionId, $data)
+ {
+ return $this->write($sessionId, $data);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doWrite(string $sessionId, string $data)
+ {
+ return $this->handler->write($sessionId, $data);
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function destroy($sessionId)
+ {
+ $this->doDestroy = true;
+ $destroyed = parent::destroy($sessionId);
+
+ return $this->doDestroy ? $this->doDestroy($sessionId) : $destroyed;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doDestroy(string $sessionId)
+ {
+ $this->doDestroy = false;
+
+ return $this->handler->destroy($sessionId);
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function close()
+ {
+ return $this->handler->close();
+ }
+
+ /**
+ * @return int|false
+ */
+ #[\ReturnTypeWillChange]
+ public function gc($maxlifetime)
+ {
+ return $this->handler->gc($maxlifetime);
+ }
+}
diff --git a/symfony/http-foundation/Session/Storage/MetadataBag.php b/symfony/http-foundation/Session/Storage/MetadataBag.php
new file mode 100644
index 00000000..1bfce552
--- /dev/null
+++ b/symfony/http-foundation/Session/Storage/MetadataBag.php
@@ -0,0 +1,167 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Storage;
+
+use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
+
+/**
+ * Metadata container.
+ *
+ * Adds metadata to the session.
+ *
+ * @author Drak <drak@zikula.org>
+ */
+class MetadataBag implements SessionBagInterface
+{
+ public const CREATED = 'c';
+ public const UPDATED = 'u';
+ public const LIFETIME = 'l';
+
+ /**
+ * @var string
+ */
+ private $name = '__metadata';
+
+ /**
+ * @var string
+ */
+ private $storageKey;
+
+ /**
+ * @var array
+ */
+ protected $meta = [self::CREATED => 0, self::UPDATED => 0, self::LIFETIME => 0];
+
+ /**
+ * Unix timestamp.
+ *
+ * @var int
+ */
+ private $lastUsed;
+
+ /**
+ * @var int
+ */
+ private $updateThreshold;
+
+ /**
+ * @param string $storageKey The key used to store bag in the session
+ * @param int $updateThreshold The time to wait between two UPDATED updates
+ */
+ public function __construct(string $storageKey = '_sf2_meta', int $updateThreshold = 0)
+ {
+ $this->storageKey = $storageKey;
+ $this->updateThreshold = $updateThreshold;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function initialize(array &$array)
+ {
+ $this->meta = &$array;
+
+ if (isset($array[self::CREATED])) {
+ $this->lastUsed = $this->meta[self::UPDATED];
+
+ $timeStamp = time();
+ if ($timeStamp - $array[self::UPDATED] >= $this->updateThreshold) {
+ $this->meta[self::UPDATED] = $timeStamp;
+ }
+ } else {
+ $this->stampCreated();
+ }
+ }
+
+ /**
+ * Gets the lifetime that the session cookie was set with.
+ *
+ * @return int
+ */
+ public function getLifetime()
+ {
+ return $this->meta[self::LIFETIME];
+ }
+
+ /**
+ * Stamps a new session's metadata.
+ *
+ * @param int $lifetime Sets the cookie lifetime for the session cookie. A null value
+ * will leave the system settings unchanged, 0 sets the cookie
+ * to expire with browser session. Time is in seconds, and is
+ * not a Unix timestamp.
+ */
+ public function stampNew(int $lifetime = null)
+ {
+ $this->stampCreated($lifetime);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStorageKey()
+ {
+ return $this->storageKey;
+ }
+
+ /**
+ * Gets the created timestamp metadata.
+ *
+ * @return int Unix timestamp
+ */
+ public function getCreated()
+ {
+ return $this->meta[self::CREATED];
+ }
+
+ /**
+ * Gets the last used metadata.
+ *
+ * @return int Unix timestamp
+ */
+ public function getLastUsed()
+ {
+ return $this->lastUsed;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function clear()
+ {
+ // nothing to do
+ return null;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Sets name.
+ */
+ public function setName(string $name)
+ {
+ $this->name = $name;
+ }
+
+ private function stampCreated(int $lifetime = null): void
+ {
+ $timeStamp = time();
+ $this->meta[self::CREATED] = $this->meta[self::UPDATED] = $this->lastUsed = $timeStamp;
+ $this->meta[self::LIFETIME] = $lifetime ?? (int) ini_get('session.cookie_lifetime');
+ }
+}
diff --git a/symfony/http-foundation/Session/Storage/MockArraySessionStorage.php b/symfony/http-foundation/Session/Storage/MockArraySessionStorage.php
new file mode 100644
index 00000000..c5c2bb07
--- /dev/null
+++ b/symfony/http-foundation/Session/Storage/MockArraySessionStorage.php
@@ -0,0 +1,252 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Storage;
+
+use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
+
+/**
+ * MockArraySessionStorage mocks the session for unit tests.
+ *
+ * No PHP session is actually started since a session can be initialized
+ * and shutdown only once per PHP execution cycle.
+ *
+ * When doing functional testing, you should use MockFileSessionStorage instead.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Bulat Shakirzyanov <mallluhuct@gmail.com>
+ * @author Drak <drak@zikula.org>
+ */
+class MockArraySessionStorage implements SessionStorageInterface
+{
+ /**
+ * @var string
+ */
+ protected $id = '';
+
+ /**
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * @var bool
+ */
+ protected $started = false;
+
+ /**
+ * @var bool
+ */
+ protected $closed = false;
+
+ /**
+ * @var array
+ */
+ protected $data = [];
+
+ /**
+ * @var MetadataBag
+ */
+ protected $metadataBag;
+
+ /**
+ * @var array|SessionBagInterface[]
+ */
+ protected $bags = [];
+
+ public function __construct(string $name = 'MOCKSESSID', MetadataBag $metaBag = null)
+ {
+ $this->name = $name;
+ $this->setMetadataBag($metaBag);
+ }
+
+ public function setSessionData(array $array)
+ {
+ $this->data = $array;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function start()
+ {
+ if ($this->started) {
+ return true;
+ }
+
+ if (empty($this->id)) {
+ $this->id = $this->generateId();
+ }
+
+ $this->loadSession();
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function regenerate(bool $destroy = false, int $lifetime = null)
+ {
+ if (!$this->started) {
+ $this->start();
+ }
+
+ $this->metadataBag->stampNew($lifetime);
+ $this->id = $this->generateId();
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setId(string $id)
+ {
+ if ($this->started) {
+ throw new \LogicException('Cannot set session ID after the session has started.');
+ }
+
+ $this->id = $id;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setName(string $name)
+ {
+ $this->name = $name;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save()
+ {
+ if (!$this->started || $this->closed) {
+ throw new \RuntimeException('Trying to save a session that was not started yet or was already closed.');
+ }
+ // nothing to do since we don't persist the session data
+ $this->closed = false;
+ $this->started = false;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function clear()
+ {
+ // clear out the bags
+ foreach ($this->bags as $bag) {
+ $bag->clear();
+ }
+
+ // clear out the session
+ $this->data = [];
+
+ // reconnect the bags to the session
+ $this->loadSession();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function registerBag(SessionBagInterface $bag)
+ {
+ $this->bags[$bag->getName()] = $bag;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getBag(string $name)
+ {
+ if (!isset($this->bags[$name])) {
+ throw new \InvalidArgumentException(sprintf('The SessionBagInterface "%s" is not registered.', $name));
+ }
+
+ if (!$this->started) {
+ $this->start();
+ }
+
+ return $this->bags[$name];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isStarted()
+ {
+ return $this->started;
+ }
+
+ public function setMetadataBag(MetadataBag $bag = null)
+ {
+ if (null === $bag) {
+ $bag = new MetadataBag();
+ }
+
+ $this->metadataBag = $bag;
+ }
+
+ /**
+ * Gets the MetadataBag.
+ *
+ * @return MetadataBag
+ */
+ public function getMetadataBag()
+ {
+ return $this->metadataBag;
+ }
+
+ /**
+ * Generates a session ID.
+ *
+ * This doesn't need to be particularly cryptographically secure since this is just
+ * a mock.
+ *
+ * @return string
+ */
+ protected function generateId()
+ {
+ return hash('sha256', uniqid('ss_mock_', true));
+ }
+
+ protected function loadSession()
+ {
+ $bags = array_merge($this->bags, [$this->metadataBag]);
+
+ foreach ($bags as $bag) {
+ $key = $bag->getStorageKey();
+ $this->data[$key] = $this->data[$key] ?? [];
+ $bag->initialize($this->data[$key]);
+ }
+
+ $this->started = true;
+ $this->closed = false;
+ }
+}
diff --git a/symfony/http-foundation/Session/Storage/MockFileSessionStorage.php b/symfony/http-foundation/Session/Storage/MockFileSessionStorage.php
new file mode 100644
index 00000000..8e32a45e
--- /dev/null
+++ b/symfony/http-foundation/Session/Storage/MockFileSessionStorage.php
@@ -0,0 +1,160 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Storage;
+
+/**
+ * MockFileSessionStorage is used to mock sessions for
+ * functional testing where you may need to persist session data
+ * across separate PHP processes.
+ *
+ * No PHP session is actually started since a session can be initialized
+ * and shutdown only once per PHP execution cycle and this class does
+ * not pollute any session related globals, including session_*() functions
+ * or session.* PHP ini directives.
+ *
+ * @author Drak <drak@zikula.org>
+ */
+class MockFileSessionStorage extends MockArraySessionStorage
+{
+ private $savePath;
+
+ /**
+ * @param string|null $savePath Path of directory to save session files
+ */
+ public function __construct(string $savePath = null, string $name = 'MOCKSESSID', MetadataBag $metaBag = null)
+ {
+ if (null === $savePath) {
+ $savePath = sys_get_temp_dir();
+ }
+
+ if (!is_dir($savePath) && !@mkdir($savePath, 0777, true) && !is_dir($savePath)) {
+ throw new \RuntimeException(sprintf('Session Storage was not able to create directory "%s".', $savePath));
+ }
+
+ $this->savePath = $savePath;
+
+ parent::__construct($name, $metaBag);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function start()
+ {
+ if ($this->started) {
+ return true;
+ }
+
+ if (!$this->id) {
+ $this->id = $this->generateId();
+ }
+
+ $this->read();
+
+ $this->started = true;
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function regenerate(bool $destroy = false, int $lifetime = null)
+ {
+ if (!$this->started) {
+ $this->start();
+ }
+
+ if ($destroy) {
+ $this->destroy();
+ }
+
+ return parent::regenerate($destroy, $lifetime);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save()
+ {
+ if (!$this->started) {
+ throw new \RuntimeException('Trying to save a session that was not started yet or was already closed.');
+ }
+
+ $data = $this->data;
+
+ foreach ($this->bags as $bag) {
+ if (empty($data[$key = $bag->getStorageKey()])) {
+ unset($data[$key]);
+ }
+ }
+ if ([$key = $this->metadataBag->getStorageKey()] === array_keys($data)) {
+ unset($data[$key]);
+ }
+
+ try {
+ if ($data) {
+ $path = $this->getFilePath();
+ $tmp = $path.bin2hex(random_bytes(6));
+ file_put_contents($tmp, serialize($data));
+ rename($tmp, $path);
+ } else {
+ $this->destroy();
+ }
+ } finally {
+ $this->data = $data;
+ }
+
+ // this is needed when the session object is re-used across multiple requests
+ // in functional tests.
+ $this->started = false;
+ }
+
+ /**
+ * Deletes a session from persistent storage.
+ * Deliberately leaves session data in memory intact.
+ */
+ private function destroy(): void
+ {
+ set_error_handler(static function () {});
+ try {
+ unlink($this->getFilePath());
+ } finally {
+ restore_error_handler();
+ }
+ }
+
+ /**
+ * Calculate path to file.
+ */
+ private function getFilePath(): string
+ {
+ return $this->savePath.'/'.$this->id.'.mocksess';
+ }
+
+ /**
+ * Reads session from storage and loads session.
+ */
+ private function read(): void
+ {
+ set_error_handler(static function () {});
+ try {
+ $data = file_get_contents($this->getFilePath());
+ } finally {
+ restore_error_handler();
+ }
+
+ $this->data = $data ? unserialize($data) : [];
+
+ $this->loadSession();
+ }
+}
diff --git a/symfony/http-foundation/Session/Storage/MockFileSessionStorageFactory.php b/symfony/http-foundation/Session/Storage/MockFileSessionStorageFactory.php
new file mode 100644
index 00000000..d0da1e16
--- /dev/null
+++ b/symfony/http-foundation/Session/Storage/MockFileSessionStorageFactory.php
@@ -0,0 +1,42 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Storage;
+
+use Symfony\Component\HttpFoundation\Request;
+
+// Help opcache.preload discover always-needed symbols
+class_exists(MockFileSessionStorage::class);
+
+/**
+ * @author Jérémy Derussé <jeremy@derusse.com>
+ */
+class MockFileSessionStorageFactory implements SessionStorageFactoryInterface
+{
+ private $savePath;
+ private $name;
+ private $metaBag;
+
+ /**
+ * @see MockFileSessionStorage constructor.
+ */
+ public function __construct(string $savePath = null, string $name = 'MOCKSESSID', MetadataBag $metaBag = null)
+ {
+ $this->savePath = $savePath;
+ $this->name = $name;
+ $this->metaBag = $metaBag;
+ }
+
+ public function createStorage(?Request $request): SessionStorageInterface
+ {
+ return new MockFileSessionStorage($this->savePath, $this->name, $this->metaBag);
+ }
+}
diff --git a/symfony/http-foundation/Session/Storage/NativeSessionStorage.php b/symfony/http-foundation/Session/Storage/NativeSessionStorage.php
new file mode 100644
index 00000000..4440dccc
--- /dev/null
+++ b/symfony/http-foundation/Session/Storage/NativeSessionStorage.php
@@ -0,0 +1,476 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Storage;
+
+use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
+use Symfony\Component\HttpFoundation\Session\SessionUtils;
+use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler;
+use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy;
+use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy;
+
+// Help opcache.preload discover always-needed symbols
+class_exists(MetadataBag::class);
+class_exists(StrictSessionHandler::class);
+class_exists(SessionHandlerProxy::class);
+
+/**
+ * This provides a base class for session attribute storage.
+ *
+ * @author Drak <drak@zikula.org>
+ */
+class NativeSessionStorage implements SessionStorageInterface
+{
+ /**
+ * @var SessionBagInterface[]
+ */
+ protected $bags = [];
+
+ /**
+ * @var bool
+ */
+ protected $started = false;
+
+ /**
+ * @var bool
+ */
+ protected $closed = false;
+
+ /**
+ * @var AbstractProxy|\SessionHandlerInterface
+ */
+ protected $saveHandler;
+
+ /**
+ * @var MetadataBag
+ */
+ protected $metadataBag;
+
+ /**
+ * @var string|null
+ */
+ private $emulateSameSite;
+
+ /**
+ * Depending on how you want the storage driver to behave you probably
+ * want to override this constructor entirely.
+ *
+ * List of options for $options array with their defaults.
+ *
+ * @see https://php.net/session.configuration for options
+ * but we omit 'session.' from the beginning of the keys for convenience.
+ *
+ * ("auto_start", is not supported as it tells PHP to start a session before
+ * PHP starts to execute user-land code. Setting during runtime has no effect).
+ *
+ * cache_limiter, "" (use "0" to prevent headers from being sent entirely).
+ * cache_expire, "0"
+ * cookie_domain, ""
+ * cookie_httponly, ""
+ * cookie_lifetime, "0"
+ * cookie_path, "/"
+ * cookie_secure, ""
+ * cookie_samesite, null
+ * gc_divisor, "100"
+ * gc_maxlifetime, "1440"
+ * gc_probability, "1"
+ * lazy_write, "1"
+ * name, "PHPSESSID"
+ * referer_check, ""
+ * serialize_handler, "php"
+ * use_strict_mode, "1"
+ * use_cookies, "1"
+ * use_only_cookies, "1"
+ * use_trans_sid, "0"
+ * sid_length, "32"
+ * sid_bits_per_character, "5"
+ * trans_sid_hosts, $_SERVER['HTTP_HOST']
+ * trans_sid_tags, "a=href,area=href,frame=src,form="
+ *
+ * @param AbstractProxy|\SessionHandlerInterface|null $handler
+ */
+ public function __construct(array $options = [], $handler = null, MetadataBag $metaBag = null)
+ {
+ if (!\extension_loaded('session')) {
+ throw new \LogicException('PHP extension "session" is required.');
+ }
+
+ $options += [
+ 'cache_limiter' => '',
+ 'cache_expire' => 0,
+ 'use_cookies' => 1,
+ 'lazy_write' => 1,
+ 'use_strict_mode' => 1,
+ ];
+
+ session_register_shutdown();
+
+ $this->setMetadataBag($metaBag);
+ $this->setOptions($options);
+ $this->setSaveHandler($handler);
+ }
+
+ /**
+ * Gets the save handler instance.
+ *
+ * @return AbstractProxy|\SessionHandlerInterface
+ */
+ public function getSaveHandler()
+ {
+ return $this->saveHandler;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function start()
+ {
+ if ($this->started) {
+ return true;
+ }
+
+ if (\PHP_SESSION_ACTIVE === session_status()) {
+ throw new \RuntimeException('Failed to start the session: already started by PHP.');
+ }
+
+ if (filter_var(ini_get('session.use_cookies'), \FILTER_VALIDATE_BOOLEAN) && headers_sent($file, $line)) {
+ throw new \RuntimeException(sprintf('Failed to start the session because headers have already been sent by "%s" at line %d.', $file, $line));
+ }
+
+ $sessionId = $_COOKIE[session_name()] ?? null;
+ if ($sessionId && $this->saveHandler instanceof AbstractProxy && 'files' === $this->saveHandler->getSaveHandlerName() && !preg_match('/^[a-zA-Z0-9,-]{22,}$/', $sessionId)) {
+ // the session ID in the header is invalid, create a new one
+ session_id(session_create_id());
+ }
+
+ // ok to try and start the session
+ if (!session_start()) {
+ throw new \RuntimeException('Failed to start the session.');
+ }
+
+ if (null !== $this->emulateSameSite) {
+ $originalCookie = SessionUtils::popSessionCookie(session_name(), session_id());
+ if (null !== $originalCookie) {
+ header(sprintf('%s; SameSite=%s', $originalCookie, $this->emulateSameSite), false);
+ }
+ }
+
+ $this->loadSession();
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return $this->saveHandler->getId();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setId(string $id)
+ {
+ $this->saveHandler->setId($id);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return $this->saveHandler->getName();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setName(string $name)
+ {
+ $this->saveHandler->setName($name);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function regenerate(bool $destroy = false, int $lifetime = null)
+ {
+ // Cannot regenerate the session ID for non-active sessions.
+ if (\PHP_SESSION_ACTIVE !== session_status()) {
+ return false;
+ }
+
+ if (headers_sent()) {
+ return false;
+ }
+
+ if (null !== $lifetime && $lifetime != ini_get('session.cookie_lifetime')) {
+ $this->save();
+ ini_set('session.cookie_lifetime', $lifetime);
+ $this->start();
+ }
+
+ if ($destroy) {
+ $this->metadataBag->stampNew();
+ }
+
+ $isRegenerated = session_regenerate_id($destroy);
+
+ if (null !== $this->emulateSameSite) {
+ $originalCookie = SessionUtils::popSessionCookie(session_name(), session_id());
+ if (null !== $originalCookie) {
+ header(sprintf('%s; SameSite=%s', $originalCookie, $this->emulateSameSite), false);
+ }
+ }
+
+ return $isRegenerated;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save()
+ {
+ // Store a copy so we can restore the bags in case the session was not left empty
+ $session = $_SESSION;
+
+ foreach ($this->bags as $bag) {
+ if (empty($_SESSION[$key = $bag->getStorageKey()])) {
+ unset($_SESSION[$key]);
+ }
+ }
+ if ([$key = $this->metadataBag->getStorageKey()] === array_keys($_SESSION)) {
+ unset($_SESSION[$key]);
+ }
+
+ // Register error handler to add information about the current save handler
+ $previousHandler = set_error_handler(function ($type, $msg, $file, $line) use (&$previousHandler) {
+ if (\E_WARNING === $type && str_starts_with($msg, 'session_write_close():')) {
+ $handler = $this->saveHandler instanceof SessionHandlerProxy ? $this->saveHandler->getHandler() : $this->saveHandler;
+ $msg = sprintf('session_write_close(): Failed to write session data with "%s" handler', \get_class($handler));
+ }
+
+ return $previousHandler ? $previousHandler($type, $msg, $file, $line) : false;
+ });
+
+ try {
+ session_write_close();
+ } finally {
+ restore_error_handler();
+
+ // Restore only if not empty
+ if ($_SESSION) {
+ $_SESSION = $session;
+ }
+ }
+
+ $this->closed = true;
+ $this->started = false;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function clear()
+ {
+ // clear out the bags
+ foreach ($this->bags as $bag) {
+ $bag->clear();
+ }
+
+ // clear out the session
+ $_SESSION = [];
+
+ // reconnect the bags to the session
+ $this->loadSession();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function registerBag(SessionBagInterface $bag)
+ {
+ if ($this->started) {
+ throw new \LogicException('Cannot register a bag when the session is already started.');
+ }
+
+ $this->bags[$bag->getName()] = $bag;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getBag(string $name)
+ {
+ if (!isset($this->bags[$name])) {
+ throw new \InvalidArgumentException(sprintf('The SessionBagInterface "%s" is not registered.', $name));
+ }
+
+ if (!$this->started && $this->saveHandler->isActive()) {
+ $this->loadSession();
+ } elseif (!$this->started) {
+ $this->start();
+ }
+
+ return $this->bags[$name];
+ }
+
+ public function setMetadataBag(MetadataBag $metaBag = null)
+ {
+ if (null === $metaBag) {
+ $metaBag = new MetadataBag();
+ }
+
+ $this->metadataBag = $metaBag;
+ }
+
+ /**
+ * Gets the MetadataBag.
+ *
+ * @return MetadataBag
+ */
+ public function getMetadataBag()
+ {
+ return $this->metadataBag;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isStarted()
+ {
+ return $this->started;
+ }
+
+ /**
+ * Sets session.* ini variables.
+ *
+ * For convenience we omit 'session.' from the beginning of the keys.
+ * Explicitly ignores other ini keys.
+ *
+ * @param array $options Session ini directives [key => value]
+ *
+ * @see https://php.net/session.configuration
+ */
+ public function setOptions(array $options)
+ {
+ if (headers_sent() || \PHP_SESSION_ACTIVE === session_status()) {
+ return;
+ }
+
+ $validOptions = array_flip([
+ 'cache_expire', 'cache_limiter', 'cookie_domain', 'cookie_httponly',
+ 'cookie_lifetime', 'cookie_path', 'cookie_secure', 'cookie_samesite',
+ 'gc_divisor', 'gc_maxlifetime', 'gc_probability',
+ 'lazy_write', 'name', 'referer_check',
+ 'serialize_handler', 'use_strict_mode', 'use_cookies',
+ 'use_only_cookies', 'use_trans_sid', 'upload_progress.enabled',
+ 'upload_progress.cleanup', 'upload_progress.prefix', 'upload_progress.name',
+ 'upload_progress.freq', 'upload_progress.min_freq', 'url_rewriter.tags',
+ 'sid_length', 'sid_bits_per_character', 'trans_sid_hosts', 'trans_sid_tags',
+ ]);
+
+ foreach ($options as $key => $value) {
+ if (isset($validOptions[$key])) {
+ if (str_starts_with($key, 'upload_progress.')) {
+ trigger_deprecation('symfony/http-foundation', '5.4', 'Support for the "%s" session option is deprecated. The settings prefixed with "session.upload_progress." can not be changed at runtime.', $key);
+ continue;
+ }
+ if ('url_rewriter.tags' === $key) {
+ trigger_deprecation('symfony/http-foundation', '5.4', 'Support for the "%s" session option is deprecated. Use "trans_sid_tags" instead.', $key);
+ }
+ if ('cookie_samesite' === $key && \PHP_VERSION_ID < 70300) {
+ // PHP < 7.3 does not support same_site cookies. We will emulate it in
+ // the start() method instead.
+ $this->emulateSameSite = $value;
+ continue;
+ }
+ if ('cookie_secure' === $key && 'auto' === $value) {
+ continue;
+ }
+ ini_set('url_rewriter.tags' !== $key ? 'session.'.$key : $key, $value);
+ }
+ }
+ }
+
+ /**
+ * Registers session save handler as a PHP session handler.
+ *
+ * To use internal PHP session save handlers, override this method using ini_set with
+ * session.save_handler and session.save_path e.g.
+ *
+ * ini_set('session.save_handler', 'files');
+ * ini_set('session.save_path', '/tmp');
+ *
+ * or pass in a \SessionHandler instance which configures session.save_handler in the
+ * constructor, for a template see NativeFileSessionHandler.
+ *
+ * @see https://php.net/session-set-save-handler
+ * @see https://php.net/sessionhandlerinterface
+ * @see https://php.net/sessionhandler
+ *
+ * @param AbstractProxy|\SessionHandlerInterface|null $saveHandler
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function setSaveHandler($saveHandler = null)
+ {
+ if (!$saveHandler instanceof AbstractProxy &&
+ !$saveHandler instanceof \SessionHandlerInterface &&
+ null !== $saveHandler) {
+ throw new \InvalidArgumentException('Must be instance of AbstractProxy; implement \SessionHandlerInterface; or be null.');
+ }
+
+ // Wrap $saveHandler in proxy and prevent double wrapping of proxy
+ if (!$saveHandler instanceof AbstractProxy && $saveHandler instanceof \SessionHandlerInterface) {
+ $saveHandler = new SessionHandlerProxy($saveHandler);
+ } elseif (!$saveHandler instanceof AbstractProxy) {
+ $saveHandler = new SessionHandlerProxy(new StrictSessionHandler(new \SessionHandler()));
+ }
+ $this->saveHandler = $saveHandler;
+
+ if (headers_sent() || \PHP_SESSION_ACTIVE === session_status()) {
+ return;
+ }
+
+ if ($this->saveHandler instanceof SessionHandlerProxy) {
+ session_set_save_handler($this->saveHandler, false);
+ }
+ }
+
+ /**
+ * Load the session with attributes.
+ *
+ * After starting the session, PHP retrieves the session from whatever handlers
+ * are set to (either PHP's internal, or a custom save handler set with session_set_save_handler()).
+ * PHP takes the return value from the read() handler, unserializes it
+ * and populates $_SESSION with the result automatically.
+ */
+ protected function loadSession(array &$session = null)
+ {
+ if (null === $session) {
+ $session = &$_SESSION;
+ }
+
+ $bags = array_merge($this->bags, [$this->metadataBag]);
+
+ foreach ($bags as $bag) {
+ $key = $bag->getStorageKey();
+ $session[$key] = isset($session[$key]) && \is_array($session[$key]) ? $session[$key] : [];
+ $bag->initialize($session[$key]);
+ }
+
+ $this->started = true;
+ $this->closed = false;
+ }
+}
diff --git a/symfony/http-foundation/Session/Storage/NativeSessionStorageFactory.php b/symfony/http-foundation/Session/Storage/NativeSessionStorageFactory.php
new file mode 100644
index 00000000..a7d7411f
--- /dev/null
+++ b/symfony/http-foundation/Session/Storage/NativeSessionStorageFactory.php
@@ -0,0 +1,49 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Storage;
+
+use Symfony\Component\HttpFoundation\Request;
+
+// Help opcache.preload discover always-needed symbols
+class_exists(NativeSessionStorage::class);
+
+/**
+ * @author Jérémy Derussé <jeremy@derusse.com>
+ */
+class NativeSessionStorageFactory implements SessionStorageFactoryInterface
+{
+ private $options;
+ private $handler;
+ private $metaBag;
+ private $secure;
+
+ /**
+ * @see NativeSessionStorage constructor.
+ */
+ public function __construct(array $options = [], $handler = null, MetadataBag $metaBag = null, bool $secure = false)
+ {
+ $this->options = $options;
+ $this->handler = $handler;
+ $this->metaBag = $metaBag;
+ $this->secure = $secure;
+ }
+
+ public function createStorage(?Request $request): SessionStorageInterface
+ {
+ $storage = new NativeSessionStorage($this->options, $this->handler, $this->metaBag);
+ if ($this->secure && $request && $request->isSecure()) {
+ $storage->setOptions(['cookie_secure' => true]);
+ }
+
+ return $storage;
+ }
+}
diff --git a/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorage.php b/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorage.php
new file mode 100644
index 00000000..72dbef13
--- /dev/null
+++ b/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorage.php
@@ -0,0 +1,64 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Storage;
+
+use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy;
+
+/**
+ * Allows session to be started by PHP and managed by Symfony.
+ *
+ * @author Drak <drak@zikula.org>
+ */
+class PhpBridgeSessionStorage extends NativeSessionStorage
+{
+ /**
+ * @param AbstractProxy|\SessionHandlerInterface|null $handler
+ */
+ public function __construct($handler = null, MetadataBag $metaBag = null)
+ {
+ if (!\extension_loaded('session')) {
+ throw new \LogicException('PHP extension "session" is required.');
+ }
+
+ $this->setMetadataBag($metaBag);
+ $this->setSaveHandler($handler);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function start()
+ {
+ if ($this->started) {
+ return true;
+ }
+
+ $this->loadSession();
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function clear()
+ {
+ // clear out the bags and nothing else that may be set
+ // since the purpose of this driver is to share a handler
+ foreach ($this->bags as $bag) {
+ $bag->clear();
+ }
+
+ // reconnect the bags to the session
+ $this->loadSession();
+ }
+}
diff --git a/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorageFactory.php b/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorageFactory.php
new file mode 100644
index 00000000..173ef71d
--- /dev/null
+++ b/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorageFactory.php
@@ -0,0 +1,47 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Storage;
+
+use Symfony\Component\HttpFoundation\Request;
+
+// Help opcache.preload discover always-needed symbols
+class_exists(PhpBridgeSessionStorage::class);
+
+/**
+ * @author Jérémy Derussé <jeremy@derusse.com>
+ */
+class PhpBridgeSessionStorageFactory implements SessionStorageFactoryInterface
+{
+ private $handler;
+ private $metaBag;
+ private $secure;
+
+ /**
+ * @see PhpBridgeSessionStorage constructor.
+ */
+ public function __construct($handler = null, MetadataBag $metaBag = null, bool $secure = false)
+ {
+ $this->handler = $handler;
+ $this->metaBag = $metaBag;
+ $this->secure = $secure;
+ }
+
+ public function createStorage(?Request $request): SessionStorageInterface
+ {
+ $storage = new PhpBridgeSessionStorage($this->handler, $this->metaBag);
+ if ($this->secure && $request && $request->isSecure()) {
+ $storage->setOptions(['cookie_secure' => true]);
+ }
+
+ return $storage;
+ }
+}
diff --git a/symfony/http-foundation/Session/Storage/Proxy/AbstractProxy.php b/symfony/http-foundation/Session/Storage/Proxy/AbstractProxy.php
new file mode 100644
index 00000000..edd04dff
--- /dev/null
+++ b/symfony/http-foundation/Session/Storage/Proxy/AbstractProxy.php
@@ -0,0 +1,118 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Storage\Proxy;
+
+/**
+ * @author Drak <drak@zikula.org>
+ */
+abstract class AbstractProxy
+{
+ /**
+ * Flag if handler wraps an internal PHP session handler (using \SessionHandler).
+ *
+ * @var bool
+ */
+ protected $wrapper = false;
+
+ /**
+ * @var string
+ */
+ protected $saveHandlerName;
+
+ /**
+ * Gets the session.save_handler name.
+ *
+ * @return string|null
+ */
+ public function getSaveHandlerName()
+ {
+ return $this->saveHandlerName;
+ }
+
+ /**
+ * Is this proxy handler and instance of \SessionHandlerInterface.
+ *
+ * @return bool
+ */
+ public function isSessionHandlerInterface()
+ {
+ return $this instanceof \SessionHandlerInterface;
+ }
+
+ /**
+ * Returns true if this handler wraps an internal PHP session save handler using \SessionHandler.
+ *
+ * @return bool
+ */
+ public function isWrapper()
+ {
+ return $this->wrapper;
+ }
+
+ /**
+ * Has a session started?
+ *
+ * @return bool
+ */
+ public function isActive()
+ {
+ return \PHP_SESSION_ACTIVE === session_status();
+ }
+
+ /**
+ * Gets the session ID.
+ *
+ * @return string
+ */
+ public function getId()
+ {
+ return session_id();
+ }
+
+ /**
+ * Sets the session ID.
+ *
+ * @throws \LogicException
+ */
+ public function setId(string $id)
+ {
+ if ($this->isActive()) {
+ throw new \LogicException('Cannot change the ID of an active session.');
+ }
+
+ session_id($id);
+ }
+
+ /**
+ * Gets the session name.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return session_name();
+ }
+
+ /**
+ * Sets the session name.
+ *
+ * @throws \LogicException
+ */
+ public function setName(string $name)
+ {
+ if ($this->isActive()) {
+ throw new \LogicException('Cannot change the name of an active session.');
+ }
+
+ session_name($name);
+ }
+}
diff --git a/symfony/http-foundation/Session/Storage/Proxy/SessionHandlerProxy.php b/symfony/http-foundation/Session/Storage/Proxy/SessionHandlerProxy.php
new file mode 100644
index 00000000..9b0cdeb7
--- /dev/null
+++ b/symfony/http-foundation/Session/Storage/Proxy/SessionHandlerProxy.php
@@ -0,0 +1,109 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Storage\Proxy;
+
+/**
+ * @author Drak <drak@zikula.org>
+ */
+class SessionHandlerProxy extends AbstractProxy implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface
+{
+ protected $handler;
+
+ public function __construct(\SessionHandlerInterface $handler)
+ {
+ $this->handler = $handler;
+ $this->wrapper = $handler instanceof \SessionHandler;
+ $this->saveHandlerName = $this->wrapper ? ini_get('session.save_handler') : 'user';
+ }
+
+ /**
+ * @return \SessionHandlerInterface
+ */
+ public function getHandler()
+ {
+ return $this->handler;
+ }
+
+ // \SessionHandlerInterface
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function open($savePath, $sessionName)
+ {
+ return $this->handler->open($savePath, $sessionName);
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function close()
+ {
+ return $this->handler->close();
+ }
+
+ /**
+ * @return string|false
+ */
+ #[\ReturnTypeWillChange]
+ public function read($sessionId)
+ {
+ return $this->handler->read($sessionId);
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function write($sessionId, $data)
+ {
+ return $this->handler->write($sessionId, $data);
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function destroy($sessionId)
+ {
+ return $this->handler->destroy($sessionId);
+ }
+
+ /**
+ * @return int|false
+ */
+ #[\ReturnTypeWillChange]
+ public function gc($maxlifetime)
+ {
+ return $this->handler->gc($maxlifetime);
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function validateId($sessionId)
+ {
+ return !$this->handler instanceof \SessionUpdateTimestampHandlerInterface || $this->handler->validateId($sessionId);
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function updateTimestamp($sessionId, $data)
+ {
+ return $this->handler instanceof \SessionUpdateTimestampHandlerInterface ? $this->handler->updateTimestamp($sessionId, $data) : $this->write($sessionId, $data);
+ }
+}
diff --git a/symfony/http-foundation/Session/Storage/ServiceSessionFactory.php b/symfony/http-foundation/Session/Storage/ServiceSessionFactory.php
new file mode 100644
index 00000000..d17c60ae
--- /dev/null
+++ b/symfony/http-foundation/Session/Storage/ServiceSessionFactory.php
@@ -0,0 +1,38 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Storage;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * @author Jérémy Derussé <jeremy@derusse.com>
+ *
+ * @internal to be removed in Symfony 6
+ */
+final class ServiceSessionFactory implements SessionStorageFactoryInterface
+{
+ private $storage;
+
+ public function __construct(SessionStorageInterface $storage)
+ {
+ $this->storage = $storage;
+ }
+
+ public function createStorage(?Request $request): SessionStorageInterface
+ {
+ if ($this->storage instanceof NativeSessionStorage && $request && $request->isSecure()) {
+ $this->storage->setOptions(['cookie_secure' => true]);
+ }
+
+ return $this->storage;
+ }
+}
diff --git a/symfony/http-foundation/Session/Storage/SessionStorageFactoryInterface.php b/symfony/http-foundation/Session/Storage/SessionStorageFactoryInterface.php
new file mode 100644
index 00000000..d03f0da4
--- /dev/null
+++ b/symfony/http-foundation/Session/Storage/SessionStorageFactoryInterface.php
@@ -0,0 +1,25 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Storage;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * @author Jérémy Derussé <jeremy@derusse.com>
+ */
+interface SessionStorageFactoryInterface
+{
+ /**
+ * Creates a new instance of SessionStorageInterface.
+ */
+ public function createStorage(?Request $request): SessionStorageInterface;
+}
diff --git a/symfony/http-foundation/Session/Storage/SessionStorageInterface.php b/symfony/http-foundation/Session/Storage/SessionStorageInterface.php
new file mode 100644
index 00000000..b7f66e7c
--- /dev/null
+++ b/symfony/http-foundation/Session/Storage/SessionStorageInterface.php
@@ -0,0 +1,131 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Storage;
+
+use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
+
+/**
+ * StorageInterface.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Drak <drak@zikula.org>
+ */
+interface SessionStorageInterface
+{
+ /**
+ * Starts the session.
+ *
+ * @return bool
+ *
+ * @throws \RuntimeException if something goes wrong starting the session
+ */
+ public function start();
+
+ /**
+ * Checks if the session is started.
+ *
+ * @return bool
+ */
+ public function isStarted();
+
+ /**
+ * Returns the session ID.
+ *
+ * @return string
+ */
+ public function getId();
+
+ /**
+ * Sets the session ID.
+ */
+ public function setId(string $id);
+
+ /**
+ * Returns the session name.
+ *
+ * @return string
+ */
+ public function getName();
+
+ /**
+ * Sets the session name.
+ */
+ public function setName(string $name);
+
+ /**
+ * Regenerates id that represents this storage.
+ *
+ * This method must invoke session_regenerate_id($destroy) unless
+ * this interface is used for a storage object designed for unit
+ * or functional testing where a real PHP session would interfere
+ * with testing.
+ *
+ * Note regenerate+destroy should not clear the session data in memory
+ * only delete the session data from persistent storage.
+ *
+ * Care: When regenerating the session ID no locking is involved in PHP's
+ * session design. See https://bugs.php.net/61470 for a discussion.
+ * So you must make sure the regenerated session is saved BEFORE sending the
+ * headers with the new ID. Symfony's HttpKernel offers a listener for this.
+ * See Symfony\Component\HttpKernel\EventListener\SaveSessionListener.
+ * Otherwise session data could get lost again for concurrent requests with the
+ * new ID. One result could be that you get logged out after just logging in.
+ *
+ * @param bool $destroy Destroy session when regenerating?
+ * @param int $lifetime Sets the cookie lifetime for the session cookie. A null value
+ * will leave the system settings unchanged, 0 sets the cookie
+ * to expire with browser session. Time is in seconds, and is
+ * not a Unix timestamp.
+ *
+ * @return bool
+ *
+ * @throws \RuntimeException If an error occurs while regenerating this storage
+ */
+ public function regenerate(bool $destroy = false, int $lifetime = null);
+
+ /**
+ * Force the session to be saved and closed.
+ *
+ * This method must invoke session_write_close() unless this interface is
+ * used for a storage object design for unit or functional testing where
+ * a real PHP session would interfere with testing, in which case
+ * it should actually persist the session data if required.
+ *
+ * @throws \RuntimeException if the session is saved without being started, or if the session
+ * is already closed
+ */
+ public function save();
+
+ /**
+ * Clear all session data in memory.
+ */
+ public function clear();
+
+ /**
+ * Gets a SessionBagInterface by name.
+ *
+ * @return SessionBagInterface
+ *
+ * @throws \InvalidArgumentException If the bag does not exist
+ */
+ public function getBag(string $name);
+
+ /**
+ * Registers a SessionBagInterface for use.
+ */
+ public function registerBag(SessionBagInterface $bag);
+
+ /**
+ * @return MetadataBag
+ */
+ public function getMetadataBag();
+}
diff --git a/symfony/http-foundation/StreamedResponse.php b/symfony/http-foundation/StreamedResponse.php
new file mode 100644
index 00000000..676cd668
--- /dev/null
+++ b/symfony/http-foundation/StreamedResponse.php
@@ -0,0 +1,139 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation;
+
+/**
+ * StreamedResponse represents a streamed HTTP response.
+ *
+ * A StreamedResponse uses a callback for its content.
+ *
+ * The callback should use the standard PHP functions like echo
+ * to stream the response back to the client. The flush() function
+ * can also be used if needed.
+ *
+ * @see flush()
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class StreamedResponse extends Response
+{
+ protected $callback;
+ protected $streamed;
+ private $headersSent;
+
+ public function __construct(callable $callback = null, int $status = 200, array $headers = [])
+ {
+ parent::__construct(null, $status, $headers);
+
+ if (null !== $callback) {
+ $this->setCallback($callback);
+ }
+ $this->streamed = false;
+ $this->headersSent = false;
+ }
+
+ /**
+ * Factory method for chainability.
+ *
+ * @param callable|null $callback A valid PHP callback or null to set it later
+ *
+ * @return static
+ *
+ * @deprecated since Symfony 5.1, use __construct() instead.
+ */
+ public static function create($callback = null, int $status = 200, array $headers = [])
+ {
+ trigger_deprecation('symfony/http-foundation', '5.1', 'The "%s()" method is deprecated, use "new %s()" instead.', __METHOD__, static::class);
+
+ return new static($callback, $status, $headers);
+ }
+
+ /**
+ * Sets the PHP callback associated with this Response.
+ *
+ * @return $this
+ */
+ public function setCallback(callable $callback)
+ {
+ $this->callback = $callback;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * This method only sends the headers once.
+ *
+ * @return $this
+ */
+ public function sendHeaders()
+ {
+ if ($this->headersSent) {
+ return $this;
+ }
+
+ $this->headersSent = true;
+
+ return parent::sendHeaders();
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * This method only sends the content once.
+ *
+ * @return $this
+ */
+ public function sendContent()
+ {
+ if ($this->streamed) {
+ return $this;
+ }
+
+ $this->streamed = true;
+
+ if (null === $this->callback) {
+ throw new \LogicException('The Response callback must not be null.');
+ }
+
+ ($this->callback)();
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @throws \LogicException when the content is not null
+ *
+ * @return $this
+ */
+ public function setContent(?string $content)
+ {
+ if (null !== $content) {
+ throw new \LogicException('The content cannot be set on a StreamedResponse instance.');
+ }
+
+ $this->streamed = true;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getContent()
+ {
+ return false;
+ }
+}
diff --git a/symfony/http-foundation/UrlHelper.php b/symfony/http-foundation/UrlHelper.php
new file mode 100644
index 00000000..c15f101c
--- /dev/null
+++ b/symfony/http-foundation/UrlHelper.php
@@ -0,0 +1,102 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation;
+
+use Symfony\Component\Routing\RequestContext;
+
+/**
+ * A helper service for manipulating URLs within and outside the request scope.
+ *
+ * @author Valentin Udaltsov <udaltsov.valentin@gmail.com>
+ */
+final class UrlHelper
+{
+ private $requestStack;
+ private $requestContext;
+
+ public function __construct(RequestStack $requestStack, RequestContext $requestContext = null)
+ {
+ $this->requestStack = $requestStack;
+ $this->requestContext = $requestContext;
+ }
+
+ public function getAbsoluteUrl(string $path): string
+ {
+ if (str_contains($path, '://') || '//' === substr($path, 0, 2)) {
+ return $path;
+ }
+
+ if (null === $request = $this->requestStack->getMainRequest()) {
+ return $this->getAbsoluteUrlFromContext($path);
+ }
+
+ if ('#' === $path[0]) {
+ $path = $request->getRequestUri().$path;
+ } elseif ('?' === $path[0]) {
+ $path = $request->getPathInfo().$path;
+ }
+
+ if (!$path || '/' !== $path[0]) {
+ $prefix = $request->getPathInfo();
+ $last = \strlen($prefix) - 1;
+ if ($last !== $pos = strrpos($prefix, '/')) {
+ $prefix = substr($prefix, 0, $pos).'/';
+ }
+
+ return $request->getUriForPath($prefix.$path);
+ }
+
+ return $request->getSchemeAndHttpHost().$path;
+ }
+
+ public function getRelativePath(string $path): string
+ {
+ if (str_contains($path, '://') || '//' === substr($path, 0, 2)) {
+ return $path;
+ }
+
+ if (null === $request = $this->requestStack->getMainRequest()) {
+ return $path;
+ }
+
+ return $request->getRelativeUriForPath($path);
+ }
+
+ private function getAbsoluteUrlFromContext(string $path): string
+ {
+ if (null === $this->requestContext || '' === $host = $this->requestContext->getHost()) {
+ return $path;
+ }
+
+ $scheme = $this->requestContext->getScheme();
+ $port = '';
+
+ if ('http' === $scheme && 80 !== $this->requestContext->getHttpPort()) {
+ $port = ':'.$this->requestContext->getHttpPort();
+ } elseif ('https' === $scheme && 443 !== $this->requestContext->getHttpsPort()) {
+ $port = ':'.$this->requestContext->getHttpsPort();
+ }
+
+ if ('#' === $path[0]) {
+ $queryString = $this->requestContext->getQueryString();
+ $path = $this->requestContext->getPathInfo().($queryString ? '?'.$queryString : '').$path;
+ } elseif ('?' === $path[0]) {
+ $path = $this->requestContext->getPathInfo().$path;
+ }
+
+ if ('/' !== $path[0]) {
+ $path = rtrim($this->requestContext->getBaseUrl(), '/').'/'.$path;
+ }
+
+ return $scheme.'://'.$host.$port.$path;
+ }
+}
diff --git a/symfony/http-foundation/composer.json b/symfony/http-foundation/composer.json
new file mode 100644
index 00000000..d54bbfd1
--- /dev/null
+++ b/symfony/http-foundation/composer.json
@@ -0,0 +1,40 @@
+{
+ "name": "symfony/http-foundation",
+ "type": "library",
+ "description": "Defines an object-oriented layer for the HTTP specification",
+ "keywords": [],
+ "homepage": "https://symfony.com",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/deprecation-contracts": "^2.1|^3",
+ "symfony/polyfill-mbstring": "~1.1",
+ "symfony/polyfill-php80": "^1.16"
+ },
+ "require-dev": {
+ "predis/predis": "~1.0",
+ "symfony/cache": "^4.4|^5.0|^6.0",
+ "symfony/mime": "^4.4|^5.0|^6.0",
+ "symfony/expression-language": "^4.4|^5.0|^6.0"
+ },
+ "suggest" : {
+ "symfony/mime": "To use the file extension guesser"
+ },
+ "autoload": {
+ "psr-4": { "Symfony\\Component\\HttpFoundation\\": "" },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "minimum-stability": "dev"
+}
diff --git a/symfony/polyfill-mbstring/Mbstring.php b/symfony/polyfill-mbstring/Mbstring.php
index b5990956..693749f2 100644
--- a/symfony/polyfill-mbstring/Mbstring.php
+++ b/symfony/polyfill-mbstring/Mbstring.php
@@ -80,7 +80,7 @@ final class Mbstring
public static function mb_convert_encoding($s, $toEncoding, $fromEncoding = null)
{
- if (\is_array($fromEncoding) || false !== strpos($fromEncoding, ',')) {
+ if (\is_array($fromEncoding) || ($fromEncoding !== null && false !== strpos($fromEncoding, ','))) {
$fromEncoding = self::mb_detect_encoding($s, $fromEncoding);
} else {
$fromEncoding = self::getEncoding($fromEncoding);
@@ -568,7 +568,7 @@ final class Mbstring
}
$rx .= '.{'.$split_length.'})/us';
- return preg_split($rx, $string, null, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY);
+ return preg_split($rx, $string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY);
}
$result = [];
@@ -602,6 +602,9 @@ final class Mbstring
if (80000 > \PHP_VERSION_ID) {
return false;
}
+ if (\is_int($c) || 'long' === $c || 'entity' === $c) {
+ return false;
+ }
throw new \ValueError('Argument #1 ($substitute_character) must be "none", "long", "entity" or a valid codepoint');
}
diff --git a/symfony/polyfill-mbstring/README.md b/symfony/polyfill-mbstring/README.md
index 4efb599d..478b40da 100644
--- a/symfony/polyfill-mbstring/README.md
+++ b/symfony/polyfill-mbstring/README.md
@@ -5,7 +5,7 @@ This component provides a partial, native PHP implementation for the
[Mbstring](https://php.net/mbstring) extension.
More information can be found in the
-[main Polyfill README](https://github.com/symfony/polyfill/blob/master/README.md).
+[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md).
License
=======
diff --git a/symfony/polyfill-mbstring/composer.json b/symfony/polyfill-mbstring/composer.json
index 2ed7a743..9cd2e924 100644
--- a/symfony/polyfill-mbstring/composer.json
+++ b/symfony/polyfill-mbstring/composer.json
@@ -18,6 +18,9 @@
"require": {
"php": ">=7.1"
},
+ "provide": {
+ "ext-mbstring": "*"
+ },
"autoload": {
"psr-4": { "Symfony\\Polyfill\\Mbstring\\": "" },
"files": [ "bootstrap.php" ]
@@ -28,7 +31,7 @@
"minimum-stability": "dev",
"extra": {
"branch-alias": {
- "dev-main": "1.23-dev"
+ "dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
diff --git a/symfony/polyfill-php80/Php80.php b/symfony/polyfill-php80/Php80.php
index 5fef5118..362dd1a9 100644
--- a/symfony/polyfill-php80/Php80.php
+++ b/symfony/polyfill-php80/Php80.php
@@ -100,6 +100,16 @@ final class Php80
public static function str_ends_with(string $haystack, string $needle): bool
{
- return '' === $needle || ('' !== $haystack && 0 === substr_compare($haystack, $needle, -\strlen($needle)));
+ if ('' === $needle || $needle === $haystack) {
+ return true;
+ }
+
+ if ('' === $haystack) {
+ return false;
+ }
+
+ $needleLength = \strlen($needle);
+
+ return $needleLength <= \strlen($haystack) && 0 === substr_compare($haystack, $needle, -$needleLength);
}
}
diff --git a/symfony/polyfill-php80/PhpToken.php b/symfony/polyfill-php80/PhpToken.php
new file mode 100644
index 00000000..fe6e6910
--- /dev/null
+++ b/symfony/polyfill-php80/PhpToken.php
@@ -0,0 +1,103 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Polyfill\Php80;
+
+/**
+ * @author Fedonyuk Anton <info@ensostudio.ru>
+ *
+ * @internal
+ */
+class PhpToken implements \Stringable
+{
+ /**
+ * @var int
+ */
+ public $id;
+
+ /**
+ * @var string
+ */
+ public $text;
+
+ /**
+ * @var int
+ */
+ public $line;
+
+ /**
+ * @var int
+ */
+ public $pos;
+
+ public function __construct(int $id, string $text, int $line = -1, int $position = -1)
+ {
+ $this->id = $id;
+ $this->text = $text;
+ $this->line = $line;
+ $this->pos = $position;
+ }
+
+ public function getTokenName(): ?string
+ {
+ if ('UNKNOWN' === $name = token_name($this->id)) {
+ $name = \strlen($this->text) > 1 || \ord($this->text) < 32 ? null : $this->text;
+ }
+
+ return $name;
+ }
+
+ /**
+ * @param int|string|array $kind
+ */
+ public function is($kind): bool
+ {
+ foreach ((array) $kind as $value) {
+ if (\in_array($value, [$this->id, $this->text], true)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public function isIgnorable(): bool
+ {
+ return \in_array($this->id, [\T_WHITESPACE, \T_COMMENT, \T_DOC_COMMENT, \T_OPEN_TAG], true);
+ }
+
+ public function __toString(): string
+ {
+ return (string) $this->text;
+ }
+
+ /**
+ * @return static[]
+ */
+ public static function tokenize(string $code, int $flags = 0): array
+ {
+ $line = 1;
+ $position = 0;
+ $tokens = token_get_all($code, $flags);
+ foreach ($tokens as $index => $token) {
+ if (\is_string($token)) {
+ $id = \ord($token);
+ $text = $token;
+ } else {
+ [$id, $text, $line] = $token;
+ }
+ $tokens[$index] = new static($id, $text, $line, $position);
+ $position += \strlen($text);
+ }
+
+ return $tokens;
+ }
+}
diff --git a/symfony/polyfill-php80/README.md b/symfony/polyfill-php80/README.md
index 10b8ee49..3816c559 100644
--- a/symfony/polyfill-php80/README.md
+++ b/symfony/polyfill-php80/README.md
@@ -3,12 +3,13 @@ Symfony Polyfill / Php80
This component provides features added to PHP 8.0 core:
-- `Stringable` interface
+- [`Stringable`](https://php.net/stringable) interface
- [`fdiv`](https://php.net/fdiv)
-- `ValueError` class
-- `UnhandledMatchError` class
+- [`ValueError`](https://php.net/valueerror) class
+- [`UnhandledMatchError`](https://php.net/unhandledmatcherror) class
- `FILTER_VALIDATE_BOOL` constant
- [`get_debug_type`](https://php.net/get_debug_type)
+- [`PhpToken`](https://php.net/phptoken) class
- [`preg_last_error_msg`](https://php.net/preg_last_error_msg)
- [`str_contains`](https://php.net/str_contains)
- [`str_starts_with`](https://php.net/str_starts_with)
diff --git a/symfony/polyfill-php80/Resources/stubs/PhpToken.php b/symfony/polyfill-php80/Resources/stubs/PhpToken.php
new file mode 100644
index 00000000..72f10812
--- /dev/null
+++ b/symfony/polyfill-php80/Resources/stubs/PhpToken.php
@@ -0,0 +1,7 @@
+<?php
+
+if (\PHP_VERSION_ID < 80000 && \extension_loaded('tokenizer')) {
+ class PhpToken extends Symfony\Polyfill\Php80\PhpToken
+ {
+ }
+}
diff --git a/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php b/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php
index 7fb2000e..37937cbf 100644
--- a/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php
+++ b/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php
@@ -1,5 +1,7 @@
<?php
-class UnhandledMatchError extends Error
-{
+if (\PHP_VERSION_ID < 80000) {
+ class UnhandledMatchError extends Error
+ {
+ }
}
diff --git a/symfony/polyfill-php80/Resources/stubs/ValueError.php b/symfony/polyfill-php80/Resources/stubs/ValueError.php
index 99843cad..a3a9b88b 100644
--- a/symfony/polyfill-php80/Resources/stubs/ValueError.php
+++ b/symfony/polyfill-php80/Resources/stubs/ValueError.php
@@ -1,5 +1,7 @@
<?php
-class ValueError extends Error
-{
+if (\PHP_VERSION_ID < 80000) {
+ class ValueError extends Error
+ {
+ }
}
diff --git a/symfony/polyfill-php80/composer.json b/symfony/polyfill-php80/composer.json
index 5fe679db..cd3e9b65 100644
--- a/symfony/polyfill-php80/composer.json
+++ b/symfony/polyfill-php80/composer.json
@@ -30,7 +30,7 @@
"minimum-stability": "dev",
"extra": {
"branch-alias": {
- "dev-main": "1.23-dev"
+ "dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",