+ */
+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.
+ *
+ * @see MimeTypes
+ * @see getMimeType()
+ */
+ public function guessExtension(): ?string
+ {
+ 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.
+ *
+ * @see MimeTypes
+ */
+ public function getMimeType(): ?string
+ {
+ 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.
+ *
+ * @throws FileException if the target file could not be created
+ */
+ public function move(string $directory, ?string $name = null): self
+ {
+ $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, 0o666 & ~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;
+ }
+
+ protected function getTargetFile(string $directory, ?string $name = null): self
+ {
+ if (!is_dir($directory) && !@mkdir($directory, 0o777, true) && !is_dir($directory)) {
+ if (is_file($directory)) {
+ throw new FileException(\sprintf('Unable to create the "%s" directory: a similarly-named file exists.', $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.
+ */
+ protected function getName(string $name): string
+ {
+ $originalName = str_replace('\\', '/', $name);
+ $pos = strrpos($originalName, '/');
+
+ return false === $pos ? $originalName : substr($originalName, $pos + 1);
+ }
+}
diff --git a/vendor/symfony/http-foundation/File/Stream.php b/vendor/symfony/http-foundation/File/Stream.php
new file mode 100644
index 0000000..2c156b2
--- /dev/null
+++ b/vendor/symfony/http-foundation/File/Stream.php
@@ -0,0 +1,25 @@
+
+ *
+ * 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
+ */
+class Stream extends File
+{
+ public function getSize(): int|false
+ {
+ return false;
+ }
+}
diff --git a/vendor/symfony/http-foundation/File/UploadedFile.php b/vendor/symfony/http-foundation/File/UploadedFile.php
new file mode 100644
index 0000000..53dce7d
--- /dev/null
+++ b/vendor/symfony/http-foundation/File/UploadedFile.php
@@ -0,0 +1,289 @@
+
+ *
+ * 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
+ * @author Florian Eckerstorfer
+ * @author Fabien Potencier
+ */
+class UploadedFile extends File
+{
+ private string $originalName;
+ private string $mimeType;
+ private int $error;
+ private string $originalPath;
+
+ /**
+ * 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,
+ private bool $test = false,
+ ) {
+ $this->originalName = $this->getName($originalName);
+ $this->originalPath = strtr($originalName, '\\', '/');
+ $this->mimeType = $mimeType ?: 'application/octet-stream';
+ $this->error = $error ?: \UPLOAD_ERR_OK;
+
+ 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.
+ * This should not be considered as a safe value to use for a file name on your servers.
+ */
+ public function getClientOriginalName(): string
+ {
+ return $this->originalName;
+ }
+
+ /**
+ * Returns the original file extension.
+ *
+ * It is extracted from the original file name that was uploaded.
+ * This should not be considered as a safe value to use for a file name on your servers.
+ */
+ public function getClientOriginalExtension(): string
+ {
+ return pathinfo($this->originalName, \PATHINFO_EXTENSION);
+ }
+
+ /**
+ * Returns the original file full path.
+ *
+ * It is extracted from the request from which the file has been uploaded.
+ * This should not be considered as a safe value to use for a file name/path on your servers.
+ *
+ * If this file was uploaded with the "webkitdirectory" upload directive, this will contain
+ * the path of the file relative to the uploaded root directory. Otherwise this will be identical
+ * to getClientOriginalName().
+ */
+ public function getClientOriginalPath(): string
+ {
+ return $this->originalPath;
+ }
+
+ /**
+ * 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).
+ *
+ * @see getMimeType()
+ */
+ public function getClientMimeType(): string
+ {
+ 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).
+ *
+ * @see guessExtension()
+ * @see getClientMimeType()
+ */
+ public function guessClientExtension(): ?string
+ {
+ 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.
+ */
+ public function getError(): int
+ {
+ return $this->error;
+ }
+
+ /**
+ * Returns whether the file has been uploaded with HTTP and no error occurred.
+ */
+ public function isValid(): bool
+ {
+ $isOk = \UPLOAD_ERR_OK === $this->error;
+
+ return $this->test ? $isOk : $isOk && is_uploaded_file($this->getPathname());
+ }
+
+ /**
+ * Moves the file to a new location.
+ *
+ * @throws FileException if, for any reason, the file could not have been moved
+ */
+ public function move(string $directory, ?string $name = null): File
+ {
+ 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, 0o666 & ~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(): int|float
+ {
+ $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);
+ }
+
+ private static function parseFilesize(string $size): int|float
+ {
+ 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;
+ // no break
+ case 'g': $max *= 1024;
+ // no break
+ case 'm': $max *= 1024;
+ // no break
+ case 'k': $max *= 1024;
+ }
+
+ return $max;
+ }
+
+ /**
+ * Returns an informative upload error message.
+ */
+ public function getErrorMessage(): string
+ {
+ 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/vendor/symfony/http-foundation/FileBag.php b/vendor/symfony/http-foundation/FileBag.php
new file mode 100644
index 0000000..561e7cd
--- /dev/null
+++ b/vendor/symfony/http-foundation/FileBag.php
@@ -0,0 +1,127 @@
+
+ *
+ * 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
+ * @author Bulat Shakirzyanov
+ */
+class FileBag extends ParameterBag
+{
+ private const FILE_KEYS = ['error', 'full_path', 'name', 'size', 'tmp_name', 'type'];
+
+ /**
+ * @param array|UploadedFile[] $parameters An array of HTTP files
+ */
+ public function __construct(array $parameters = [])
+ {
+ $this->replace($parameters);
+ }
+
+ public function replace(array $files = []): void
+ {
+ $this->parameters = [];
+ $this->add($files);
+ }
+
+ public function set(string $key, mixed $value): void
+ {
+ 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));
+ }
+
+ public function add(array $files = []): void
+ {
+ foreach ($files as $key => $file) {
+ $this->set($key, $file);
+ }
+ }
+
+ /**
+ * Converts uploaded files to UploadedFile instances.
+ *
+ * @return UploadedFile[]|UploadedFile|null
+ */
+ protected function convertFileInformation(array|UploadedFile $file): array|UploadedFile|null
+ {
+ if ($file instanceof UploadedFile) {
+ return $file;
+ }
+
+ $file = $this->fixPhpFilesArray($file);
+ $keys = array_keys($file + ['full_path' => null]);
+ sort($keys);
+
+ if (self::FILE_KEYS === $keys) {
+ if (\UPLOAD_ERR_NO_FILE === $file['error']) {
+ $file = null;
+ } else {
+ $file = new UploadedFile($file['tmp_name'], $file['full_path'] ?? $file['name'], $file['type'], $file['error'], false);
+ }
+ } else {
+ $file = array_map(fn ($v) => $v instanceof UploadedFile || \is_array($v) ? $this->convertFileInformation($v) : $v, $file);
+ if (array_is_list($file)) {
+ $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.
+ */
+ protected function fixPhpFilesArray(array $data): array
+ {
+ $keys = array_keys($data + ['full_path' => null]);
+ 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],
+ ] + (isset($data['full_path'][$key]) ? [
+ 'full_path' => $data['full_path'][$key],
+ ] : []));
+ }
+
+ return $files;
+ }
+}
diff --git a/vendor/symfony/http-foundation/HeaderBag.php b/vendor/symfony/http-foundation/HeaderBag.php
new file mode 100644
index 0000000..c2ede56
--- /dev/null
+++ b/vendor/symfony/http-foundation/HeaderBag.php
@@ -0,0 +1,273 @@
+
+ *
+ * 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
+ *
+ * @implements \IteratorAggregate>
+ */
+class HeaderBag implements \IteratorAggregate, \Countable, \Stringable
+{
+ protected const UPPER = '_ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+ protected const LOWER = '-abcdefghijklmnopqrstuvwxyz';
+
+ /**
+ * @var array>
+ */
+ protected array $headers = [];
+ protected array $cacheControl = [];
+
+ public function __construct(array $headers = [])
+ {
+ foreach ($headers as $key => $values) {
+ $this->set($key, $values);
+ }
+ }
+
+ /**
+ * Returns the headers as a string.
+ */
+ public function __toString(): string
+ {
+ 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 ($key is null ? array> : list)
+ */
+ public function all(?string $key = null): array
+ {
+ if (null !== $key) {
+ return $this->headers[strtr($key, self::UPPER, self::LOWER)] ?? [];
+ }
+
+ return $this->headers;
+ }
+
+ /**
+ * Returns the parameter keys.
+ *
+ * @return string[]
+ */
+ public function keys(): array
+ {
+ return array_keys($this->all());
+ }
+
+ /**
+ * Replaces the current HTTP headers by a new set.
+ */
+ public function replace(array $headers = []): void
+ {
+ $this->headers = [];
+ $this->add($headers);
+ }
+
+ /**
+ * Adds new headers the current HTTP headers set.
+ */
+ public function add(array $headers): void
+ {
+ foreach ($headers as $key => $values) {
+ $this->set($key, $values);
+ }
+ }
+
+ /**
+ * Returns the first header by name or the default one.
+ */
+ public function get(string $key, ?string $default = null): ?string
+ {
+ $headers = $this->all($key);
+
+ if (!$headers) {
+ return $default;
+ }
+
+ if (null === $headers[0]) {
+ return null;
+ }
+
+ return $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, string|array|null $values, bool $replace = true): void
+ {
+ $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.
+ */
+ public function has(string $key): bool
+ {
+ return \array_key_exists(strtr($key, self::UPPER, self::LOWER), $this->all());
+ }
+
+ /**
+ * Returns true if the given HTTP header contains the given value.
+ */
+ public function contains(string $key, string $value): bool
+ {
+ return \in_array($value, $this->all($key), true);
+ }
+
+ /**
+ * Removes a header.
+ */
+ public function remove(string $key): void
+ {
+ $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.
+ *
+ * @throws \RuntimeException When the HTTP header is not parseable
+ */
+ public function getDate(string $key, ?\DateTimeInterface $default = null): ?\DateTimeImmutable
+ {
+ if (null === $value = $this->get($key)) {
+ return null !== $default ? \DateTimeImmutable::createFromInterface($default) : null;
+ }
+
+ if (false === $date = \DateTimeImmutable::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.
+ */
+ public function addCacheControlDirective(string $key, bool|string $value = true): void
+ {
+ $this->cacheControl[$key] = $value;
+
+ $this->set('Cache-Control', $this->getCacheControlHeader());
+ }
+
+ /**
+ * Returns true if the Cache-Control directive is defined.
+ */
+ public function hasCacheControlDirective(string $key): bool
+ {
+ return \array_key_exists($key, $this->cacheControl);
+ }
+
+ /**
+ * Returns a Cache-Control directive value by name.
+ */
+ public function getCacheControlDirective(string $key): bool|string|null
+ {
+ return $this->cacheControl[$key] ?? null;
+ }
+
+ /**
+ * Removes a Cache-Control directive.
+ */
+ public function removeCacheControlDirective(string $key): void
+ {
+ unset($this->cacheControl[$key]);
+
+ $this->set('Cache-Control', $this->getCacheControlHeader());
+ }
+
+ /**
+ * Returns an iterator for headers.
+ *
+ * @return \ArrayIterator>
+ */
+ public function getIterator(): \ArrayIterator
+ {
+ return new \ArrayIterator($this->headers);
+ }
+
+ /**
+ * Returns the number of headers.
+ */
+ public function count(): int
+ {
+ return \count($this->headers);
+ }
+
+ protected function getCacheControlHeader(): string
+ {
+ ksort($this->cacheControl);
+
+ return HeaderUtils::toString($this->cacheControl, ',');
+ }
+
+ /**
+ * Parses a Cache-Control HTTP header.
+ */
+ protected function parseCacheControl(string $header): array
+ {
+ $parts = HeaderUtils::split($header, ',=');
+
+ return HeaderUtils::combine($parts);
+ }
+}
diff --git a/vendor/symfony/http-foundation/HeaderUtils.php b/vendor/symfony/http-foundation/HeaderUtils.php
new file mode 100644
index 0000000..37953af
--- /dev/null
+++ b/vendor/symfony/http-foundation/HeaderUtils.php
@@ -0,0 +1,298 @@
+
+ *
+ * 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
+ */
+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', ',;')
+ * # returns [['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
+ {
+ if ('' === $separators) {
+ throw new \InvalidArgumentException('At least one separator must be specified.');
+ }
+
+ $quotedSeparators = preg_quote($separators, '/');
+
+ preg_match_all('
+ /
+ (?!\s)
+ (?:
+ # quoted-string
+ "(?:[^"\\\\]|\\\\.)*(?:"|\\\\|$)
+ |
+ # token
+ [^"'.$quotedSeparators.']+
+ )+
+ (?['.$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 verbatim.
+ */
+ 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], true)) {
+ 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];
+ $separators = substr($separators, 1) ?: '';
+ $i = 0;
+
+ if ('' === $separators && !$first) {
+ $parts = [''];
+
+ foreach ($matches as $match) {
+ if (!$i && isset($match['separator'])) {
+ $i = 1;
+ $parts[1] = '';
+ } else {
+ $parts[$i] .= self::unquote($match[0]);
+ }
+ }
+
+ return $parts;
+ }
+
+ $parts = [];
+ $partMatches = [];
+
+ foreach ($matches as $match) {
+ if (($match['separator'] ?? null) === $separator) {
+ ++$i;
+ } else {
+ $partMatches[$i][] = $match;
+ }
+ }
+
+ foreach ($partMatches as $matches) {
+ if ('' === $separators && '' !== $unquoted = self::unquote($matches[0][0])) {
+ $parts[] = $unquoted;
+ } elseif ($groupedParts = self::groupParts($matches, $separators, false)) {
+ $parts[] = $groupedParts;
+ }
+ }
+
+ return $parts;
+ }
+}
diff --git a/vendor/symfony/http-foundation/InputBag.php b/vendor/symfony/http-foundation/InputBag.php
new file mode 100644
index 0000000..8c153dc
--- /dev/null
+++ b/vendor/symfony/http-foundation/InputBag.php
@@ -0,0 +1,152 @@
+
+ *
+ * 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;
+use Symfony\Component\HttpFoundation\Exception\UnexpectedValueException;
+
+/**
+ * InputBag is a container for user input values such as $_GET, $_POST, $_REQUEST, and $_COOKIE.
+ *
+ * @template TInput of string|int|float|bool|null
+ *
+ * @author Saif Eddin Gmati
+ */
+final class InputBag extends ParameterBag
+{
+ /**
+ * Returns a scalar input value by name.
+ *
+ * @template TDefault of string|int|float|bool|null
+ *
+ * @param TDefault $default The default value if the input key does not exist
+ *
+ * @return TDefault|TInput
+ *
+ * @throws BadRequestException if the input contains a non-scalar value
+ */
+ public function get(string $key, mixed $default = null): string|int|float|bool|null
+ {
+ if (null !== $default && !\is_scalar($default) && !$default instanceof \Stringable) {
+ throw new \InvalidArgumentException(\sprintf('Expected a scalar value as a 2nd argument to "%s()", "%s" given.', __METHOD__, get_debug_type($default)));
+ }
+
+ $value = parent::get($key, $this);
+
+ if (null !== $value && $this !== $value && !\is_scalar($value) && !$value instanceof \Stringable) {
+ throw new BadRequestException(\sprintf('Input value "%s" contains a non-scalar value.', $key));
+ }
+
+ return $this === $value ? $default : $value;
+ }
+
+ /**
+ * Replaces the current input values by a new set.
+ */
+ public function replace(array $inputs = []): void
+ {
+ $this->parameters = [];
+ $this->add($inputs);
+ }
+
+ /**
+ * Adds input values.
+ */
+ public function add(array $inputs = []): void
+ {
+ 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, mixed $value): void
+ {
+ if (null !== $value && !\is_scalar($value) && !\is_array($value) && !$value instanceof \Stringable) {
+ throw new \InvalidArgumentException(\sprintf('Expected a scalar, or an array as a 2nd argument to "%s()", "%s" given.', __METHOD__, get_debug_type($value)));
+ }
+
+ $this->parameters[$key] = $value;
+ }
+
+ /**
+ * Returns the parameter value converted to an enum.
+ *
+ * @template T of \BackedEnum
+ *
+ * @param class-string $class
+ * @param ?T $default
+ *
+ * @return ?T
+ *
+ * @psalm-return ($default is null ? T|null : T)
+ *
+ * @throws BadRequestException if the input cannot be converted to an enum
+ */
+ public function getEnum(string $key, string $class, ?\BackedEnum $default = null): ?\BackedEnum
+ {
+ try {
+ return parent::getEnum($key, $class, $default);
+ } catch (UnexpectedValueException $e) {
+ throw new BadRequestException($e->getMessage(), $e->getCode(), $e);
+ }
+ }
+
+ /**
+ * Returns the parameter value converted to string.
+ *
+ * @throws BadRequestException if the input contains a non-scalar value
+ */
+ public function getString(string $key, string $default = ''): string
+ {
+ // Shortcuts the parent method because the validation on scalar is already done in get().
+ return (string) $this->get($key, $default);
+ }
+
+ /**
+ * @throws BadRequestException if the input value is an array and \FILTER_REQUIRE_ARRAY or \FILTER_FORCE_ARRAY is not set
+ * @throws BadRequestException if the input value is invalid and \FILTER_NULL_ON_FAILURE is not set
+ */
+ public function filter(string $key, mixed $default = null, int $filter = \FILTER_DEFAULT, mixed $options = []): mixed
+ {
+ $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))) {
+ throw new BadRequestException(\sprintf('Input value "%s" contains an array, but "FILTER_REQUIRE_ARRAY" or "FILTER_FORCE_ARRAY" flags were not set.', $key));
+ }
+
+ if ((\FILTER_CALLBACK & $filter) && !(($options['options'] ?? null) instanceof \Closure)) {
+ 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)));
+ }
+
+ $options['flags'] ??= 0;
+ $nullOnFailure = $options['flags'] & \FILTER_NULL_ON_FAILURE;
+ $options['flags'] |= \FILTER_NULL_ON_FAILURE;
+
+ $value = filter_var($value, $filter, $options);
+
+ if (null !== $value || $nullOnFailure) {
+ return $value;
+ }
+
+ throw new BadRequestException(\sprintf('Input value "%s" is invalid and flag "FILTER_NULL_ON_FAILURE" was not set.', $key));
+ }
+}
diff --git a/vendor/symfony/http-foundation/IpUtils.php b/vendor/symfony/http-foundation/IpUtils.php
new file mode 100644
index 0000000..b7a3467
--- /dev/null
+++ b/vendor/symfony/http-foundation/IpUtils.php
@@ -0,0 +1,270 @@
+
+ *
+ * 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
+ */
+class IpUtils
+{
+ public const PRIVATE_SUBNETS = [
+ '127.0.0.0/8', // RFC1700 (Loopback)
+ '10.0.0.0/8', // RFC1918
+ '192.168.0.0/16', // RFC1918
+ '172.16.0.0/12', // RFC1918
+ '169.254.0.0/16', // RFC3927
+ '0.0.0.0/8', // RFC5735
+ '240.0.0.0/4', // RFC1112
+ '::1/128', // Loopback
+ 'fc00::/7', // Unique Local Address
+ 'fe80::/10', // Link Local Address
+ '::ffff:0:0/96', // IPv4 translations
+ '::/128', // Unspecified address
+ ];
+
+ private static array $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)
+ */
+ public static function checkIp(string $requestIp, string|array $ips): bool
+ {
+ 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): bool
+ {
+ $cacheKey = $requestIp.'-'.$ip.'-v4';
+ if (null !== $cacheValue = self::getCacheResult($cacheKey)) {
+ return $cacheValue;
+ }
+
+ if (!filter_var($requestIp, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)) {
+ return self::setCacheResult($cacheKey, false);
+ }
+
+ if (str_contains($ip, '/')) {
+ [$address, $netmask] = explode('/', $ip, 2);
+
+ if ('0' === $netmask) {
+ return self::setCacheResult($cacheKey, false !== filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4));
+ }
+
+ if ($netmask < 0 || $netmask > 32) {
+ return self::setCacheResult($cacheKey, false);
+ }
+ } else {
+ $address = $ip;
+ $netmask = 32;
+ }
+
+ if (false === ip2long($address)) {
+ return self::setCacheResult($cacheKey, false);
+ }
+
+ return self::setCacheResult($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
+ *
+ * @see https://github.com/dsp/v6tools
+ *
+ * @param string $ip IPv6 address or subnet in CIDR notation
+ *
+ * @throws \RuntimeException When IPV6 support is not enabled
+ */
+ public static function checkIp6(string $requestIp, string $ip): bool
+ {
+ $cacheKey = $requestIp.'-'.$ip.'-v6';
+ if (null !== $cacheValue = self::getCacheResult($cacheKey)) {
+ return $cacheValue;
+ }
+
+ 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".');
+ }
+
+ // Check to see if we were given a IP4 $requestIp or $ip by mistake
+ if (!filter_var($requestIp, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) {
+ return self::setCacheResult($cacheKey, false);
+ }
+
+ if (str_contains($ip, '/')) {
+ [$address, $netmask] = explode('/', $ip, 2);
+
+ if (!filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) {
+ return self::setCacheResult($cacheKey, false);
+ }
+
+ if ('0' === $netmask) {
+ return (bool) unpack('n*', @inet_pton($address));
+ }
+
+ if ($netmask < 1 || $netmask > 128) {
+ return self::setCacheResult($cacheKey, false);
+ }
+ } else {
+ if (!filter_var($ip, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) {
+ return self::setCacheResult($cacheKey, false);
+ }
+
+ $address = $ip;
+ $netmask = 128;
+ }
+
+ $bytesAddr = unpack('n*', @inet_pton($address));
+ $bytesTest = unpack('n*', @inet_pton($requestIp));
+
+ if (!$bytesAddr || !$bytesTest) {
+ return self::setCacheResult($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::setCacheResult($cacheKey, false);
+ }
+ }
+
+ return self::setCacheResult($cacheKey, true);
+ }
+
+ /**
+ * Anonymizes an IP/IPv6.
+ *
+ * Removes the last bytes of IPv4 and IPv6 addresses (1 byte for IPv4 and 8 bytes for IPv6 by default).
+ *
+ * @param int<0, 4> $v4Bytes
+ * @param int<0, 16> $v6Bytes
+ */
+ public static function anonymize(string $ip, int $v4Bytes = 1, int $v6Bytes = 8): string
+ {
+ if ($v4Bytes < 0 || $v6Bytes < 0) {
+ throw new \InvalidArgumentException('Cannot anonymize less than 0 bytes.');
+ }
+
+ if ($v4Bytes > 4 || $v6Bytes > 16) {
+ throw new \InvalidArgumentException('Cannot anonymize more than 4 bytes for IPv4 and 16 bytes for IPv6.');
+ }
+
+ /*
+ * If the IP contains a % symbol, then it is a local-link address with scoping according to RFC 4007
+ * In that case, we only care about the part before the % symbol, as the following functions, can only work with
+ * the IP address itself. As the scope can leak information (containing interface name), we do not want to
+ * include it in our anonymized IP data.
+ */
+ if (str_contains($ip, '%')) {
+ $ip = substr($ip, 0, strpos($ip, '%'));
+ }
+
+ $wrappedIPv6 = false;
+ if (str_starts_with($ip, '[') && str_ends_with($ip, ']')) {
+ $wrappedIPv6 = true;
+ $ip = substr($ip, 1, -1);
+ }
+
+ $mappedIpV4MaskGenerator = function (string $mask, int $bytesToAnonymize) {
+ $mask .= str_repeat('ff', 4 - $bytesToAnonymize);
+ $mask .= str_repeat('00', $bytesToAnonymize);
+
+ return '::'.implode(':', str_split($mask, 4));
+ };
+
+ $packedAddress = inet_pton($ip);
+ if (4 === \strlen($packedAddress)) {
+ $mask = rtrim(str_repeat('255.', 4 - $v4Bytes).str_repeat('0.', $v4Bytes), '.');
+ } elseif ($ip === inet_ntop($packedAddress & inet_pton('::ffff:ffff:ffff'))) {
+ $mask = $mappedIpV4MaskGenerator('ffff', $v4Bytes);
+ } elseif ($ip === inet_ntop($packedAddress & inet_pton('::ffff:ffff'))) {
+ $mask = $mappedIpV4MaskGenerator('', $v4Bytes);
+ } else {
+ $mask = str_repeat('ff', 16 - $v6Bytes).str_repeat('00', $v6Bytes);
+ $mask = implode(':', str_split($mask, 4));
+ }
+ $ip = inet_ntop($packedAddress & inet_pton($mask));
+
+ if ($wrappedIPv6) {
+ $ip = '['.$ip.']';
+ }
+
+ return $ip;
+ }
+
+ /**
+ * Checks if an IPv4 or IPv6 address is contained in the list of private IP subnets.
+ */
+ public static function isPrivateIp(string $requestIp): bool
+ {
+ return self::checkIp($requestIp, self::PRIVATE_SUBNETS);
+ }
+
+ private static function getCacheResult(string $cacheKey): ?bool
+ {
+ if (isset(self::$checkedIps[$cacheKey])) {
+ // Move the item last in cache (LRU)
+ $value = self::$checkedIps[$cacheKey];
+ unset(self::$checkedIps[$cacheKey]);
+ self::$checkedIps[$cacheKey] = $value;
+
+ return self::$checkedIps[$cacheKey];
+ }
+
+ return null;
+ }
+
+ private static function setCacheResult(string $cacheKey, bool $result): bool
+ {
+ if (1000 < \count(self::$checkedIps)) {
+ // stop memory leak if there are many keys
+ self::$checkedIps = \array_slice(self::$checkedIps, 500, null, true);
+ }
+
+ return self::$checkedIps[$cacheKey] = $result;
+ }
+}
diff --git a/vendor/symfony/http-foundation/JsonResponse.php b/vendor/symfony/http-foundation/JsonResponse.php
new file mode 100644
index 0000000..187173b
--- /dev/null
+++ b/vendor/symfony/http-foundation/JsonResponse.php
@@ -0,0 +1,187 @@
+
+ *
+ * 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
+ */
+class JsonResponse extends Response
+{
+ protected mixed $data;
+ protected ?string $callback = null;
+
+ // 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 int $encodingOptions = self::DEFAULT_ENCODING_OPTIONS;
+
+ /**
+ * @param bool $json If the data is already a JSON string
+ */
+ public function __construct(mixed $data = null, int $status = 200, array $headers = [], bool $json = false)
+ {
+ parent::__construct('', $status, $headers);
+
+ if ($json && !\is_string($data) && !is_numeric($data) && !$data instanceof \Stringable) {
+ 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)));
+ }
+
+ $data ??= new \ArrayObject();
+
+ $json ? $this->setJson($data) : $this->setData($data);
+ }
+
+ /**
+ * 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 (200 "OK" by default)
+ * @param array $headers An array of response headers
+ */
+ public static function fromJsonString(string $data, int $status = 200, array $headers = []): static
+ {
+ 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): static
+ {
+ 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
+ $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): static
+ {
+ $this->data = $json;
+
+ return $this->update();
+ }
+
+ /**
+ * Sets the data to be sent as JSON.
+ *
+ * @return $this
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function setData(mixed $data = []): static
+ {
+ try {
+ $data = json_encode($data, $this->encodingOptions);
+ } catch (\Exception $e) {
+ if ('Exception' === $e::class && str_starts_with($e->getMessage(), 'Failed calling ')) {
+ throw $e->getPrevious() ?: $e;
+ }
+ throw $e;
+ }
+
+ if (\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.
+ */
+ public function getEncodingOptions(): int
+ {
+ return $this->encodingOptions;
+ }
+
+ /**
+ * Sets options used while encoding data to JSON.
+ *
+ * @return $this
+ */
+ public function setEncodingOptions(int $encodingOptions): static
+ {
+ $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(): static
+ {
+ 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/vendor/symfony/http-foundation/LICENSE b/vendor/symfony/http-foundation/LICENSE
new file mode 100644
index 0000000..0138f8f
--- /dev/null
+++ b/vendor/symfony/http-foundation/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2004-present 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/vendor/symfony/http-foundation/ParameterBag.php b/vendor/symfony/http-foundation/ParameterBag.php
new file mode 100644
index 0000000..5e36cf2
--- /dev/null
+++ b/vendor/symfony/http-foundation/ParameterBag.php
@@ -0,0 +1,271 @@
+
+ *
+ * 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;
+use Symfony\Component\HttpFoundation\Exception\UnexpectedValueException;
+
+/**
+ * ParameterBag is a container for key/value pairs.
+ *
+ * @author Fabien Potencier
+ *
+ * @implements \IteratorAggregate
+ */
+class ParameterBag implements \IteratorAggregate, \Countable
+{
+ /**
+ * @param array $parameters
+ */
+ public function __construct(
+ protected array $parameters = [],
+ ) {
+ }
+
+ /**
+ * Returns the parameters.
+ *
+ * @template TKey of string|null
+ *
+ * @param TKey $key The name of the parameter to return or null to get them all
+ *
+ * @return (TKey is null ? array : array)
+ *
+ * @throws BadRequestException if the value is not an array
+ */
+ public function all(?string $key = null): array
+ {
+ 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 list
+ */
+ public function keys(): array
+ {
+ return array_keys($this->parameters);
+ }
+
+ /**
+ * Replaces the current parameters by a new set.
+ *
+ * @param array $parameters
+ */
+ public function replace(array $parameters = []): void
+ {
+ $this->parameters = $parameters;
+ }
+
+ /**
+ * Adds parameters.
+ *
+ * @param array $parameters
+ */
+ public function add(array $parameters = []): void
+ {
+ $this->parameters = array_replace($this->parameters, $parameters);
+ }
+
+ public function get(string $key, mixed $default = null): mixed
+ {
+ return \array_key_exists($key, $this->parameters) ? $this->parameters[$key] : $default;
+ }
+
+ public function set(string $key, mixed $value): void
+ {
+ $this->parameters[$key] = $value;
+ }
+
+ /**
+ * Returns true if the parameter is defined.
+ */
+ public function has(string $key): bool
+ {
+ return \array_key_exists($key, $this->parameters);
+ }
+
+ /**
+ * Removes a parameter.
+ */
+ public function remove(string $key): void
+ {
+ unset($this->parameters[$key]);
+ }
+
+ /**
+ * Returns the alphabetic characters of the parameter value.
+ *
+ * @throws UnexpectedValueException if the value cannot be converted to string
+ */
+ public function getAlpha(string $key, string $default = ''): string
+ {
+ return preg_replace('/[^[:alpha:]]/', '', $this->getString($key, $default));
+ }
+
+ /**
+ * Returns the alphabetic characters and digits of the parameter value.
+ *
+ * @throws UnexpectedValueException if the value cannot be converted to string
+ */
+ public function getAlnum(string $key, string $default = ''): string
+ {
+ return preg_replace('/[^[:alnum:]]/', '', $this->getString($key, $default));
+ }
+
+ /**
+ * Returns the digits of the parameter value.
+ *
+ * @throws UnexpectedValueException if the value cannot be converted to string
+ */
+ public function getDigits(string $key, string $default = ''): string
+ {
+ return preg_replace('/[^[:digit:]]/', '', $this->getString($key, $default));
+ }
+
+ /**
+ * Returns the parameter as string.
+ *
+ * @throws UnexpectedValueException if the value cannot be converted to string
+ */
+ public function getString(string $key, string $default = ''): string
+ {
+ $value = $this->get($key, $default);
+ if (!\is_scalar($value) && !$value instanceof \Stringable) {
+ throw new UnexpectedValueException(\sprintf('Parameter value "%s" cannot be converted to "string".', $key));
+ }
+
+ return (string) $value;
+ }
+
+ /**
+ * Returns the parameter value converted to integer.
+ *
+ * @throws UnexpectedValueException if the value cannot be converted to integer
+ */
+ public function getInt(string $key, int $default = 0): int
+ {
+ return $this->filter($key, $default, \FILTER_VALIDATE_INT, ['flags' => \FILTER_REQUIRE_SCALAR]);
+ }
+
+ /**
+ * Returns the parameter value converted to boolean.
+ *
+ * @throws UnexpectedValueException if the value cannot be converted to a boolean
+ */
+ public function getBoolean(string $key, bool $default = false): bool
+ {
+ return $this->filter($key, $default, \FILTER_VALIDATE_BOOL, ['flags' => \FILTER_REQUIRE_SCALAR]);
+ }
+
+ /**
+ * Returns the parameter value converted to an enum.
+ *
+ * @template T of \BackedEnum
+ *
+ * @param class-string $class
+ * @param ?T $default
+ *
+ * @return ?T
+ *
+ * @psalm-return ($default is null ? T|null : T)
+ *
+ * @throws UnexpectedValueException if the parameter value cannot be converted to an enum
+ */
+ public function getEnum(string $key, string $class, ?\BackedEnum $default = null): ?\BackedEnum
+ {
+ $value = $this->get($key);
+
+ if (null === $value) {
+ return $default;
+ }
+
+ try {
+ return $class::from($value);
+ } catch (\ValueError|\TypeError $e) {
+ throw new UnexpectedValueException(\sprintf('Parameter "%s" cannot be converted to enum: ', $key).$e->getMessage().'.', $e->getCode(), $e);
+ }
+ }
+
+ /**
+ * Filter key.
+ *
+ * @param int $filter FILTER_* constant
+ * @param int|array{flags?: int, options?: array} $options Flags from FILTER_* constants
+ *
+ * @see https://php.net/filter-var
+ *
+ * @throws UnexpectedValueException if the parameter value is a non-stringable object
+ * @throws UnexpectedValueException if the parameter value is invalid and \FILTER_NULL_ON_FAILURE is not set
+ */
+ public function filter(string $key, mixed $default = null, int $filter = \FILTER_DEFAULT, mixed $options = []): mixed
+ {
+ $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 (\is_object($value) && !$value instanceof \Stringable) {
+ throw new UnexpectedValueException(\sprintf('Parameter value "%s" cannot be filtered.', $key));
+ }
+
+ if ((\FILTER_CALLBACK & $filter) && !(($options['options'] ?? null) instanceof \Closure)) {
+ 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)));
+ }
+
+ $options['flags'] ??= 0;
+ $nullOnFailure = $options['flags'] & \FILTER_NULL_ON_FAILURE;
+ $options['flags'] |= \FILTER_NULL_ON_FAILURE;
+
+ $value = filter_var($value, $filter, $options);
+
+ if (null !== $value || $nullOnFailure) {
+ return $value;
+ }
+
+ throw new \UnexpectedValueException(\sprintf('Parameter value "%s" is invalid and flag "FILTER_NULL_ON_FAILURE" was not set.', $key));
+ }
+
+ /**
+ * Returns an iterator for parameters.
+ *
+ * @return \ArrayIterator
+ */
+ public function getIterator(): \ArrayIterator
+ {
+ return new \ArrayIterator($this->parameters);
+ }
+
+ /**
+ * Returns the number of parameters.
+ */
+ public function count(): int
+ {
+ return \count($this->parameters);
+ }
+}
diff --git a/vendor/symfony/http-foundation/README.md b/vendor/symfony/http-foundation/README.md
new file mode 100644
index 0000000..5cf9007
--- /dev/null
+++ b/vendor/symfony/http-foundation/README.md
@@ -0,0 +1,14 @@
+HttpFoundation Component
+========================
+
+The HttpFoundation component defines an object-oriented layer for the HTTP
+specification.
+
+Resources
+---------
+
+ * [Documentation](https://symfony.com/doc/current/components/http_foundation.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)
diff --git a/vendor/symfony/http-foundation/RateLimiter/AbstractRequestRateLimiter.php b/vendor/symfony/http-foundation/RateLimiter/AbstractRequestRateLimiter.php
new file mode 100644
index 0000000..550090f
--- /dev/null
+++ b/vendor/symfony/http-foundation/RateLimiter/AbstractRequestRateLimiter.php
@@ -0,0 +1,81 @@
+
+ *
+ * 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 PeekableRequestRateLimiterInterface that
+ * fits most use-cases.
+ *
+ * @author Wouter de Jong
+ */
+abstract class AbstractRequestRateLimiter implements PeekableRequestRateLimiterInterface
+{
+ public function consume(Request $request): RateLimit
+ {
+ return $this->doConsume($request, 1);
+ }
+
+ public function peek(Request $request): RateLimit
+ {
+ return $this->doConsume($request, 0);
+ }
+
+ private function doConsume(Request $request, int $tokens): RateLimit
+ {
+ $limiters = $this->getLimiters($request);
+ if (0 === \count($limiters)) {
+ $limiters = [new NoLimiter()];
+ }
+
+ $minimalRateLimit = null;
+ foreach ($limiters as $limiter) {
+ $rateLimit = $limiter->consume($tokens);
+
+ $minimalRateLimit = $minimalRateLimit ? self::getMinimalRateLimit($minimalRateLimit, $rateLimit) : $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;
+
+ private static function getMinimalRateLimit(RateLimit $first, RateLimit $second): RateLimit
+ {
+ if ($first->isAccepted() !== $second->isAccepted()) {
+ return $first->isAccepted() ? $second : $first;
+ }
+
+ $firstRemainingTokens = $first->getRemainingTokens();
+ $secondRemainingTokens = $second->getRemainingTokens();
+
+ if ($firstRemainingTokens === $secondRemainingTokens) {
+ return $first->getRetryAfter() < $second->getRetryAfter() ? $second : $first;
+ }
+
+ return $firstRemainingTokens > $secondRemainingTokens ? $second : $first;
+ }
+}
diff --git a/vendor/symfony/http-foundation/RateLimiter/PeekableRequestRateLimiterInterface.php b/vendor/symfony/http-foundation/RateLimiter/PeekableRequestRateLimiterInterface.php
new file mode 100644
index 0000000..63471af
--- /dev/null
+++ b/vendor/symfony/http-foundation/RateLimiter/PeekableRequestRateLimiterInterface.php
@@ -0,0 +1,35 @@
+
+ *
+ * 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 request limiter which allows peeking ahead.
+ *
+ * This is valuable to reduce the cache backend load in scenarios
+ * like a login when we only want to consume a token on login failure,
+ * and where the majority of requests will be successful and thus not
+ * need to consume a token.
+ *
+ * This way we can peek ahead before allowing the request through, and
+ * only consume if the request failed (1 backend op). This is compared
+ * to always consuming and then resetting the limit if the request
+ * is successful (2 backend ops).
+ *
+ * @author Jordi Boggiano
+ */
+interface PeekableRequestRateLimiterInterface extends RequestRateLimiterInterface
+{
+ public function peek(Request $request): RateLimit;
+}
diff --git a/vendor/symfony/http-foundation/RateLimiter/RequestRateLimiterInterface.php b/vendor/symfony/http-foundation/RateLimiter/RequestRateLimiterInterface.php
new file mode 100644
index 0000000..4c87a40
--- /dev/null
+++ b/vendor/symfony/http-foundation/RateLimiter/RequestRateLimiterInterface.php
@@ -0,0 +1,30 @@
+
+ *
+ * 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
+ */
+interface RequestRateLimiterInterface
+{
+ public function consume(Request $request): RateLimit;
+
+ public function reset(Request $request): void;
+}
diff --git a/vendor/symfony/http-foundation/RedirectResponse.php b/vendor/symfony/http-foundation/RedirectResponse.php
new file mode 100644
index 0000000..b1b1cf3
--- /dev/null
+++ b/vendor/symfony/http-foundation/RedirectResponse.php
@@ -0,0 +1,92 @@
+
+ *
+ * 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
+ */
+class RedirectResponse extends Response
+{
+ protected string $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 HTTP status code (302 "Found" 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');
+ }
+ }
+
+ /**
+ * Returns the target URL.
+ */
+ public function getTargetUrl(): string
+ {
+ return $this->targetUrl;
+ }
+
+ /**
+ * Sets the redirect target of this response.
+ *
+ * @return $this
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function setTargetUrl(string $url): static
+ {
+ if ('' === $url) {
+ throw new \InvalidArgumentException('Cannot redirect to an empty URL.');
+ }
+
+ $this->targetUrl = $url;
+
+ $this->setContent(
+ \sprintf('
+
+
+
+
+
+ Redirecting to %1$s
+
+
+ Redirecting to %1$s.
+
+', htmlspecialchars($url, \ENT_QUOTES, 'UTF-8')));
+
+ $this->headers->set('Location', $url);
+ $this->headers->set('Content-Type', 'text/html; charset=utf-8');
+
+ return $this;
+ }
+}
diff --git a/vendor/symfony/http-foundation/Request.php b/vendor/symfony/http-foundation/Request.php
new file mode 100644
index 0000000..9d74e2f
--- /dev/null
+++ b/vendor/symfony/http-foundation/Request.php
@@ -0,0 +1,2214 @@
+
+ *
+ * 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;
+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
+ */
+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;
+
+ 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';
+ public const METHOD_QUERY = 'QUERY';
+
+ /**
+ * @var string[]
+ */
+ protected static array $trustedProxies = [];
+
+ /**
+ * @var string[]
+ */
+ protected static array $trustedHostPatterns = [];
+
+ /**
+ * @var string[]
+ */
+ protected static array $trustedHosts = [];
+
+ protected static bool $httpMethodParameterOverride = false;
+
+ /**
+ * The HTTP methods that can be overridden.
+ *
+ * @var uppercase-string[]|null
+ */
+ protected static ?array $allowedHttpMethodOverride = null;
+
+ /**
+ * Custom parameters.
+ */
+ public ParameterBag $attributes;
+
+ /**
+ * Request body parameters ($_POST).
+ *
+ * @see getPayload() for portability between content types
+ */
+ public InputBag $request;
+
+ /**
+ * Query string parameters ($_GET).
+ *
+ * @var InputBag
+ */
+ public InputBag $query;
+
+ /**
+ * Server and execution environment parameters ($_SERVER).
+ */
+ public ServerBag $server;
+
+ /**
+ * Uploaded files ($_FILES).
+ */
+ public FileBag $files;
+
+ /**
+ * Cookies ($_COOKIE).
+ *
+ * @var InputBag
+ */
+ public InputBag $cookies;
+
+ /**
+ * Headers (taken from the $_SERVER).
+ */
+ public HeaderBag $headers;
+
+ /**
+ * @var string|resource|false|null
+ */
+ protected $content;
+
+ /**
+ * @var string[]|null
+ */
+ protected ?array $languages = null;
+
+ /**
+ * @var string[]|null
+ */
+ protected ?array $charsets = null;
+
+ /**
+ * @var string[]|null
+ */
+ protected ?array $encodings = null;
+
+ /**
+ * @var string[]|null
+ */
+ protected ?array $acceptableContentTypes = null;
+
+ protected ?string $pathInfo = null;
+ protected ?string $requestUri = null;
+ protected ?string $baseUrl = null;
+ protected ?string $basePath = null;
+ protected ?string $method = null;
+ protected ?string $format = null;
+ protected SessionInterface|\Closure|null $session = null;
+ protected ?string $locale = null;
+ protected string $defaultLocale = 'en';
+
+ /**
+ * @var array|null
+ */
+ protected static ?array $formats = null;
+
+ protected static ?\Closure $requestFactory = null;
+
+ private ?string $preferredFormat = null;
+
+ private bool $isHostValid = true;
+ private bool $isForwardedValid = true;
+ private bool $isSafeContentPreferred;
+
+ private array $trustedValuesCache = [];
+
+ private static int $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',
+ ];
+
+ /**
+ * This mapping is used when no exact MIME match is found in $formats.
+ *
+ * It enables mappings like application/soap+xml -> xml.
+ *
+ * @see https://datatracker.ietf.org/doc/html/rfc6839
+ * @see https://datatracker.ietf.org/doc/html/rfc7303
+ * @see https://www.iana.org/assignments/media-types/media-types.xhtml
+ */
+ private const STRUCTURED_SUFFIX_FORMATS = [
+ 'json' => 'json',
+ 'xml' => 'xml',
+ 'xhtml' => 'html',
+ 'cbor' => 'cbor',
+ 'zip' => 'zip',
+ 'ber' => 'asn1',
+ 'der' => 'asn1',
+ 'tlv' => 'tlv',
+ 'wbxml' => 'xml',
+ 'yaml' => 'yaml',
+ ];
+
+ private bool $isIisRewrite = false;
+
+ /**
+ * @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): void
+ {
+ $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.
+ */
+ public static function createFromGlobals(): static
+ {
+ if (!\in_array($_SERVER['REQUEST_METHOD'] ?? null, ['PUT', 'DELETE', 'PATCH', 'QUERY'], true)) {
+ return self::createRequestFromFactory($_GET, $_POST, [], $_COOKIE, $_FILES, $_SERVER);
+ }
+
+ try {
+ [$post, $files] = request_parse_body();
+ } catch (\RequestParseBodyException) {
+ $post = $_POST;
+ $files = $_FILES;
+ }
+
+ return self::createRequestFromFactory($_GET, $post, [], $_COOKIE, $files, $_SERVER);
+ }
+
+ /**
+ * 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
+ *
+ * @throws BadRequestException When the URI is invalid
+ */
+ public static function create(string $uri, string $method = 'GET', array $parameters = [], array $cookies = [], array $files = [], array $server = [], $content = null): static
+ {
+ $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);
+
+ if (($i = strcspn($uri, ':/?#')) && ':' === ($uri[$i] ?? null) && (strspn($uri, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-.') !== $i || strcspn($uri, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'))) {
+ throw new BadRequestException('Invalid URI: Scheme is malformed.');
+ }
+ if (false === $components = parse_url(\strlen($uri) !== strcspn($uri, '?#') ? $uri : $uri.'#')) {
+ throw new BadRequestException('Invalid URI.');
+ }
+
+ $part = ($components['user'] ?? '').':'.($components['pass'] ?? '');
+
+ if (':' !== $part && \strlen($part) !== strcspn($part, '[]')) {
+ throw new BadRequestException('Invalid URI: Userinfo is malformed.');
+ }
+ if (($part = $components['host'] ?? '') && !self::isHostValid($part)) {
+ throw new BadRequestException('Invalid URI: Host is malformed.');
+ }
+ if (false !== ($i = strpos($uri, '\\')) && $i < strcspn($uri, '?#')) {
+ throw new BadRequestException('Invalid URI: A URI cannot contain a backslash.');
+ }
+ if (\strlen($uri) !== strcspn($uri, "\r\n\t")) {
+ throw new BadRequestException('Invalid URI: A URI cannot contain CR/LF/TAB characters.');
+ }
+ if ('' !== $uri && (\ord($uri[0]) <= 32 || \ord($uri[-1]) <= 32)) {
+ throw new BadRequestException('Invalid URI: A URI must not start nor end with ASCII control characters or spaces.');
+ }
+
+ 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':
+ case 'QUERY':
+ 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): void
+ {
+ self::$requestFactory = null === $callable ? null : $callable(...);
+ }
+
+ /**
+ * Clones a request and overrides some of its parameters.
+ *
+ * @param array|null $query The GET parameters
+ * @param array|null $request The POST parameters
+ * @param array|null $attributes The request attributes (parameters parsed from the PATH_INFO, ...)
+ * @param array|null $cookies The COOKIE parameters
+ * @param array|null $files The FILES parameters
+ * @param array|null $server The SERVER parameters
+ */
+ public function duplicate(?array $query = null, ?array $request = null, ?array $attributes = null, ?array $cookies = null, ?array $files = null, ?array $server = null): static
+ {
+ $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->attributes->has('_format') && $this->attributes->has('_format')) {
+ $dup->attributes->set('_format', $this->attributes->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;
+ }
+
+ public function __toString(): string
+ {
+ $content = $this->getContent();
+
+ $cookieHeader = '';
+ $cookies = [];
+
+ foreach ($this->cookies as $k => $v) {
+ $cookies[] = \is_array($v) ? http_build_query([$k => $v], '', '; ', \PHP_QUERY_RFC3986) : "$k=$v";
+ }
+
+ if ($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(): void
+ {
+ $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'] and 'PRIVATE_SUBNETS' by IpUtils::PRIVATE_SUBNETS
+ * @param int-mask-of $trustedHeaderSet A bit field to set which headers to trust from your proxies
+ */
+ public static function setTrustedProxies(array $proxies, int $trustedHeaderSet): void
+ {
+ if (false !== $i = array_search('REMOTE_ADDR', $proxies, true)) {
+ if (isset($_SERVER['REMOTE_ADDR'])) {
+ $proxies[$i] = $_SERVER['REMOTE_ADDR'];
+ } else {
+ unset($proxies[$i]);
+ $proxies = array_values($proxies);
+ }
+ }
+
+ if (false !== ($i = array_search('PRIVATE_SUBNETS', $proxies, true)) || false !== ($i = array_search('private_ranges', $proxies, true))) {
+ unset($proxies[$i]);
+ $proxies = array_merge($proxies, IpUtils::PRIVATE_SUBNETS);
+ }
+
+ self::$trustedProxies = $proxies;
+ self::$trustedHeaderSet = $trustedHeaderSet;
+ }
+
+ /**
+ * Gets the list of trusted proxies.
+ *
+ * @return string[]
+ */
+ public static function getTrustedProxies(): array
+ {
+ 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(): int
+ {
+ 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): void
+ {
+ self::$trustedHostPatterns = array_map(fn ($hostPattern) => \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 string[]
+ */
+ public static function getTrustedHosts(): array
+ {
+ 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.
+ */
+ public static function normalizeQueryString(?string $qs): string
+ {
+ 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(): void
+ {
+ self::$httpMethodParameterOverride = true;
+ }
+
+ /**
+ * Checks whether support for the _method request parameter is enabled.
+ */
+ public static function getHttpMethodParameterOverride(): bool
+ {
+ return self::$httpMethodParameterOverride;
+ }
+
+ /**
+ * Sets the list of HTTP methods that can be overridden.
+ *
+ * Set to null to allow all methods to be overridden (default). Set to an
+ * empty array to disallow overrides entirely. Otherwise, provide the list
+ * of uppercased method names that are allowed.
+ *
+ * @param uppercase-string[]|null $methods
+ */
+ public static function setAllowedHttpMethodOverride(?array $methods): void
+ {
+ if (array_intersect($methods ?? [], ['GET', 'HEAD', 'CONNECT', 'TRACE'])) {
+ throw new \InvalidArgumentException('The HTTP methods "GET", "HEAD", "CONNECT", and "TRACE" cannot be overridden.');
+ }
+
+ self::$allowedHttpMethodOverride = $methods;
+ }
+
+ /**
+ * Gets the list of HTTP methods that can be overridden.
+ *
+ * @return uppercase-string[]|null
+ */
+ public static function getAllowedHttpMethodOverride(): ?array
+ {
+ return self::$allowedHttpMethodOverride;
+ }
+
+ /**
+ * Gets the Session.
+ *
+ * @throws SessionNotFoundException When session is not set properly
+ */
+ public function getSession(): SessionInterface
+ {
+ $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.
+ */
+ public function hasPreviousSession(): bool
+ {
+ // 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`
+ */
+ public function hasSession(bool $skipIfUninitialized = false): bool
+ {
+ return null !== $this->session && (!$skipIfUninitialized || $this->session instanceof SessionInterface);
+ }
+
+ public function setSession(SessionInterface $session): void
+ {
+ $this->session = $session;
+ }
+
+ /**
+ * @internal
+ *
+ * @param callable(): SessionInterface $factory
+ */
+ public function setSessionFactory(callable $factory): void
+ {
+ $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.
+ *
+ * @see getClientIp()
+ */
+ public function getClientIps(): array
+ {
+ $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.
+ *
+ * @see getClientIps()
+ * @see https://wikipedia.org/wiki/X-Forwarded-For
+ */
+ public function getClientIp(): ?string
+ {
+ return $this->getClientIps()[0];
+ }
+
+ /**
+ * Returns current script name.
+ */
+ public function getScriptName(): string
+ {
+ 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 '/'
+ * * 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(): string
+ {
+ return $this->pathInfo ??= $this->preparePathInfo();
+ }
+
+ /**
+ * 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(): string
+ {
+ return $this->basePath ??= $this->prepareBasePath();
+ }
+
+ /**
+ * 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(): string
+ {
+ $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
+ {
+ return $this->baseUrl ??= $this->prepareBaseUrl();
+ }
+
+ /**
+ * Gets the request's scheme.
+ */
+ public function getScheme(): string
+ {
+ 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(): int|string|null
+ {
+ 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.
+ */
+ public function getUser(): ?string
+ {
+ return $this->headers->get('PHP_AUTH_USER');
+ }
+
+ /**
+ * Returns the password.
+ */
+ public function getPassword(): ?string
+ {
+ 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(): ?string
+ {
+ $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.
+ */
+ public function getHttpHost(): string
+ {
+ $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(): string
+ {
+ return $this->requestUri ??= $this->prepareRequestUri();
+ }
+
+ /**
+ * 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.
+ */
+ public function getSchemeAndHttpHost(): string
+ {
+ return $this->getScheme().'://'.$this->getHttpHost();
+ }
+
+ /**
+ * Generates a normalized URI (URL) for the Request.
+ *
+ * @see getQueryString()
+ */
+ public function getUri(): string
+ {
+ 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
+ */
+ public function getUriForPath(string $path): string
+ {
+ 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"
+ */
+ public function getRelativeUriForPath(string $path): string
+ {
+ // 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.
+ */
+ public function getQueryString(): ?string
+ {
+ $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".
+ */
+ public function isSecure(): bool
+ {
+ 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 $https && (!\is_string($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.
+ *
+ * @throws SuspiciousOperationException when the host name is invalid or not trusted
+ */
+ public function getHost(): string
+ {
+ if ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_X_FORWARDED_HOST)) {
+ $host = $host[0];
+ } else {
+ $host = $this->headers->get('HOST') ?: $this->server->get('SERVER_NAME') ?: $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)));
+
+ // the host can come from the user (HTTP_HOST and depending on the configuration, SERVER_NAME too can come from the user)
+ if ($host && !self::isHostValid($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, true)) {
+ 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): void
+ {
+ $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.
+ *
+ * @see getRealMethod()
+ */
+ public function getMethod(): string
+ {
+ if (null !== $this->method) {
+ return $this->method;
+ }
+
+ $this->method = strtoupper($this->server->get('REQUEST_METHOD', 'GET'));
+
+ if ('POST' !== $this->method || !(self::$allowedHttpMethodOverride ?? true)) {
+ 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', 'CONNECT', 'TRACE'], true)) {
+ return $this->method;
+ }
+
+ if (self::$allowedHttpMethodOverride && !\in_array($method, self::$allowedHttpMethodOverride, true)) {
+ return $this->method;
+ }
+
+ if (\strlen($method) !== strspn($method, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')) {
+ throw new SuspiciousOperationException('Invalid HTTP method override.');
+ }
+
+ return $this->method = $method;
+ }
+
+ /**
+ * Gets the "real" request method.
+ *
+ * @see getMethod()
+ */
+ public function getRealMethod(): string
+ {
+ return strtoupper($this->server->get('REQUEST_METHOD', 'GET'));
+ }
+
+ /**
+ * Gets the mime type associated with the format.
+ */
+ public function getMimeType(string $format): ?string
+ {
+ 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 string[]
+ */
+ public static function getMimeTypes(string $format): array
+ {
+ if (null === static::$formats) {
+ static::initializeFormats();
+ }
+
+ return static::$formats[$format] ?? [];
+ }
+
+ /**
+ * Gets the format associated with the mime type.
+ *
+ * Resolution order:
+ * 1) Exact match on the full MIME type (e.g. "application/json").
+ * 2) Match on the canonical MIME type (i.e. before the first ";" parameter).
+ * 3) If the type is "application/*+suffix", use the structured syntax suffix
+ * mapping (e.g. "application/foo+json" → "json"), when available.
+ * 4) If $subtypeFallback is true and no match was found:
+ * - return the MIME subtype (without "x-" prefix), provided it does not
+ * contain a "+" (e.g. "application/x-yaml" → "yaml", "text/csv" → "csv").
+ *
+ * @param string|null $mimeType The mime type to check
+ * @param bool $subtypeFallback Whether to fall back to the subtype if no exact match is found
+ */
+ public function getFormat(?string $mimeType, bool $subtypeFallback = false): ?string
+ {
+ $canonicalMimeType = null;
+ if ($mimeType && false !== $pos = strpos($mimeType, ';')) {
+ $canonicalMimeType = trim(substr($mimeType, 0, $pos));
+ }
+
+ if (null === static::$formats) {
+ static::initializeFormats();
+ }
+
+ $exactFormat = null;
+ $canonicalFormat = null;
+
+ foreach (static::$formats as $format => $mimeTypes) {
+ if (\in_array($mimeType, $mimeTypes, true)) {
+ $exactFormat = $format;
+ }
+ if (null !== $canonicalMimeType && \in_array($canonicalMimeType, $mimeTypes, true)) {
+ $canonicalFormat = $format;
+ }
+ }
+
+ if ($format = $exactFormat ?? $canonicalFormat) {
+ return $format;
+ }
+
+ if (!$canonicalMimeType ??= $mimeType) {
+ return null;
+ }
+
+ if (str_starts_with($canonicalMimeType, 'application/') && str_contains($canonicalMimeType, '+')) {
+ $suffix = substr(strrchr($canonicalMimeType, '+'), 1);
+ if (isset(self::STRUCTURED_SUFFIX_FORMATS[$suffix])) {
+ return self::STRUCTURED_SUFFIX_FORMATS[$suffix];
+ }
+ }
+
+ if ($subtypeFallback && str_contains($canonicalMimeType, '/')) {
+ [, $subtype] = explode('/', $canonicalMimeType, 2);
+ if (str_starts_with($subtype, 'x-')) {
+ $subtype = substr($subtype, 2);
+ }
+ if (!str_contains($subtype, '+')) {
+ return $subtype;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Associates a format with mime types.
+ *
+ * @param string|string[] $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, string|array $mimeTypes): void
+ {
+ if (null === static::$formats) {
+ static::initializeFormats();
+ }
+
+ static::$formats[$format] = (array) $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
+ */
+ public function getRequestFormat(?string $default = 'html'): ?string
+ {
+ $this->format ??= $this->attributes->get('_format');
+
+ return $this->format ?? $default;
+ }
+
+ /**
+ * Sets the request format.
+ */
+ public function setRequestFormat(?string $format): void
+ {
+ $this->format = $format;
+ }
+
+ /**
+ * Gets the usual name of the format associated with the request's media type (provided in the Content-Type header).
+ *
+ * @see Request::$formats
+ */
+ public function getContentTypeFormat(): ?string
+ {
+ return $this->getFormat($this->headers->get('CONTENT_TYPE', ''));
+ }
+
+ /**
+ * Sets the default locale.
+ */
+ public function setDefaultLocale(string $locale): void
+ {
+ $this->defaultLocale = $locale;
+
+ if (null === $this->locale) {
+ $this->setPhpDefaultLocale($locale);
+ }
+ }
+
+ /**
+ * Get the default locale.
+ */
+ public function getDefaultLocale(): string
+ {
+ return $this->defaultLocale;
+ }
+
+ /**
+ * Sets the locale.
+ */
+ public function setLocale(string $locale): void
+ {
+ $this->setPhpDefaultLocale($this->locale = $locale);
+ }
+
+ /**
+ * Get the locale.
+ */
+ public function getLocale(): string
+ {
+ return $this->locale ?? $this->defaultLocale;
+ }
+
+ /**
+ * Checks if the request method is of specified type.
+ *
+ * @param string $method Uppercase request method (GET, POST etc)
+ */
+ public function isMethod(string $method): bool
+ {
+ return $this->getMethod() === strtoupper($method);
+ }
+
+ /**
+ * Checks whether or not the method is safe.
+ *
+ * @see https://tools.ietf.org/html/rfc7231#section-4.2.1
+ */
+ public function isMethodSafe(): bool
+ {
+ return \in_array($this->getMethod(), ['GET', 'HEAD', 'OPTIONS', 'TRACE', 'QUERY'], true);
+ }
+
+ /**
+ * Checks whether or not the method is idempotent.
+ */
+ public function isMethodIdempotent(): bool
+ {
+ return \in_array($this->getMethod(), ['HEAD', 'GET', 'PUT', 'DELETE', 'TRACE', 'OPTIONS', 'PURGE', 'QUERY'], true);
+ }
+
+ /**
+ * Checks whether the method is cacheable or not.
+ *
+ * @see https://tools.ietf.org/html/rfc7231#section-4.2.3
+ */
+ public function isMethodCacheable(): bool
+ {
+ return \in_array($this->getMethod(), ['GET', 'HEAD', 'QUERY'], true);
+ }
+
+ /**
+ * 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).
+ */
+ public function getProtocolVersion(): ?string
+ {
+ if ($this->isFromTrustedProxy()) {
+ preg_match('~^(HTTP/)?([1-9]\.[0-9])\b~', $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
+ *
+ * @psalm-return ($asResource is true ? resource : string)
+ */
+ public function getContent(bool $asResource = false)
+ {
+ if ($asResource) {
+ if (\is_resource($this->content)) {
+ 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 (\is_resource($this->content)) {
+ 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 decoded form or json request body.
+ *
+ * @throws JsonException When the body cannot be decoded to an array
+ */
+ public function getPayload(): InputBag
+ {
+ if ($this->request->count()) {
+ return clone $this->request;
+ }
+
+ if ('' === $content = $this->getContent()) {
+ return new InputBag([]);
+ }
+
+ try {
+ $content = json_decode($content, true, 512, \JSON_BIGINT_AS_STRING | \JSON_THROW_ON_ERROR);
+ } catch (\JsonException $e) {
+ throw new JsonException('Could not decode request body.', $e->getCode(), $e);
+ }
+
+ if (!\is_array($content)) {
+ throw new JsonException(\sprintf('JSON content was expected to decode to an array, "%s" returned.', get_debug_type($content)));
+ }
+
+ return new InputBag($content);
+ }
+
+ /**
+ * Gets the request body decoded as array, typically from a JSON payload.
+ *
+ * @see getPayload() for portability between content types
+ *
+ * @throws JsonException When the body cannot be decoded to an array
+ */
+ public function toArray(): array
+ {
+ if ('' === $content = $this->getContent()) {
+ throw new JsonException('Request body is empty.');
+ }
+
+ try {
+ $content = json_decode($content, true, 512, \JSON_BIGINT_AS_STRING | \JSON_THROW_ON_ERROR);
+ } catch (\JsonException $e) {
+ throw new JsonException('Could not decode request body.', $e->getCode(), $e);
+ }
+
+ 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.
+ */
+ public function getETags(): array
+ {
+ return preg_split('/\s*,\s*/', $this->headers->get('If-None-Match', ''), -1, \PREG_SPLIT_NO_EMPTY);
+ }
+
+ public function isNoCache(): bool
+ {
+ 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 (!isset($this->preferredFormat) && null !== $preferredFormat = $this->getRequestFormat(null)) {
+ $this->preferredFormat = $preferredFormat;
+ }
+
+ if ($this->preferredFormat ?? 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
+ */
+ public function getPreferredLanguage(?array $locales = null): ?string
+ {
+ $preferredLanguages = $this->getLanguages();
+
+ if (!$locales) {
+ return $preferredLanguages[0] ?? null;
+ }
+
+ $locales = array_map($this->formatLocale(...), $locales);
+ if (!$preferredLanguages) {
+ return $locales[0];
+ }
+
+ $combinations = array_merge(...array_map($this->getLanguageCombinations(...), $preferredLanguages));
+ foreach ($combinations as $combination) {
+ foreach ($locales as $locale) {
+ if (str_starts_with($locale, $combination)) {
+ return $locale;
+ }
+ }
+ }
+
+ return $locales[0];
+ }
+
+ /**
+ * Gets a list of languages acceptable by the client browser ordered in the user browser preferences.
+ *
+ * @return string[]
+ */
+ public function getLanguages(): array
+ {
+ if (null !== $this->languages) {
+ return $this->languages;
+ }
+
+ $languages = AcceptHeader::fromString($this->headers->get('Accept-Language'))->all();
+ $this->languages = [];
+ foreach ($languages as $acceptHeaderItem) {
+ $lang = $acceptHeaderItem->getValue();
+ $this->languages[] = self::formatLocale($lang);
+ }
+ $this->languages = array_unique($this->languages);
+
+ return $this->languages;
+ }
+
+ /**
+ * Strips the locale to only keep the canonicalized language value.
+ *
+ * Depending on the $locale value, this method can return values like :
+ * - language_Script_REGION: "fr_Latn_FR", "zh_Hans_TW"
+ * - language_Script: "fr_Latn", "zh_Hans"
+ * - language_REGION: "fr_FR", "zh_TW"
+ * - language: "fr", "zh"
+ *
+ * Invalid locale values are returned as is.
+ *
+ * @see https://wikipedia.org/wiki/IETF_language_tag
+ * @see https://datatracker.ietf.org/doc/html/rfc5646
+ */
+ private static function formatLocale(string $locale): string
+ {
+ [$language, $script, $region] = self::getLanguageComponents($locale);
+
+ return implode('_', array_filter([$language, $script, $region]));
+ }
+
+ /**
+ * Returns an array of all possible combinations of the language components.
+ *
+ * For instance, if the locale is "fr_Latn_FR", this method will return:
+ * - "fr_Latn_FR"
+ * - "fr_Latn"
+ * - "fr_FR"
+ * - "fr"
+ *
+ * @return string[]
+ */
+ private static function getLanguageCombinations(string $locale): array
+ {
+ [$language, $script, $region] = self::getLanguageComponents($locale);
+
+ return array_unique([
+ implode('_', array_filter([$language, $script, $region])),
+ implode('_', array_filter([$language, $script])),
+ implode('_', array_filter([$language, $region])),
+ $language,
+ ]);
+ }
+
+ /**
+ * Returns an array with the language components of the locale.
+ *
+ * For example:
+ * - If the locale is "fr_Latn_FR", this method will return "fr", "Latn", "FR"
+ * - If the locale is "fr_FR", this method will return "fr", null, "FR"
+ * - If the locale is "zh_Hans", this method will return "zh", "Hans", null
+ *
+ * @see https://wikipedia.org/wiki/IETF_language_tag
+ * @see https://datatracker.ietf.org/doc/html/rfc5646
+ *
+ * @return array{string, string|null, string|null}
+ */
+ private static function getLanguageComponents(string $locale): array
+ {
+ $locale = str_replace('_', '-', strtolower($locale));
+ $pattern = '/^([a-zA-Z]{2,3}|i-[a-zA-Z]{5,})(?:-([a-zA-Z]{4}))?(?:-([a-zA-Z]{2}))?(?:-(.+))?$/';
+ if (!preg_match($pattern, $locale, $matches)) {
+ return [$locale, null, null];
+ }
+ if (str_starts_with($matches[1], 'i-')) {
+ // 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
+ $matches[1] = substr($matches[1], 2);
+ }
+
+ return [
+ $matches[1],
+ isset($matches[2]) ? ucfirst(strtolower($matches[2])) : null,
+ isset($matches[3]) ? strtoupper($matches[3]) : null,
+ ];
+ }
+
+ /**
+ * Gets a list of charsets acceptable by the client browser in preferable order.
+ *
+ * @return string[]
+ */
+ public function getCharsets(): array
+ {
+ return $this->charsets ??= array_map('strval', array_keys(AcceptHeader::fromString($this->headers->get('Accept-Charset'))->all()));
+ }
+
+ /**
+ * Gets a list of encodings acceptable by the client browser in preferable order.
+ *
+ * @return string[]
+ */
+ public function getEncodings(): array
+ {
+ return $this->encodings ??= array_map('strval', 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 string[]
+ */
+ public function getAcceptableContentTypes(): array
+ {
+ return $this->acceptableContentTypes ??= array_map('strval', 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
+ */
+ public function isXmlHttpRequest(): bool
+ {
+ 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 (isset($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(): string
+ {
+ $requestUri = '';
+
+ if ($this->isIisRewrite() && '' != $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');
+ } 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.
+ */
+ protected function prepareBaseUrl(): string
+ {
+ $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 (!$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.
+ */
+ protected function prepareBasePath(): string
+ {
+ $baseUrl = $this->getBaseUrl();
+ if (!$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.
+ */
+ protected function preparePathInfo(): string
+ {
+ 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 ('' === $pathInfo || '/' !== $pathInfo[0]) {
+ return '/'.$pathInfo;
+ }
+
+ return $pathInfo;
+ }
+
+ /**
+ * Initializes HTTP request formats.
+ */
+ protected static function initializeFormats(): void
+ {
+ 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'],
+ 'soap' => ['application/soap+xml'],
+ 'problem' => ['application/problem+json'],
+ 'hal' => ['application/hal+json', 'application/hal+xml'],
+ 'jsonapi' => ['application/vnd.api+json'],
+ 'yaml' => ['text/yaml', 'application/x-yaml'],
+ 'wbxml' => ['application/vnd.wap.wbxml'],
+ 'pdf' => ['application/pdf'],
+ 'csv' => ['text/csv'],
+ ];
+ }
+
+ 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) {
+ }
+ }
+
+ /**
+ * 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 ($this->isIisRewrite()) {
+ // ISS with UrlRewriteModule might report SCRIPT_NAME/PHP_SELF with wrong case
+ // see https://github.com/php/php-src/issues/11981
+ if (0 !== stripos(rawurldecode($string), $prefix)) {
+ return null;
+ }
+ } elseif (!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): static
+ {
+ 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.
+ */
+ public function isFromTrustedProxy(): bool
+ {
+ return self::$trustedProxies && IpUtils::checkIp($this->server->get('REMOTE_ADDR', ''), self::$trustedProxies);
+ }
+
+ /**
+ * This method is rather heavy because it splits and merges headers, and it's called by many other methods such as
+ * getPort(), isSecure(), getHost(), getClientIps(), getBaseUrl() etc. Thus, we try to cache the results for
+ * best performance.
+ */
+ private function getTrustedValues(int $type, ?string $ip = null): array
+ {
+ $cacheKey = $type."\0".((self::$trustedHeaderSet & $type) ? $this->headers->get(self::TRUSTED_HEADERS[$type]) : '');
+ $cacheKey .= "\0".$ip."\0".$this->headers->get(self::TRUSTED_HEADERS[self::HEADER_FORWARDED]);
+
+ if (isset($this->trustedValuesCache[$cacheKey])) {
+ return $this->trustedValuesCache[$cacheKey];
+ }
+
+ $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, ',;=');
+ $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 $this->trustedValuesCache[$cacheKey] = $forwardedValues;
+ }
+
+ if (!$forwardedValues) {
+ return $this->trustedValuesCache[$cacheKey] = $clientValues;
+ }
+
+ if (!$this->isForwardedValid) {
+ return $this->trustedValuesCache[$cacheKey] = 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
+ $firstTrustedIp ??= $clientIp;
+ }
+ }
+
+ // Now the IP chain contains only untrusted proxies and the client IP
+ return $clientIps ? array_reverse($clientIps) : [$firstTrustedIp];
+ }
+
+ /**
+ * Is this IIS with UrlRewriteModule?
+ *
+ * This method consumes, caches and removed the IIS_WasUrlRewritten env var,
+ * so we don't inherit it to sub-requests.
+ */
+ private function isIisRewrite(): bool
+ {
+ if (1 === $this->server->getInt('IIS_WasUrlRewritten')) {
+ $this->isIisRewrite = true;
+ $this->server->remove('IIS_WasUrlRewritten');
+ }
+
+ return $this->isIisRewrite;
+ }
+
+ /**
+ * See https://url.spec.whatwg.org/.
+ */
+ private static function isHostValid(string $host): bool
+ {
+ if ('[' === $host[0]) {
+ return ']' === $host[-1] && filter_var(substr($host, 1, -1), \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6);
+ }
+
+ if (preg_match('/\.[0-9]++\.?$/D', $host)) {
+ return null !== filter_var($host, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4 | \FILTER_NULL_ON_FAILURE);
+ }
+
+ // use preg_replace() instead of preg_match() to prevent DoS attacks with long host names
+ return '' === preg_replace('/[-a-zA-Z0-9_]++\.?/', '', $host);
+ }
+}
diff --git a/vendor/symfony/http-foundation/RequestMatcher/AttributesRequestMatcher.php b/vendor/symfony/http-foundation/RequestMatcher/AttributesRequestMatcher.php
new file mode 100644
index 0000000..09d6f49
--- /dev/null
+++ b/vendor/symfony/http-foundation/RequestMatcher/AttributesRequestMatcher.php
@@ -0,0 +1,45 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\RequestMatcher;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestMatcherInterface;
+
+/**
+ * Checks the Request attributes matches all regular expressions.
+ *
+ * @author Fabien Potencier
+ */
+class AttributesRequestMatcher implements RequestMatcherInterface
+{
+ /**
+ * @param array $regexps
+ */
+ public function __construct(private array $regexps)
+ {
+ }
+
+ public function matches(Request $request): bool
+ {
+ foreach ($this->regexps as $key => $regexp) {
+ $attribute = $request->attributes->get($key);
+ if (!\is_string($attribute)) {
+ return false;
+ }
+ if (!preg_match('{'.$regexp.'}', $attribute)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/vendor/symfony/http-foundation/RequestMatcher/ExpressionRequestMatcher.php b/vendor/symfony/http-foundation/RequestMatcher/ExpressionRequestMatcher.php
new file mode 100644
index 0000000..935853f
--- /dev/null
+++ b/vendor/symfony/http-foundation/RequestMatcher/ExpressionRequestMatcher.php
@@ -0,0 +1,43 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\RequestMatcher;
+
+use Symfony\Component\ExpressionLanguage\Expression;
+use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestMatcherInterface;
+
+/**
+ * ExpressionRequestMatcher uses an expression to match a Request.
+ *
+ * @author Fabien Potencier
+ */
+class ExpressionRequestMatcher implements RequestMatcherInterface
+{
+ public function __construct(
+ private ExpressionLanguage $language,
+ private Expression|string $expression,
+ ) {
+ }
+
+ public function matches(Request $request): bool
+ {
+ 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(),
+ ]);
+ }
+}
diff --git a/vendor/symfony/http-foundation/RequestMatcher/HeaderRequestMatcher.php b/vendor/symfony/http-foundation/RequestMatcher/HeaderRequestMatcher.php
new file mode 100644
index 0000000..8617a8a
--- /dev/null
+++ b/vendor/symfony/http-foundation/RequestMatcher/HeaderRequestMatcher.php
@@ -0,0 +1,52 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\RequestMatcher;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestMatcherInterface;
+
+/**
+ * Checks the presence of HTTP headers in a Request.
+ *
+ * @author Alexandre Daubois
+ */
+class HeaderRequestMatcher implements RequestMatcherInterface
+{
+ /**
+ * @var string[]
+ */
+ private array $headers;
+
+ /**
+ * @param string[]|string $headers A header or a list of headers
+ * Strings can contain a comma-delimited list of headers
+ */
+ public function __construct(array|string $headers)
+ {
+ $this->headers = array_reduce((array) $headers, static fn (array $headers, string $header) => array_merge($headers, preg_split('/\s*,\s*/', $header)), []);
+ }
+
+ public function matches(Request $request): bool
+ {
+ if (!$this->headers) {
+ return true;
+ }
+
+ foreach ($this->headers as $header) {
+ if (!$request->headers->has($header)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/vendor/symfony/http-foundation/RequestMatcher/HostRequestMatcher.php b/vendor/symfony/http-foundation/RequestMatcher/HostRequestMatcher.php
new file mode 100644
index 0000000..2836759
--- /dev/null
+++ b/vendor/symfony/http-foundation/RequestMatcher/HostRequestMatcher.php
@@ -0,0 +1,32 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\RequestMatcher;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestMatcherInterface;
+
+/**
+ * Checks the Request URL host name matches a regular expression.
+ *
+ * @author Fabien Potencier
+ */
+class HostRequestMatcher implements RequestMatcherInterface
+{
+ public function __construct(private string $regexp)
+ {
+ }
+
+ public function matches(Request $request): bool
+ {
+ return preg_match('{'.$this->regexp.'}i', $request->getHost());
+ }
+}
diff --git a/vendor/symfony/http-foundation/RequestMatcher/IpsRequestMatcher.php b/vendor/symfony/http-foundation/RequestMatcher/IpsRequestMatcher.php
new file mode 100644
index 0000000..333612e
--- /dev/null
+++ b/vendor/symfony/http-foundation/RequestMatcher/IpsRequestMatcher.php
@@ -0,0 +1,44 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\RequestMatcher;
+
+use Symfony\Component\HttpFoundation\IpUtils;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestMatcherInterface;
+
+/**
+ * Checks the client IP of a Request.
+ *
+ * @author Fabien Potencier
+ */
+class IpsRequestMatcher implements RequestMatcherInterface
+{
+ private array $ips;
+
+ /**
+ * @param string[]|string $ips A specific IP address or a range specified using IP/netmask like 192.168.1.0/24
+ * Strings can contain a comma-delimited list of IPs/ranges
+ */
+ public function __construct(array|string $ips)
+ {
+ $this->ips = array_reduce((array) $ips, static fn (array $ips, string $ip) => array_merge($ips, preg_split('/\s*,\s*/', $ip)), []);
+ }
+
+ public function matches(Request $request): bool
+ {
+ if (!$this->ips) {
+ return true;
+ }
+
+ return IpUtils::checkIp($request->getClientIp() ?? '', $this->ips);
+ }
+}
diff --git a/vendor/symfony/http-foundation/RequestMatcher/IsJsonRequestMatcher.php b/vendor/symfony/http-foundation/RequestMatcher/IsJsonRequestMatcher.php
new file mode 100644
index 0000000..875f992
--- /dev/null
+++ b/vendor/symfony/http-foundation/RequestMatcher/IsJsonRequestMatcher.php
@@ -0,0 +1,28 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\RequestMatcher;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestMatcherInterface;
+
+/**
+ * Checks the Request content is valid JSON.
+ *
+ * @author Fabien Potencier
+ */
+class IsJsonRequestMatcher implements RequestMatcherInterface
+{
+ public function matches(Request $request): bool
+ {
+ return json_validate($request->getContent());
+ }
+}
diff --git a/vendor/symfony/http-foundation/RequestMatcher/MethodRequestMatcher.php b/vendor/symfony/http-foundation/RequestMatcher/MethodRequestMatcher.php
new file mode 100644
index 0000000..b37f6e3
--- /dev/null
+++ b/vendor/symfony/http-foundation/RequestMatcher/MethodRequestMatcher.php
@@ -0,0 +1,46 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\RequestMatcher;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestMatcherInterface;
+
+/**
+ * Checks the HTTP method of a Request.
+ *
+ * @author Fabien Potencier
+ */
+class MethodRequestMatcher implements RequestMatcherInterface
+{
+ /**
+ * @var string[]
+ */
+ private array $methods = [];
+
+ /**
+ * @param string[]|string $methods An HTTP method or an array of HTTP methods
+ * Strings can contain a comma-delimited list of methods
+ */
+ public function __construct(array|string $methods)
+ {
+ $this->methods = array_reduce(array_map('strtoupper', (array) $methods), static fn (array $methods, string $method) => array_merge($methods, preg_split('/\s*,\s*/', $method)), []);
+ }
+
+ public function matches(Request $request): bool
+ {
+ if (!$this->methods) {
+ return true;
+ }
+
+ return \in_array($request->getMethod(), $this->methods, true);
+ }
+}
diff --git a/vendor/symfony/http-foundation/RequestMatcher/PathRequestMatcher.php b/vendor/symfony/http-foundation/RequestMatcher/PathRequestMatcher.php
new file mode 100644
index 0000000..c7c7a02
--- /dev/null
+++ b/vendor/symfony/http-foundation/RequestMatcher/PathRequestMatcher.php
@@ -0,0 +1,32 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\RequestMatcher;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestMatcherInterface;
+
+/**
+ * Checks the Request URL path info matches a regular expression.
+ *
+ * @author Fabien Potencier
+ */
+class PathRequestMatcher implements RequestMatcherInterface
+{
+ public function __construct(private string $regexp)
+ {
+ }
+
+ public function matches(Request $request): bool
+ {
+ return preg_match('{'.$this->regexp.'}', rawurldecode($request->getPathInfo()));
+ }
+}
diff --git a/vendor/symfony/http-foundation/RequestMatcher/PortRequestMatcher.php b/vendor/symfony/http-foundation/RequestMatcher/PortRequestMatcher.php
new file mode 100644
index 0000000..5a01ce9
--- /dev/null
+++ b/vendor/symfony/http-foundation/RequestMatcher/PortRequestMatcher.php
@@ -0,0 +1,32 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\RequestMatcher;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestMatcherInterface;
+
+/**
+ * Checks the HTTP port of a Request.
+ *
+ * @author Fabien Potencier
+ */
+class PortRequestMatcher implements RequestMatcherInterface
+{
+ public function __construct(private int $port)
+ {
+ }
+
+ public function matches(Request $request): bool
+ {
+ return $request->getPort() === $this->port;
+ }
+}
diff --git a/vendor/symfony/http-foundation/RequestMatcher/QueryParameterRequestMatcher.php b/vendor/symfony/http-foundation/RequestMatcher/QueryParameterRequestMatcher.php
new file mode 100644
index 0000000..86161e7
--- /dev/null
+++ b/vendor/symfony/http-foundation/RequestMatcher/QueryParameterRequestMatcher.php
@@ -0,0 +1,46 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\RequestMatcher;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestMatcherInterface;
+
+/**
+ * Checks the presence of HTTP query parameters of a Request.
+ *
+ * @author Alexandre Daubois
+ */
+class QueryParameterRequestMatcher implements RequestMatcherInterface
+{
+ /**
+ * @var string[]
+ */
+ private array $parameters;
+
+ /**
+ * @param string[]|string $parameters A parameter or a list of parameters
+ * Strings can contain a comma-delimited list of query parameters
+ */
+ public function __construct(array|string $parameters)
+ {
+ $this->parameters = array_reduce(array_map(strtolower(...), (array) $parameters), static fn (array $parameters, string $parameter) => array_merge($parameters, preg_split('/\s*,\s*/', $parameter)), []);
+ }
+
+ public function matches(Request $request): bool
+ {
+ if (!$this->parameters) {
+ return true;
+ }
+
+ return 0 === \count(array_diff_assoc($this->parameters, $request->query->keys()));
+ }
+}
diff --git a/vendor/symfony/http-foundation/RequestMatcher/SchemeRequestMatcher.php b/vendor/symfony/http-foundation/RequestMatcher/SchemeRequestMatcher.php
new file mode 100644
index 0000000..9c9cd58
--- /dev/null
+++ b/vendor/symfony/http-foundation/RequestMatcher/SchemeRequestMatcher.php
@@ -0,0 +1,46 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\RequestMatcher;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestMatcherInterface;
+
+/**
+ * Checks the HTTP scheme of a Request.
+ *
+ * @author Fabien Potencier
+ */
+class SchemeRequestMatcher implements RequestMatcherInterface
+{
+ /**
+ * @var string[]
+ */
+ private array $schemes;
+
+ /**
+ * @param string[]|string $schemes A scheme or a list of schemes
+ * Strings can contain a comma-delimited list of schemes
+ */
+ public function __construct(array|string $schemes)
+ {
+ $this->schemes = array_reduce(array_map('strtolower', (array) $schemes), static fn (array $schemes, string $scheme) => array_merge($schemes, preg_split('/\s*,\s*/', $scheme)), []);
+ }
+
+ public function matches(Request $request): bool
+ {
+ if (!$this->schemes) {
+ return true;
+ }
+
+ return \in_array($request->getScheme(), $this->schemes, true);
+ }
+}
diff --git a/vendor/symfony/http-foundation/RequestMatcherInterface.php b/vendor/symfony/http-foundation/RequestMatcherInterface.php
new file mode 100644
index 0000000..6dcc3e0
--- /dev/null
+++ b/vendor/symfony/http-foundation/RequestMatcherInterface.php
@@ -0,0 +1,25 @@
+
+ *
+ * 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
+ */
+interface RequestMatcherInterface
+{
+ /**
+ * Decides whether the rule(s) implemented by the strategy matches the supplied request.
+ */
+ public function matches(Request $request): bool;
+}
diff --git a/vendor/symfony/http-foundation/RequestStack.php b/vendor/symfony/http-foundation/RequestStack.php
new file mode 100644
index 0000000..153bd9a
--- /dev/null
+++ b/vendor/symfony/http-foundation/RequestStack.php
@@ -0,0 +1,124 @@
+
+ *
+ * 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
+ */
+class RequestStack
+{
+ /**
+ * @var Request[]
+ */
+ private array $requests = [];
+
+ /**
+ * @param Request[] $requests
+ */
+ public function __construct(array $requests = [])
+ {
+ foreach ($requests as $request) {
+ $this->push($request);
+ }
+ }
+
+ /**
+ * 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): void
+ {
+ $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.
+ */
+ public function pop(): ?Request
+ {
+ if (!$this->requests) {
+ return null;
+ }
+
+ return array_pop($this->requests);
+ }
+
+ public function getCurrentRequest(): ?Request
+ {
+ 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];
+ }
+
+ /**
+ * 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.
+ */
+ public function getParentRequest(): ?Request
+ {
+ $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();
+ }
+
+ public function resetRequestFormats(): void
+ {
+ static $resetRequestFormats;
+ $resetRequestFormats ??= \Closure::bind(static fn () => self::$formats = null, null, Request::class);
+ $resetRequestFormats();
+ }
+}
diff --git a/vendor/symfony/http-foundation/Response.php b/vendor/symfony/http-foundation/Response.php
new file mode 100644
index 0000000..7cd3b87
--- /dev/null
+++ b/vendor/symfony/http-foundation/Response.php
@@ -0,0 +1,1322 @@
+
+ *
+ * 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
+ */
+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; // RFC7725
+ 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,
+ 'stale_if_error' => true, // RFC5861
+ 'stale_while_revalidate' => true, // RFC5861
+ 'immutable' => false,
+ 'last_modified' => true,
+ 'etag' => true,
+ ];
+
+ public ResponseHeaderBag $headers;
+
+ protected string $content;
+ protected string $version;
+ protected int $statusCode;
+ protected string $statusText;
+ protected ?string $charset = null;
+
+ /**
+ * 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 array $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
+ ];
+
+ /**
+ * Tracks headers already sent in informational responses.
+ */
+ private array $sentHeaders;
+
+ /**
+ * @param int $status The HTTP status code (200 "OK" by default)
+ *
+ * @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');
+ }
+
+ /**
+ * 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.
+ *
+ * @see prepare()
+ */
+ public function __toString(): string
+ {
+ 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): static
+ {
+ $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.
+ *
+ * @param positive-int|null $statusCode The status code to use, override the statusCode property if set and not null
+ *
+ * @return $this
+ */
+ public function sendHeaders(?int $statusCode = null): static
+ {
+ // headers have already been sent by the developer
+ if (headers_sent()) {
+ if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
+ $statusCode ??= $this->statusCode;
+ header(\sprintf('HTTP/%s %s %s', $this->version, $statusCode, $this->statusText), true, $statusCode);
+ }
+
+ return $this;
+ }
+
+ $informationalResponse = $statusCode >= 100 && $statusCode < 200;
+ if ($informationalResponse && !\function_exists('headers_send')) {
+ // skip informational responses if not supported by the SAPI
+ return $this;
+ }
+
+ // headers
+ foreach ($this->headers->allPreserveCaseWithoutCookies() as $name => $values) {
+ // As recommended by RFC 8297, PHP automatically copies headers from previous 103 responses, we need to deal with that if headers changed
+ $previousValues = $this->sentHeaders[$name] ?? null;
+ if ($previousValues === $values) {
+ // Header already sent in a previous response, it will be automatically copied in this response by PHP
+ continue;
+ }
+
+ $replace = 0 === strcasecmp($name, 'Content-Type');
+
+ if (null !== $previousValues && array_diff($previousValues, $values)) {
+ header_remove($name);
+ $previousValues = null;
+ }
+
+ $newValues = null === $previousValues ? $values : array_diff($values, $previousValues);
+
+ foreach ($newValues as $value) {
+ header($name.': '.$value, $replace, $this->statusCode);
+ }
+
+ if ($informationalResponse) {
+ $this->sentHeaders[$name] = $values;
+ }
+ }
+
+ // cookies
+ foreach ($this->headers->getCookies() as $cookie) {
+ header('Set-Cookie: '.$cookie, false, $this->statusCode);
+ }
+
+ if ($informationalResponse) {
+ headers_send($statusCode);
+
+ return $this;
+ }
+
+ $statusCode ??= $this->statusCode;
+
+ // status
+ header(\sprintf('HTTP/%s %s %s', $this->version, $statusCode, $this->statusText), true, $statusCode);
+
+ return $this;
+ }
+
+ /**
+ * Sends content for the current web response.
+ *
+ * @return $this
+ */
+ public function sendContent(): static
+ {
+ echo $this->content;
+
+ return $this;
+ }
+
+ /**
+ * Sends HTTP headers and content.
+ *
+ * @param bool $flush Whether output buffers should be flushed
+ *
+ * @return $this
+ */
+ public function send(bool $flush = true): static
+ {
+ $this->sendHeaders();
+ $this->sendContent();
+
+ if (!$flush) {
+ return $this;
+ }
+
+ 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', 'embed'], true)) {
+ static::closeOutputBuffers(0, true);
+ flush();
+ }
+
+ return $this;
+ }
+
+ /**
+ * Sets the response content.
+ *
+ * @return $this
+ */
+ public function setContent(?string $content): static
+ {
+ $this->content = $content ?? '';
+
+ return $this;
+ }
+
+ /**
+ * Gets the current response content.
+ */
+ public function getContent(): string|false
+ {
+ return $this->content;
+ }
+
+ /**
+ * Sets the HTTP protocol version (1.0 or 1.1).
+ *
+ * @return $this
+ *
+ * @final
+ */
+ public function setProtocolVersion(string $version): static
+ {
+ $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): static
+ {
+ $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;
+ }
+
+ $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): static
+ {
+ $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], true)) {
+ 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(): static
+ {
+ $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(): static
+ {
+ $this->headers->addCacheControlDirective('public');
+ $this->headers->removeCacheControlDirective('private');
+
+ return $this;
+ }
+
+ /**
+ * Marks the response as "immutable".
+ *
+ * @return $this
+ *
+ * @final
+ */
+ public function setImmutable(bool $immutable = true): static
+ {
+ 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(): ?\DateTimeImmutable
+ {
+ return $this->headers->getDate('Date');
+ }
+
+ /**
+ * Sets the Date header.
+ *
+ * @return $this
+ *
+ * @final
+ */
+ public function setDate(\DateTimeInterface $date): static
+ {
+ $date = \DateTimeImmutable::createFromInterface($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(): static
+ {
+ 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(): ?\DateTimeImmutable
+ {
+ try {
+ return $this->headers->getDate('Expires');
+ } catch (\RuntimeException) {
+ // according to RFC 2616 invalid date formats (e.g. "0" and "-1") must be treated as in the past
+ return \DateTimeImmutable::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): static
+ {
+ if (null === $date) {
+ $this->headers->remove('Expires');
+
+ return $this;
+ }
+
+ $date = \DateTimeImmutable::createFromInterface($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 !== $expires = $this->getExpires()) {
+ $maxAge = (int) $expires->format('U') - (int) $this->getDate()->format('U');
+
+ return max($maxAge, 0);
+ }
+
+ return null;
+ }
+
+ /**
+ * Sets the number of seconds after which the response should no longer be considered fresh.
+ *
+ * This method sets the Cache-Control max-age directive.
+ *
+ * @return $this
+ *
+ * @final
+ */
+ public function setMaxAge(int $value): static
+ {
+ $this->headers->addCacheControlDirective('max-age', $value);
+
+ return $this;
+ }
+
+ /**
+ * Sets the number of seconds after which the response should no longer be returned by shared caches when backend is down.
+ *
+ * This method sets the Cache-Control stale-if-error directive.
+ *
+ * @return $this
+ *
+ * @final
+ */
+ public function setStaleIfError(int $value): static
+ {
+ $this->headers->addCacheControlDirective('stale-if-error', $value);
+
+ return $this;
+ }
+
+ /**
+ * Sets the number of seconds after which the response should no longer return stale content by shared caches.
+ *
+ * This method sets the Cache-Control stale-while-revalidate directive.
+ *
+ * @return $this
+ *
+ * @final
+ */
+ public function setStaleWhileRevalidate(int $value): static
+ {
+ $this->headers->addCacheControlDirective('stale-while-revalidate', $value);
+
+ return $this;
+ }
+
+ /**
+ * Sets the number of seconds after which the response should no longer be considered fresh by shared caches.
+ *
+ * This method sets the Cache-Control s-maxage directive.
+ *
+ * @return $this
+ *
+ * @final
+ */
+ public function setSharedMaxAge(int $value): static
+ {
+ $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 response's 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 ? max($maxAge - $this->getAge(), 0) : 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): static
+ {
+ $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): static
+ {
+ $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(): ?\DateTimeImmutable
+ {
+ 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): static
+ {
+ if (null === $date) {
+ $this->headers->remove('Last-Modified');
+
+ return $this;
+ }
+
+ $date = \DateTimeImmutable::createFromInterface($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, bool $weak = false): static
+ {
+ 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): static
+ {
+ 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']);
+ }
+
+ if (isset($options['stale_while_revalidate'])) {
+ $this->setStaleWhileRevalidate($options['stale_while_revalidate']);
+ }
+
+ if (isset($options['stale_if_error'])) {
+ $this->setStaleIfError($options['stale_if_error']);
+ }
+
+ 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(): static
+ {
+ $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 bool $replace Whether to replace the actual value or not (true by default)
+ *
+ * @return $this
+ *
+ * @final
+ */
+ public function setVary(string|array $headers, bool $replace = true): static
+ {
+ $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], true) && (null === $location ?: $location == $this->headers->get('Location'));
+ }
+
+ /**
+ * Is the response empty?
+ *
+ * @final
+ */
+ public function isEmpty(): bool
+ {
+ return \in_array($this->statusCode, [204, 304], true);
+ }
+
+ /**
+ * 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/vendor/symfony/http-foundation/ResponseHeaderBag.php b/vendor/symfony/http-foundation/ResponseHeaderBag.php
new file mode 100644
index 0000000..5f11ffd
--- /dev/null
+++ b/vendor/symfony/http-foundation/ResponseHeaderBag.php
@@ -0,0 +1,267 @@
+
+ *
+ * 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
+ */
+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 array $computedCacheControl = [];
+ protected array $cookies = [];
+ protected array $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.
+ */
+ public function allPreserveCase(): array
+ {
+ $headers = [];
+ foreach ($this->all() as $name => $value) {
+ $headers[$this->headerNames[$name] ?? $name] = $value;
+ }
+
+ return $headers;
+ }
+
+ public function allPreserveCaseWithoutCookies(): array
+ {
+ $headers = $this->allPreserveCase();
+ if (isset($this->headerNames['set-cookie'])) {
+ unset($headers[$this->headerNames['set-cookie']]);
+ }
+
+ return $headers;
+ }
+
+ public function replace(array $headers = []): void
+ {
+ $this->headerNames = [];
+
+ parent::replace($headers);
+
+ if (!isset($this->headers['cache-control'])) {
+ $this->set('Cache-Control', '');
+ }
+
+ if (!isset($this->headers['date'])) {
+ $this->initDate();
+ }
+ }
+
+ public function all(?string $key = null): array
+ {
+ $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;
+ }
+
+ public function set(string $key, string|array|null $values, bool $replace = true): void
+ {
+ $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);
+ }
+ }
+
+ public function remove(string $key): void
+ {
+ $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();
+ }
+ }
+
+ public function hasCacheControlDirective(string $key): bool
+ {
+ return \array_key_exists($key, $this->computedCacheControl);
+ }
+
+ public function getCacheControlDirective(string $key): bool|string|null
+ {
+ return $this->computedCacheControl[$key] ?? null;
+ }
+
+ public function setCookie(Cookie $cookie): void
+ {
+ $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): void
+ {
+ $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 (!$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): array
+ {
+ if (!\in_array($format, [self::COOKIES_FLAT, self::COOKIES_ARRAY], true)) {
+ 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, bool $partitioned = false): void
+ {
+ $this->setCookie(new Cookie($name, null, 1, $path, $domain, $secure, $httpOnly, false, $sameSite, $partitioned));
+ }
+
+ /**
+ * @see HeaderUtils::makeDisposition()
+ */
+ public function makeDisposition(string $disposition, string $filename, string $filenameFallback = ''): string
+ {
+ 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.
+ */
+ protected function computeCacheControlValue(): string
+ {
+ 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/vendor/symfony/http-foundation/ServerBag.php b/vendor/symfony/http-foundation/ServerBag.php
new file mode 100644
index 0000000..09fc386
--- /dev/null
+++ b/vendor/symfony/http-foundation/ServerBag.php
@@ -0,0 +1,97 @@
+
+ *
+ * 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
+ * @author Bulat Shakirzyanov
+ * @author Robert Kiss
+ */
+class ServerBag extends ParameterBag
+{
+ /**
+ * Gets the HTTP headers.
+ */
+ public function getHeaders(): array
+ {
+ $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) && '' !== $value) {
+ $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 ^(.*)$ index.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/vendor/symfony/http-foundation/ServerEvent.php b/vendor/symfony/http-foundation/ServerEvent.php
new file mode 100644
index 0000000..7597058
--- /dev/null
+++ b/vendor/symfony/http-foundation/ServerEvent.php
@@ -0,0 +1,145 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation;
+
+/**
+ * An event generated on the server intended for streaming to the client
+ * as part of the SSE streaming technique.
+ *
+ * @implements \IteratorAggregate
+ *
+ * @author Yonel Ceruto
+ */
+class ServerEvent implements \IteratorAggregate
+{
+ /**
+ * @param string|iterable $data The event data field for the message
+ * @param string|null $type The event type
+ * @param int|null $retry The number of milliseconds the client should wait
+ * before reconnecting in case of network failure
+ * @param string|null $id The event ID to set the EventSource object's last event ID value
+ * @param string|null $comment The event comment
+ */
+ public function __construct(
+ private string|iterable $data,
+ private ?string $type = null,
+ private ?int $retry = null,
+ private ?string $id = null,
+ private ?string $comment = null,
+ ) {
+ }
+
+ public function getData(): iterable|string
+ {
+ return $this->data;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setData(iterable|string $data): static
+ {
+ $this->data = $data;
+
+ return $this;
+ }
+
+ public function getType(): ?string
+ {
+ return $this->type;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setType(string $type): static
+ {
+ $this->type = $type;
+
+ return $this;
+ }
+
+ public function getRetry(): ?int
+ {
+ return $this->retry;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setRetry(?int $retry): static
+ {
+ $this->retry = $retry;
+
+ return $this;
+ }
+
+ public function getId(): ?string
+ {
+ return $this->id;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setId(string $id): static
+ {
+ $this->id = $id;
+
+ return $this;
+ }
+
+ public function getComment(): ?string
+ {
+ return $this->comment;
+ }
+
+ public function setComment(string $comment): static
+ {
+ $this->comment = $comment;
+
+ return $this;
+ }
+
+ /**
+ * @return \Traversable
+ */
+ public function getIterator(): \Traversable
+ {
+ static $lastRetry = null;
+
+ $head = '';
+ if ($this->comment) {
+ $head .= \sprintf(': %s', $this->comment)."\n";
+ }
+ if ($this->id) {
+ $head .= \sprintf('id: %s', $this->id)."\n";
+ }
+ if ($this->retry > 0 && $this->retry !== $lastRetry) {
+ $head .= \sprintf('retry: %s', $lastRetry = $this->retry)."\n";
+ }
+ if ($this->type) {
+ $head .= \sprintf('event: %s', $this->type)."\n";
+ }
+ yield $head;
+
+ if (is_iterable($this->data)) {
+ foreach ($this->data as $data) {
+ yield \sprintf('data: %s', $data)."\n";
+ }
+ } elseif ('' !== $this->data) {
+ yield \sprintf('data: %s', $this->data)."\n";
+ }
+
+ yield "\n";
+ }
+}
diff --git a/vendor/symfony/http-foundation/Session/Attribute/AttributeBag.php b/vendor/symfony/http-foundation/Session/Attribute/AttributeBag.php
new file mode 100644
index 0000000..e34a497
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/Attribute/AttributeBag.php
@@ -0,0 +1,117 @@
+
+ *
+ * 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
+ */
+class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Countable
+{
+ protected array $attributes = [];
+
+ private string $name = 'attributes';
+
+ /**
+ * @param string $storageKey The key used to store attributes in the session
+ */
+ public function __construct(
+ private string $storageKey = '_sf2_attributes',
+ ) {
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ public function setName(string $name): void
+ {
+ $this->name = $name;
+ }
+
+ public function initialize(array &$attributes): void
+ {
+ $this->attributes = &$attributes;
+ }
+
+ public function getStorageKey(): string
+ {
+ return $this->storageKey;
+ }
+
+ public function has(string $name): bool
+ {
+ return \array_key_exists($name, $this->attributes);
+ }
+
+ public function get(string $name, mixed $default = null): mixed
+ {
+ return \array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default;
+ }
+
+ public function set(string $name, mixed $value): void
+ {
+ $this->attributes[$name] = $value;
+ }
+
+ public function all(): array
+ {
+ return $this->attributes;
+ }
+
+ public function replace(array $attributes): void
+ {
+ $this->attributes = [];
+ foreach ($attributes as $key => $value) {
+ $this->set($key, $value);
+ }
+ }
+
+ public function remove(string $name): mixed
+ {
+ $retval = null;
+ if (\array_key_exists($name, $this->attributes)) {
+ $retval = $this->attributes[$name];
+ unset($this->attributes[$name]);
+ }
+
+ return $retval;
+ }
+
+ public function clear(): mixed
+ {
+ $return = $this->attributes;
+ $this->attributes = [];
+
+ return $return;
+ }
+
+ /**
+ * Returns an iterator for attributes.
+ *
+ * @return \ArrayIterator
+ */
+ public function getIterator(): \ArrayIterator
+ {
+ return new \ArrayIterator($this->attributes);
+ }
+
+ /**
+ * Returns the number of attributes.
+ */
+ public function count(): int
+ {
+ return \count($this->attributes);
+ }
+}
diff --git a/vendor/symfony/http-foundation/Session/Attribute/AttributeBagInterface.php b/vendor/symfony/http-foundation/Session/Attribute/AttributeBagInterface.php
new file mode 100644
index 0000000..39ec9d7
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/Attribute/AttributeBagInterface.php
@@ -0,0 +1,53 @@
+
+ *
+ * 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
+ */
+interface AttributeBagInterface extends SessionBagInterface
+{
+ /**
+ * Checks if an attribute is defined.
+ */
+ public function has(string $name): bool;
+
+ /**
+ * Returns an attribute.
+ */
+ public function get(string $name, mixed $default = null): mixed;
+
+ /**
+ * Sets an attribute.
+ */
+ public function set(string $name, mixed $value): void;
+
+ /**
+ * Returns attributes.
+ *
+ * @return array
+ */
+ public function all(): array;
+
+ public function replace(array $attributes): void;
+
+ /**
+ * Removes an attribute.
+ *
+ * @return mixed The removed value or null when it does not exist
+ */
+ public function remove(string $name): mixed;
+}
diff --git a/vendor/symfony/http-foundation/Session/Flash/AutoExpireFlashBag.php b/vendor/symfony/http-foundation/Session/Flash/AutoExpireFlashBag.php
new file mode 100644
index 0000000..bfb856d
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/Flash/AutoExpireFlashBag.php
@@ -0,0 +1,121 @@
+
+ *
+ * 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
+ */
+class AutoExpireFlashBag implements FlashBagInterface
+{
+ private string $name = 'flashes';
+ private array $flashes = ['display' => [], 'new' => []];
+
+ /**
+ * @param string $storageKey The key used to store flashes in the session
+ */
+ public function __construct(
+ private string $storageKey = '_symfony_flashes',
+ ) {
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ public function setName(string $name): void
+ {
+ $this->name = $name;
+ }
+
+ public function initialize(array &$flashes): void
+ {
+ $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'] = [];
+ }
+
+ public function add(string $type, mixed $message): void
+ {
+ $this->flashes['new'][$type][] = $message;
+ }
+
+ public function peek(string $type, array $default = []): array
+ {
+ return $this->has($type) ? $this->flashes['display'][$type] : $default;
+ }
+
+ public function peekAll(): array
+ {
+ return \array_key_exists('display', $this->flashes) ? $this->flashes['display'] : [];
+ }
+
+ public function get(string $type, array $default = []): array
+ {
+ $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;
+ }
+
+ public function all(): array
+ {
+ $return = $this->flashes['display'];
+ $this->flashes['display'] = [];
+
+ return $return;
+ }
+
+ public function setAll(array $messages): void
+ {
+ $this->flashes['new'] = $messages;
+ }
+
+ public function set(string $type, string|array $messages): void
+ {
+ $this->flashes['new'][$type] = (array) $messages;
+ }
+
+ public function has(string $type): bool
+ {
+ return \array_key_exists($type, $this->flashes['display']) && $this->flashes['display'][$type];
+ }
+
+ public function keys(): array
+ {
+ return array_keys($this->flashes['display']);
+ }
+
+ public function getStorageKey(): string
+ {
+ return $this->storageKey;
+ }
+
+ public function clear(): mixed
+ {
+ return $this->all();
+ }
+}
diff --git a/vendor/symfony/http-foundation/Session/Flash/FlashBag.php b/vendor/symfony/http-foundation/Session/Flash/FlashBag.php
new file mode 100644
index 0000000..72753a6
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/Flash/FlashBag.php
@@ -0,0 +1,112 @@
+
+ *
+ * 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
+ */
+class FlashBag implements FlashBagInterface
+{
+ private string $name = 'flashes';
+ private array $flashes = [];
+
+ /**
+ * @param string $storageKey The key used to store flashes in the session
+ */
+ public function __construct(
+ private string $storageKey = '_symfony_flashes',
+ ) {
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ public function setName(string $name): void
+ {
+ $this->name = $name;
+ }
+
+ public function initialize(array &$flashes): void
+ {
+ $this->flashes = &$flashes;
+ }
+
+ public function add(string $type, mixed $message): void
+ {
+ $this->flashes[$type][] = $message;
+ }
+
+ public function peek(string $type, array $default = []): array
+ {
+ return $this->has($type) ? $this->flashes[$type] : $default;
+ }
+
+ public function peekAll(): array
+ {
+ return $this->flashes;
+ }
+
+ public function get(string $type, array $default = []): array
+ {
+ if (!$this->has($type)) {
+ return $default;
+ }
+
+ $return = $this->flashes[$type];
+
+ unset($this->flashes[$type]);
+
+ return $return;
+ }
+
+ public function all(): array
+ {
+ $return = $this->peekAll();
+ $this->flashes = [];
+
+ return $return;
+ }
+
+ public function set(string $type, string|array $messages): void
+ {
+ $this->flashes[$type] = (array) $messages;
+ }
+
+ public function setAll(array $messages): void
+ {
+ $this->flashes = $messages;
+ }
+
+ public function has(string $type): bool
+ {
+ return \array_key_exists($type, $this->flashes) && $this->flashes[$type];
+ }
+
+ public function keys(): array
+ {
+ return array_keys($this->flashes);
+ }
+
+ public function getStorageKey(): string
+ {
+ return $this->storageKey;
+ }
+
+ public function clear(): mixed
+ {
+ return $this->all();
+ }
+}
diff --git a/vendor/symfony/http-foundation/Session/Flash/FlashBagInterface.php b/vendor/symfony/http-foundation/Session/Flash/FlashBagInterface.php
new file mode 100644
index 0000000..79e98f5
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/Flash/FlashBagInterface.php
@@ -0,0 +1,72 @@
+
+ *
+ * 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
+ */
+interface FlashBagInterface extends SessionBagInterface
+{
+ /**
+ * Adds a flash message for the given type.
+ */
+ public function add(string $type, mixed $message): void;
+
+ /**
+ * Registers one or more messages for a given type.
+ */
+ public function set(string $type, string|array $messages): void;
+
+ /**
+ * Gets flash messages for a given type.
+ *
+ * @param string $type Message category type
+ * @param array $default Default value if $type does not exist
+ */
+ public function peek(string $type, array $default = []): array;
+
+ /**
+ * Gets all flash messages.
+ */
+ public function peekAll(): array;
+
+ /**
+ * Gets and clears flash from the stack.
+ *
+ * @param array $default Default value if $type does not exist
+ */
+ public function get(string $type, array $default = []): array;
+
+ /**
+ * Gets and clears flashes from the stack.
+ */
+ public function all(): array;
+
+ /**
+ * Sets all flash messages.
+ */
+ public function setAll(array $messages): void;
+
+ /**
+ * Has flash messages for a given type?
+ */
+ public function has(string $type): bool;
+
+ /**
+ * Returns a list of all defined types.
+ */
+ public function keys(): array;
+}
diff --git a/vendor/symfony/http-foundation/Session/FlashBagAwareSessionInterface.php b/vendor/symfony/http-foundation/Session/FlashBagAwareSessionInterface.php
new file mode 100644
index 0000000..90151d3
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/FlashBagAwareSessionInterface.php
@@ -0,0 +1,22 @@
+
+ *
+ * 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\Flash\FlashBagInterface;
+
+/**
+ * Interface for session with a flashbag.
+ */
+interface FlashBagAwareSessionInterface extends SessionInterface
+{
+ public function getFlashBag(): FlashBagInterface;
+}
diff --git a/vendor/symfony/http-foundation/Session/Session.php b/vendor/symfony/http-foundation/Session/Session.php
new file mode 100644
index 0000000..972021f
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/Session.php
@@ -0,0 +1,223 @@
+
+ *
+ * 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\MetadataBag;
+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
+ * @author Drak
+ *
+ * @implements \IteratorAggregate
+ */
+class Session implements FlashBagAwareSessionInterface, \IteratorAggregate, \Countable
+{
+ protected SessionStorageInterface $storage;
+
+ private string $flashName;
+ private string $attributeName;
+ private array $data = [];
+ private int $usageIndex = 0;
+ private ?\Closure $usageReporter;
+
+ public function __construct(?SessionStorageInterface $storage = null, ?AttributeBagInterface $attributes = null, ?FlashBagInterface $flashes = null, ?callable $usageReporter = null)
+ {
+ $this->storage = $storage ?? new NativeSessionStorage();
+ $this->usageReporter = null === $usageReporter ? null : $usageReporter(...);
+
+ $attributes ??= new AttributeBag();
+ $this->attributeName = $attributes->getName();
+ $this->registerBag($attributes);
+
+ $flashes ??= new FlashBag();
+ $this->flashName = $flashes->getName();
+ $this->registerBag($flashes);
+ }
+
+ public function start(): bool
+ {
+ return $this->storage->start();
+ }
+
+ public function has(string $name): bool
+ {
+ return $this->getAttributeBag()->has($name);
+ }
+
+ public function get(string $name, mixed $default = null): mixed
+ {
+ return $this->getAttributeBag()->get($name, $default);
+ }
+
+ public function set(string $name, mixed $value): void
+ {
+ $this->getAttributeBag()->set($name, $value);
+ }
+
+ public function all(): array
+ {
+ return $this->getAttributeBag()->all();
+ }
+
+ public function replace(array $attributes): void
+ {
+ $this->getAttributeBag()->replace($attributes);
+ }
+
+ public function remove(string $name): mixed
+ {
+ return $this->getAttributeBag()->remove($name);
+ }
+
+ public function clear(): void
+ {
+ $this->getAttributeBag()->clear();
+ }
+
+ public function isStarted(): bool
+ {
+ return $this->storage->isStarted();
+ }
+
+ /**
+ * Returns an iterator for attributes.
+ *
+ * @return \ArrayIterator
+ */
+ public function getIterator(): \ArrayIterator
+ {
+ return new \ArrayIterator($this->getAttributeBag()->all());
+ }
+
+ /**
+ * Returns the number of attributes.
+ */
+ public function count(): int
+ {
+ 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 ($data) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public function invalidate(?int $lifetime = null): bool
+ {
+ $this->storage->clear();
+
+ return $this->migrate(true, $lifetime);
+ }
+
+ public function migrate(bool $destroy = false, ?int $lifetime = null): bool
+ {
+ return $this->storage->regenerate($destroy, $lifetime);
+ }
+
+ public function save(): void
+ {
+ $this->storage->save();
+ }
+
+ public function getId(): string
+ {
+ return $this->storage->getId();
+ }
+
+ public function setId(string $id): void
+ {
+ if ($this->storage->getId() !== $id) {
+ $this->storage->setId($id);
+ }
+ }
+
+ public function getName(): string
+ {
+ return $this->storage->getName();
+ }
+
+ public function setName(string $name): void
+ {
+ $this->storage->setName($name);
+ }
+
+ public function getMetadataBag(): MetadataBag
+ {
+ ++$this->usageIndex;
+ if ($this->usageReporter && 0 <= $this->usageIndex) {
+ ($this->usageReporter)();
+ }
+
+ return $this->storage->getMetadataBag();
+ }
+
+ public function registerBag(SessionBagInterface $bag): void
+ {
+ $this->storage->registerBag(new SessionBagProxy($bag, $this->data, $this->usageIndex, $this->usageReporter));
+ }
+
+ public function getBag(string $name): SessionBagInterface
+ {
+ $bag = $this->storage->getBag($name);
+
+ return method_exists($bag, 'getBag') ? $bag->getBag() : $bag;
+ }
+
+ /**
+ * Gets the flashbag interface.
+ */
+ public function getFlashBag(): FlashBagInterface
+ {
+ 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/vendor/symfony/http-foundation/Session/SessionBagInterface.php b/vendor/symfony/http-foundation/Session/SessionBagInterface.php
new file mode 100644
index 0000000..6a224cf
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/SessionBagInterface.php
@@ -0,0 +1,42 @@
+
+ *
+ * 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
+ */
+interface SessionBagInterface
+{
+ /**
+ * Gets this bag's name.
+ */
+ public function getName(): string;
+
+ /**
+ * Initializes the Bag.
+ */
+ public function initialize(array &$array): void;
+
+ /**
+ * Gets the storage key for this bag.
+ */
+ public function getStorageKey(): string;
+
+ /**
+ * Clears out data from bag.
+ *
+ * @return mixed Whatever data was contained
+ */
+ public function clear(): mixed;
+}
diff --git a/vendor/symfony/http-foundation/Session/SessionBagProxy.php b/vendor/symfony/http-foundation/Session/SessionBagProxy.php
new file mode 100644
index 0000000..a389bd8
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/SessionBagProxy.php
@@ -0,0 +1,86 @@
+
+ *
+ * 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
+ *
+ * @internal
+ */
+final class SessionBagProxy implements SessionBagInterface
+{
+ private array $data;
+ private ?int $usageIndex;
+ private ?\Closure $usageReporter;
+
+ public function __construct(
+ private SessionBagInterface $bag,
+ array &$data,
+ ?int &$usageIndex,
+ ?callable $usageReporter,
+ ) {
+ $this->bag = $bag;
+ $this->data = &$data;
+ $this->usageIndex = &$usageIndex;
+ $this->usageReporter = null === $usageReporter ? null : $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()]);
+ }
+
+ public function getName(): string
+ {
+ return $this->bag->getName();
+ }
+
+ 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);
+ }
+
+ public function getStorageKey(): string
+ {
+ return $this->bag->getStorageKey();
+ }
+
+ public function clear(): mixed
+ {
+ return $this->bag->clear();
+ }
+}
diff --git a/vendor/symfony/http-foundation/Session/SessionFactory.php b/vendor/symfony/http-foundation/Session/SessionFactory.php
new file mode 100644
index 0000000..b875a23
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/SessionFactory.php
@@ -0,0 +1,39 @@
+
+ *
+ * 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é
+ */
+class SessionFactory implements SessionFactoryInterface
+{
+ private ?\Closure $usageReporter;
+
+ public function __construct(
+ private RequestStack $requestStack,
+ private SessionStorageFactoryInterface $storageFactory,
+ ?callable $usageReporter = null,
+ ) {
+ $this->usageReporter = null === $usageReporter ? null : $usageReporter(...);
+ }
+
+ public function createSession(): SessionInterface
+ {
+ return new Session($this->storageFactory->createStorage($this->requestStack->getMainRequest()), null, null, $this->usageReporter);
+ }
+}
diff --git a/vendor/symfony/http-foundation/Session/SessionFactoryInterface.php b/vendor/symfony/http-foundation/Session/SessionFactoryInterface.php
new file mode 100644
index 0000000..b24fdc4
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/SessionFactoryInterface.php
@@ -0,0 +1,20 @@
+
+ *
+ * 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
+ */
+interface SessionFactoryInterface
+{
+ public function createSession(): SessionInterface;
+}
diff --git a/vendor/symfony/http-foundation/Session/SessionInterface.php b/vendor/symfony/http-foundation/Session/SessionInterface.php
new file mode 100644
index 0000000..3e29ba4
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/SessionInterface.php
@@ -0,0 +1,140 @@
+
+ *
+ * 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
+ */
+interface SessionInterface
+{
+ /**
+ * Starts the session storage.
+ *
+ * @throws \RuntimeException if session fails to start
+ */
+ public function start(): bool;
+
+ /**
+ * Returns the session ID.
+ */
+ public function getId(): string;
+
+ /**
+ * Sets the session ID.
+ */
+ public function setId(string $id): void;
+
+ /**
+ * Returns the session name.
+ */
+ public function getName(): string;
+
+ /**
+ * Sets the session name.
+ */
+ public function setName(string $name): void;
+
+ /**
+ * Invalidates the current session.
+ *
+ * Clears all session attributes and flashes and regenerates the
+ * session and deletes the old session from persistence.
+ *
+ * @param int|null $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 invalidate(?int $lifetime = null): bool;
+
+ /**
+ * 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|null $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 migrate(bool $destroy = false, ?int $lifetime = null): bool;
+
+ /**
+ * 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(): void;
+
+ /**
+ * Checks if an attribute is defined.
+ */
+ public function has(string $name): bool;
+
+ /**
+ * Returns an attribute.
+ */
+ public function get(string $name, mixed $default = null): mixed;
+
+ /**
+ * Sets an attribute.
+ */
+ public function set(string $name, mixed $value): void;
+
+ /**
+ * Returns attributes.
+ */
+ public function all(): array;
+
+ /**
+ * Sets attributes.
+ */
+ public function replace(array $attributes): void;
+
+ /**
+ * Removes an attribute.
+ *
+ * @return mixed The removed value or null when it does not exist
+ */
+ public function remove(string $name): mixed;
+
+ /**
+ * Clears all attributes.
+ */
+ public function clear(): void;
+
+ /**
+ * Checks if the session was started.
+ */
+ public function isStarted(): bool;
+
+ /**
+ * Registers a SessionBagInterface with the session.
+ */
+ public function registerBag(SessionBagInterface $bag): void;
+
+ /**
+ * Gets a bag instance by name.
+ */
+ public function getBag(string $name): SessionBagInterface;
+
+ /**
+ * Gets session meta.
+ */
+ public function getMetadataBag(): MetadataBag;
+}
diff --git a/vendor/symfony/http-foundation/Session/SessionUtils.php b/vendor/symfony/http-foundation/Session/SessionUtils.php
new file mode 100644
index 0000000..57aa565
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/SessionUtils.php
@@ -0,0 +1,59 @@
+
+ *
+ * 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
+ * @author Rémon van de Kamp
+ *
+ * @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, #[\SensitiveParameter] 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/vendor/symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php b/vendor/symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php
new file mode 100644
index 0000000..fd85623
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php
@@ -0,0 +1,111 @@
+
+ *
+ * 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
+ */
+abstract class AbstractSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface
+{
+ private string $sessionName;
+ private string $prefetchId;
+ private string $prefetchData;
+ private ?string $newSessionId = null;
+ private string $igbinaryEmptyData;
+
+ public function open(string $savePath, string $sessionName): bool
+ {
+ $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;
+ }
+
+ abstract protected function doRead(#[\SensitiveParameter] string $sessionId): string;
+
+ abstract protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool;
+
+ abstract protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool;
+
+ public function validateId(#[\SensitiveParameter] string $sessionId): bool
+ {
+ $this->prefetchData = $this->read($sessionId);
+ $this->prefetchId = $sessionId;
+
+ return '' !== $this->prefetchData;
+ }
+
+ public function read(#[\SensitiveParameter] string $sessionId): string
+ {
+ if (isset($this->prefetchId)) {
+ $prefetchId = $this->prefetchId;
+ $prefetchData = $this->prefetchData;
+ unset($this->prefetchId, $this->prefetchData);
+
+ if ($prefetchId === $sessionId || '' === $prefetchData) {
+ $this->newSessionId = '' === $prefetchData ? $sessionId : null;
+
+ return $prefetchData;
+ }
+ }
+
+ $data = $this->doRead($sessionId);
+ $this->newSessionId = '' === $data ? $sessionId : null;
+
+ return $data;
+ }
+
+ public function write(#[\SensitiveParameter] string $sessionId, string $data): bool
+ {
+ // 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);
+ }
+
+ public function destroy(#[\SensitiveParameter] string $sessionId): bool
+ {
+ if (!headers_sent() && filter_var(\ini_get('session.use_cookies'), \FILTER_VALIDATE_BOOL)) {
+ if (!isset($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])) {
+ $params = session_get_cookie_params();
+ unset($params['lifetime']);
+ setcookie($this->sessionName, '', $params);
+ }
+ }
+
+ return $this->newSessionId === $sessionId || $this->doDestroy($sessionId);
+ }
+}
diff --git a/vendor/symfony/http-foundation/Session/Storage/Handler/IdentityMarshaller.php b/vendor/symfony/http-foundation/Session/Storage/Handler/IdentityMarshaller.php
new file mode 100644
index 0000000..70ac762
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/Storage/Handler/IdentityMarshaller.php
@@ -0,0 +1,36 @@
+
+ *
+ * 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
+ */
+class IdentityMarshaller implements MarshallerInterface
+{
+ 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;
+ }
+
+ public function unmarshall(string $value): string
+ {
+ return $value;
+ }
+}
diff --git a/vendor/symfony/http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php b/vendor/symfony/http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php
new file mode 100644
index 0000000..8e82f18
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php
@@ -0,0 +1,73 @@
+
+ *
+ * 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
+ */
+class MarshallingSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface
+{
+ public function __construct(
+ private AbstractSessionHandler $handler,
+ private MarshallerInterface $marshaller,
+ ) {
+ }
+
+ public function open(string $savePath, string $name): bool
+ {
+ return $this->handler->open($savePath, $name);
+ }
+
+ public function close(): bool
+ {
+ return $this->handler->close();
+ }
+
+ public function destroy(#[\SensitiveParameter] string $sessionId): bool
+ {
+ return $this->handler->destroy($sessionId);
+ }
+
+ public function gc(int $maxlifetime): int|false
+ {
+ return $this->handler->gc($maxlifetime);
+ }
+
+ public function read(#[\SensitiveParameter] string $sessionId): string
+ {
+ return $this->marshaller->unmarshall($this->handler->read($sessionId));
+ }
+
+ public function write(#[\SensitiveParameter] string $sessionId, string $data): bool
+ {
+ $failed = [];
+ $marshalledData = $this->marshaller->marshall(['data' => $data], $failed);
+
+ if (isset($failed['data'])) {
+ return false;
+ }
+
+ return $this->handler->write($sessionId, $marshalledData['data']);
+ }
+
+ public function validateId(#[\SensitiveParameter] string $sessionId): bool
+ {
+ return $this->handler->validateId($sessionId);
+ }
+
+ public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool
+ {
+ return $this->handler->updateTimestamp($sessionId, $data);
+ }
+}
diff --git a/vendor/symfony/http-foundation/Session/Storage/Handler/MemcachedSessionHandler.php b/vendor/symfony/http-foundation/Session/Storage/Handler/MemcachedSessionHandler.php
new file mode 100644
index 0000000..4b95d88
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/Storage/Handler/MemcachedSessionHandler.php
@@ -0,0 +1,110 @@
+
+ *
+ * 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
+ */
+class MemcachedSessionHandler extends AbstractSessionHandler
+{
+ /**
+ * Time to live in seconds.
+ */
+ private int|\Closure|null $ttl;
+
+ /**
+ * Key prefix for shared environments.
+ */
+ private string $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(
+ private \Memcached $memcached,
+ array $options = [],
+ ) {
+ 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';
+ }
+
+ public function close(): bool
+ {
+ return $this->memcached->quit();
+ }
+
+ protected function doRead(#[\SensitiveParameter] string $sessionId): string
+ {
+ return $this->memcached->get($this->prefix.$sessionId) ?: '';
+ }
+
+ public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool
+ {
+ $this->memcached->touch($this->prefix.$sessionId, $this->getCompatibleTtl());
+
+ return true;
+ }
+
+ protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool
+ {
+ return $this->memcached->set($this->prefix.$sessionId, $data, $this->getCompatibleTtl());
+ }
+
+ private function getCompatibleTtl(): int
+ {
+ $ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime');
+
+ // If the relative TTL that is used exceeds 30 days, memcached will treat the value as Unix time.
+ // We have to convert it to an absolute Unix time at this point, to make sure the TTL is correct.
+ if ($ttl > 60 * 60 * 24 * 30) {
+ $ttl += time();
+ }
+
+ return $ttl;
+ }
+
+ protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool
+ {
+ $result = $this->memcached->delete($this->prefix.$sessionId);
+
+ return $result || \Memcached::RES_NOTFOUND == $this->memcached->getResultCode();
+ }
+
+ public function gc(int $maxlifetime): int|false
+ {
+ // not required here because memcached will auto expire the records anyhow.
+ return 0;
+ }
+
+ /**
+ * Return a Memcached instance.
+ */
+ protected function getMemcached(): \Memcached
+ {
+ return $this->memcached;
+ }
+}
diff --git a/vendor/symfony/http-foundation/Session/Storage/Handler/MigratingSessionHandler.php b/vendor/symfony/http-foundation/Session/Storage/Handler/MigratingSessionHandler.php
new file mode 100644
index 0000000..8ed6a7b
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/Storage/Handler/MigratingSessionHandler.php
@@ -0,0 +1,100 @@
+
+ *
+ * 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
+ * @author Oliver Radwell
+ */
+class MigratingSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface
+{
+ private \SessionHandlerInterface&\SessionUpdateTimestampHandlerInterface $currentHandler;
+ private \SessionHandlerInterface&\SessionUpdateTimestampHandlerInterface $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;
+ }
+
+ public function close(): bool
+ {
+ $result = $this->currentHandler->close();
+ $this->writeOnlyHandler->close();
+
+ return $result;
+ }
+
+ public function destroy(#[\SensitiveParameter] string $sessionId): bool
+ {
+ $result = $this->currentHandler->destroy($sessionId);
+ $this->writeOnlyHandler->destroy($sessionId);
+
+ return $result;
+ }
+
+ public function gc(int $maxlifetime): int|false
+ {
+ $result = $this->currentHandler->gc($maxlifetime);
+ $this->writeOnlyHandler->gc($maxlifetime);
+
+ return $result;
+ }
+
+ public function open(string $savePath, string $sessionName): bool
+ {
+ $result = $this->currentHandler->open($savePath, $sessionName);
+ $this->writeOnlyHandler->open($savePath, $sessionName);
+
+ return $result;
+ }
+
+ public function read(#[\SensitiveParameter] string $sessionId): string
+ {
+ // No reading from new handler until switch-over
+ return $this->currentHandler->read($sessionId);
+ }
+
+ public function write(#[\SensitiveParameter] string $sessionId, string $sessionData): bool
+ {
+ $result = $this->currentHandler->write($sessionId, $sessionData);
+ $this->writeOnlyHandler->write($sessionId, $sessionData);
+
+ return $result;
+ }
+
+ public function validateId(#[\SensitiveParameter] string $sessionId): bool
+ {
+ // No reading from new handler until switch-over
+ return $this->currentHandler->validateId($sessionId);
+ }
+
+ public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $sessionData): bool
+ {
+ $result = $this->currentHandler->updateTimestamp($sessionId, $sessionData);
+ $this->writeOnlyHandler->updateTimestamp($sessionId, $sessionData);
+
+ return $result;
+ }
+}
diff --git a/vendor/symfony/http-foundation/Session/Storage/Handler/MongoDbSessionHandler.php b/vendor/symfony/http-foundation/Session/Storage/Handler/MongoDbSessionHandler.php
new file mode 100644
index 0000000..d558603
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/Storage/Handler/MongoDbSessionHandler.php
@@ -0,0 +1,186 @@
+
+ *
+ * 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\Driver\BulkWrite;
+use MongoDB\Driver\Manager;
+use MongoDB\Driver\Query;
+
+/**
+ * Session handler using the MongoDB driver extension.
+ *
+ * @author Markus Bachmann
+ * @author Jérôme Tamarelle
+ *
+ * @see https://php.net/mongodb
+ */
+class MongoDbSessionHandler extends AbstractSessionHandler
+{
+ private Manager $manager;
+ private string $namespace;
+ private array $options;
+ private int|\Closure|null $ttl;
+
+ /**
+ * 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]
+ * * ttl: The time to live in seconds.
+ *
+ * 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..createIndex(
+ * { "": 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|Manager $mongo, array $options)
+ {
+ if (!isset($options['database']) || !isset($options['collection'])) {
+ throw new \InvalidArgumentException('You must provide the "database" and "collection" option for MongoDBSessionHandler.');
+ }
+
+ if ($mongo instanceof Client) {
+ $mongo = $mongo->getManager();
+ }
+
+ $this->manager = $mongo;
+ $this->namespace = $options['database'].'.'.$options['collection'];
+
+ $this->options = array_merge([
+ 'id_field' => '_id',
+ 'data_field' => 'data',
+ 'time_field' => 'time',
+ 'expiry_field' => 'expires_at',
+ ], $options);
+ $this->ttl = $this->options['ttl'] ?? null;
+ }
+
+ public function close(): bool
+ {
+ return true;
+ }
+
+ protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool
+ {
+ $write = new BulkWrite();
+ $write->delete(
+ [$this->options['id_field'] => $sessionId],
+ ['limit' => 1]
+ );
+
+ $this->manager->executeBulkWrite($this->namespace, $write);
+
+ return true;
+ }
+
+ public function gc(int $maxlifetime): int|false
+ {
+ $write = new BulkWrite();
+ $write->delete(
+ [$this->options['expiry_field'] => ['$lt' => $this->getUTCDateTime()]],
+ );
+ $result = $this->manager->executeBulkWrite($this->namespace, $write);
+
+ return $result->getDeletedCount() ?? false;
+ }
+
+ protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool
+ {
+ $ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime');
+ $expiry = $this->getUTCDateTime($ttl);
+
+ $fields = [
+ $this->options['time_field'] => $this->getUTCDateTime(),
+ $this->options['expiry_field'] => $expiry,
+ $this->options['data_field'] => new Binary($data, Binary::TYPE_GENERIC),
+ ];
+
+ $write = new BulkWrite();
+ $write->update(
+ [$this->options['id_field'] => $sessionId],
+ ['$set' => $fields],
+ ['upsert' => true]
+ );
+
+ $this->manager->executeBulkWrite($this->namespace, $write);
+
+ return true;
+ }
+
+ public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool
+ {
+ $ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime');
+ $expiry = $this->getUTCDateTime($ttl);
+
+ $write = new BulkWrite();
+ $write->update(
+ [$this->options['id_field'] => $sessionId],
+ ['$set' => [
+ $this->options['time_field'] => $this->getUTCDateTime(),
+ $this->options['expiry_field'] => $expiry,
+ ]],
+ ['multi' => false],
+ );
+
+ $this->manager->executeBulkWrite($this->namespace, $write);
+
+ return true;
+ }
+
+ protected function doRead(#[\SensitiveParameter] string $sessionId): string
+ {
+ $cursor = $this->manager->executeQuery($this->namespace, new Query([
+ $this->options['id_field'] => $sessionId,
+ $this->options['expiry_field'] => ['$gte' => $this->getUTCDateTime()],
+ ], [
+ 'projection' => [
+ '_id' => false,
+ $this->options['data_field'] => true,
+ ],
+ 'limit' => 1,
+ ]));
+
+ foreach ($cursor as $document) {
+ return (string) $document->{$this->options['data_field']} ?? '';
+ }
+
+ // Not found
+ return '';
+ }
+
+ private function getUTCDateTime(int $additionalSeconds = 0): UTCDateTime
+ {
+ return new UTCDateTime((time() + $additionalSeconds) * 1000);
+ }
+}
diff --git a/vendor/symfony/http-foundation/Session/Storage/Handler/NativeFileSessionHandler.php b/vendor/symfony/http-foundation/Session/Storage/Handler/NativeFileSessionHandler.php
new file mode 100644
index 0000000..81e97be
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/Storage/Handler/NativeFileSessionHandler.php
@@ -0,0 +1,55 @@
+
+ *
+ * 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
+ */
+class NativeFileSessionHandler extends \SessionHandler
+{
+ /**
+ * @param string|null $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)
+ {
+ $baseDir = $savePath ??= \ini_get('session.save_path');
+
+ 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, 0o777, true) && !is_dir($baseDir)) {
+ throw new \RuntimeException(\sprintf('Session Storage was not able to create directory "%s".', $baseDir));
+ }
+
+ if ($savePath !== \ini_get('session.save_path')) {
+ ini_set('session.save_path', $savePath);
+ }
+ if ('files' !== \ini_get('session.save_handler')) {
+ ini_set('session.save_handler', 'files');
+ }
+ }
+}
diff --git a/vendor/symfony/http-foundation/Session/Storage/Handler/NullSessionHandler.php b/vendor/symfony/http-foundation/Session/Storage/Handler/NullSessionHandler.php
new file mode 100644
index 0000000..a77185e
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/Storage/Handler/NullSessionHandler.php
@@ -0,0 +1,55 @@
+
+ *
+ * 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
+ */
+class NullSessionHandler extends AbstractSessionHandler
+{
+ public function close(): bool
+ {
+ return true;
+ }
+
+ public function validateId(#[\SensitiveParameter] string $sessionId): bool
+ {
+ return true;
+ }
+
+ protected function doRead(#[\SensitiveParameter] string $sessionId): string
+ {
+ return '';
+ }
+
+ public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool
+ {
+ return true;
+ }
+
+ protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool
+ {
+ return true;
+ }
+
+ protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool
+ {
+ return true;
+ }
+
+ public function gc(int $maxlifetime): int|false
+ {
+ return 0;
+ }
+}
diff --git a/vendor/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php b/vendor/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php
new file mode 100644
index 0000000..2fef5cc
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php
@@ -0,0 +1,919 @@
+
+ *
+ * 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\Schema\Name\Identifier;
+use Doctrine\DBAL\Schema\Name\UnqualifiedName;
+use Doctrine\DBAL\Schema\PrimaryKeyConstraint;
+use Doctrine\DBAL\Schema\Schema;
+use Doctrine\DBAL\Types\Types;
+
+/**
+ * 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
+ * @author Michael Williams
+ * @author Tobias Schultze
+ */
+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 \PDO $pdo;
+
+ /**
+ * DSN string or null for session.save_path or false when lazy connection disabled.
+ */
+ private string|false|null $dsn = false;
+
+ private string $driver;
+ private string $table = 'sessions';
+ private string $idCol = 'sess_id';
+ private string $dataCol = 'sess_data';
+ private string $lifetimeCol = 'sess_lifetime';
+ private string $timeCol = 'sess_time';
+
+ /**
+ * Time to live in seconds.
+ */
+ private int|\Closure|null $ttl;
+
+ /**
+ * Username when lazy-connect.
+ */
+ private ?string $username = null;
+
+ /**
+ * Password when lazy-connect.
+ */
+ private ?string $password = null;
+
+ /**
+ * Connection options when lazy-connect.
+ */
+ private array $connectionOptions = [];
+
+ /**
+ * The strategy for locking, see constants.
+ */
+ private int $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 array $unlockStatements = [];
+
+ /**
+ * True when the current session exists but expired according to session.gc_maxlifetime.
+ */
+ private bool $sessionExpired = false;
+
+ /**
+ * Whether a transaction is active.
+ */
+ private bool $inTransaction = false;
+
+ /**
+ * Whether gc() has been called.
+ */
+ private bool $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]
+ * * ttl: The time to live in seconds.
+ *
+ * @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(#[\SensitiveParameter] \PDO|string|null $pdoOrDsn = null, #[\SensitiveParameter] 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;
+ $this->ttl = $options['ttl'] ?? null;
+ }
+
+ /**
+ * Adds the Table to the Schema if it doesn't exist.
+ */
+ public function configureSchema(Schema $schema, ?\Closure $isSameDatabase = null): void
+ {
+ if ($schema->hasTable($this->table) || ($isSameDatabase && !$isSameDatabase($this->getConnection()->exec(...)))) {
+ return;
+ }
+
+ $table = $schema->createTable($this->table);
+ switch ($this->driver) {
+ case 'mysql':
+ $table->addColumn($this->idCol, Types::BINARY)->setLength(128)->setNotnull(true);
+ $table->addColumn($this->dataCol, Types::BLOB)->setNotnull(true);
+ $table->addColumn($this->lifetimeCol, Types::INTEGER)->setUnsigned(true)->setNotnull(true);
+ $table->addColumn($this->timeCol, Types::INTEGER)->setUnsigned(true)->setNotnull(true);
+ $table->addOption('collate', 'utf8mb4_bin');
+ $table->addOption('engine', 'InnoDB');
+ break;
+ case 'sqlite':
+ $table->addColumn($this->idCol, Types::TEXT)->setNotnull(true);
+ $table->addColumn($this->dataCol, Types::BLOB)->setNotnull(true);
+ $table->addColumn($this->lifetimeCol, Types::INTEGER)->setNotnull(true);
+ $table->addColumn($this->timeCol, Types::INTEGER)->setNotnull(true);
+ break;
+ case 'pgsql':
+ $table->addColumn($this->idCol, Types::STRING)->setLength(128)->setNotnull(true);
+ $table->addColumn($this->dataCol, Types::BINARY)->setNotnull(true);
+ $table->addColumn($this->lifetimeCol, Types::INTEGER)->setNotnull(true);
+ $table->addColumn($this->timeCol, Types::INTEGER)->setNotnull(true);
+ break;
+ case 'oci':
+ $table->addColumn($this->idCol, Types::STRING)->setLength(128)->setNotnull(true);
+ $table->addColumn($this->dataCol, Types::BLOB)->setNotnull(true);
+ $table->addColumn($this->lifetimeCol, Types::INTEGER)->setNotnull(true);
+ $table->addColumn($this->timeCol, Types::INTEGER)->setNotnull(true);
+ break;
+ case 'sqlsrv':
+ $table->addColumn($this->idCol, Types::STRING)->setLength(128)->setNotnull(true);
+ $table->addColumn($this->dataCol, Types::BLOB)->setNotnull(true);
+ $table->addColumn($this->lifetimeCol, Types::INTEGER)->setUnsigned(true)->setNotnull(true);
+ $table->addColumn($this->timeCol, Types::INTEGER)->setUnsigned(true)->setNotnull(true);
+ break;
+ default:
+ throw new \DomainException(\sprintf('Creating the session table is currently not implemented for PDO driver "%s".', $this->driver));
+ }
+
+ $table->addPrimaryKeyConstraint(new PrimaryKeyConstraint(null, [new UnqualifiedName(Identifier::unquoted($this->idCol))], true));
+ $table->addIndex([$this->lifetimeCol], $this->lifetimeCol.'_idx');
+ }
+
+ /**
+ * 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(): void
+ {
+ // connect if we are not yet
+ $this->getConnection();
+
+ $sql = match ($this->driver) {
+ // 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
+ 'mysql' => "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",
+ 'sqlite' => "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)",
+ 'pgsql' => "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)",
+ 'oci' => "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)",
+ 'sqlsrv' => "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)",
+ 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 {$this->lifetimeCol}_idx 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.
+ */
+ public function isSessionExpired(): bool
+ {
+ return $this->sessionExpired;
+ }
+
+ public function open(string $savePath, string $sessionName): bool
+ {
+ $this->sessionExpired = false;
+
+ if (!isset($this->pdo)) {
+ $this->connect($this->dsn ?: $savePath);
+ }
+
+ return parent::open($savePath, $sessionName);
+ }
+
+ public function read(#[\SensitiveParameter] string $sessionId): string
+ {
+ try {
+ return parent::read($sessionId);
+ } catch (\PDOException $e) {
+ $this->rollback();
+
+ throw $e;
+ }
+ }
+
+ public function gc(int $maxlifetime): int|false
+ {
+ // 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;
+ }
+
+ protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool
+ {
+ // 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;
+ }
+
+ protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool
+ {
+ $maxlifetime = (int) (($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \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;
+ }
+
+ public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool
+ {
+ $expiry = time() + (int) (($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \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->bindValue(':id', $sessionId, \PDO::PARAM_STR);
+ $updateStmt->bindValue(':expiry', $expiry, \PDO::PARAM_INT);
+ $updateStmt->bindValue(':time', time(), \PDO::PARAM_INT);
+ $updateStmt->execute();
+ } catch (\PDOException $e) {
+ $this->rollback();
+
+ throw $e;
+ }
+
+ return true;
+ }
+
+ public function close(): bool
+ {
+ $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";
+ $stmt = $this->pdo->prepare($sql);
+ $stmt->bindValue(':time', time(), \PDO::PARAM_INT);
+ $stmt->execute();
+ }
+
+ if (false !== $this->dsn) {
+ unset($this->pdo, $this->driver); // only close lazy-connection
+ }
+
+ return true;
+ }
+
+ /**
+ * Lazy-connects to the database.
+ */
+ private function connect(#[\SensitiveParameter] 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(#[\SensitiveParameter] 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 ??= '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.
+ */
+ protected function doRead(#[\SensitiveParameter] string $sessionId): string
+ {
+ 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 < 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_BOOL) && 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(#[\SensitiveParameter] 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();
+
+ switch ($this->driver) {
+ case 'mysql':
+ case 'oci':
+ case 'pgsql':
+ return "SELECT $this->dataCol, $this->lifetimeCol FROM $this->table WHERE $this->idCol = :id FOR UPDATE";
+ case 'sqlsrv':
+ return "SELECT $this->dataCol, $this->lifetimeCol 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 FROM $this->table WHERE $this->idCol = :id";
+ }
+
+ /**
+ * Returns an insert statement supported by the database for writing session data.
+ */
+ private function getInsertStatement(#[\SensitiveParameter] 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;
+ case 'sqlsrv':
+ $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, :data, :expiry, :time)";
+ 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(#[\SensitiveParameter] 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;
+ case 'sqlsrv':
+ $data = fopen('php://memory', 'r+');
+ fwrite($data, $sessionData);
+ rewind($data);
+ $sql = "UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :expiry, $this->timeCol = :time WHERE $this->idCol = :id";
+ 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(#[\SensitiveParameter] 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) {
+ $dataStream = fopen('php://memory', 'r+');
+ fwrite($dataStream, $data);
+ rewind($dataStream);
+
+ $mergeStmt->bindParam(1, $sessionId, \PDO::PARAM_STR);
+ $mergeStmt->bindParam(2, $sessionId, \PDO::PARAM_STR);
+ $mergeStmt->bindParam(3, $dataStream, \PDO::PARAM_LOB);
+ $mergeStmt->bindValue(4, time() + $maxlifetime, \PDO::PARAM_INT);
+ $mergeStmt->bindValue(5, time(), \PDO::PARAM_INT);
+ $mergeStmt->bindParam(6, $dataStream, \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.
+ */
+ protected function getConnection(): \PDO
+ {
+ if (!isset($this->pdo)) {
+ $this->connect($this->dsn ?: \ini_get('session.save_path'));
+ }
+
+ return $this->pdo;
+ }
+}
diff --git a/vendor/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php b/vendor/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php
new file mode 100644
index 0000000..78cd4e7
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php
@@ -0,0 +1,103 @@
+
+ *
+ * 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 Relay\Relay;
+
+/**
+ * Redis based session storage handler based on the Redis class
+ * provided by the PHP redis extension.
+ *
+ * @author Dalibor Karlović
+ */
+class RedisSessionHandler extends AbstractSessionHandler
+{
+ /**
+ * Key prefix for shared environments.
+ */
+ private string $prefix;
+
+ /**
+ * Time to live in seconds.
+ */
+ private int|\Closure|null $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.
+ *
+ * @throws \InvalidArgumentException When unsupported client or options are passed
+ */
+ public function __construct(
+ private \Redis|Relay|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis,
+ array $options = [],
+ ) {
+ if ($diff = array_diff(array_keys($options), ['prefix', 'ttl'])) {
+ throw new \InvalidArgumentException(\sprintf('The following options are not supported "%s".', implode(', ', $diff)));
+ }
+
+ $this->prefix = $options['prefix'] ?? 'sf_s';
+ $this->ttl = $options['ttl'] ?? null;
+ }
+
+ protected function doRead(#[\SensitiveParameter] string $sessionId): string
+ {
+ return $this->redis->get($this->prefix.$sessionId) ?: '';
+ }
+
+ protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool
+ {
+ $ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime');
+ $result = $this->redis->setEx($this->prefix.$sessionId, (int) $ttl, $data);
+
+ return $result && !$result instanceof ErrorInterface;
+ }
+
+ protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool
+ {
+ static $unlink = true;
+
+ if ($unlink) {
+ try {
+ $unlink = false !== $this->redis->unlink($this->prefix.$sessionId);
+ } catch (\Throwable) {
+ $unlink = false;
+ }
+ }
+
+ if (!$unlink) {
+ $this->redis->del($this->prefix.$sessionId);
+ }
+
+ return true;
+ }
+
+ #[\ReturnTypeWillChange]
+ public function close(): bool
+ {
+ return true;
+ }
+
+ public function gc(int $maxlifetime): int|false
+ {
+ return 0;
+ }
+
+ public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool
+ {
+ $ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime');
+
+ return $this->redis->expire($this->prefix.$sessionId, (int) $ttl);
+ }
+}
diff --git a/vendor/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php b/vendor/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php
new file mode 100644
index 0000000..cb0b6f8
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php
@@ -0,0 +1,97 @@
+
+ *
+ * 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\Configuration;
+use Doctrine\DBAL\DriverManager;
+use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory;
+use Doctrine\DBAL\Tools\DsnParser;
+use Relay\Relay;
+use Symfony\Component\Cache\Adapter\AbstractAdapter;
+
+/**
+ * @author Nicolas Grekas
+ */
+class SessionHandlerFactory
+{
+ public static function createHandler(object|string $connection, array $options = []): AbstractSessionHandler
+ {
+ if ($query = \is_string($connection) ? parse_url($connection) : false) {
+ parse_str($query['query'] ?? '', $query);
+
+ if (($options['ttl'] ?? null) instanceof \Closure) {
+ $query['ttl'] = $options['ttl'];
+ }
+ }
+ $options = ($query ?: []) + $options;
+
+ switch (true) {
+ case $connection instanceof \Redis:
+ case $connection instanceof Relay:
+ case $connection instanceof \RedisArray:
+ case $connection instanceof \RedisCluster:
+ case $connection instanceof \Predis\ClientInterface:
+ 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, 'valkey:'):
+ case str_starts_with($connection, 'valkeys:'):
+ case str_starts_with($connection, 'memcached:'):
+ if (!class_exists(AbstractAdapter::class)) {
+ throw new \InvalidArgumentException('Unsupported Redis or Memcached DSN. Try running "composer require symfony/cache".');
+ }
+ $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('Unsupported PDO OCI DSN. Try running "composer require doctrine/dbal".');
+ }
+ $connection[3] = '-';
+ $params = (new DsnParser())->parse($connection);
+ $config = new Configuration();
+ $config->setSchemaManagerFactory(new DefaultSchemaManagerFactory());
+
+ $connection = DriverManager::getConnection($params, $config)->getNativeConnection();
+ // 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/vendor/symfony/http-foundation/Session/Storage/Handler/StrictSessionHandler.php b/vendor/symfony/http-foundation/Session/Storage/Handler/StrictSessionHandler.php
new file mode 100644
index 0000000..0d84eac
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/Storage/Handler/StrictSessionHandler.php
@@ -0,0 +1,87 @@
+
+ *
+ * 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
+ */
+class StrictSessionHandler extends AbstractSessionHandler
+{
+ private bool $doDestroy;
+
+ public function __construct(
+ private \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));
+ }
+ }
+
+ /**
+ * Returns true if this handler wraps an internal PHP session save handler using \SessionHandler.
+ *
+ * @internal
+ */
+ public function isWrapper(): bool
+ {
+ return $this->handler instanceof \SessionHandler;
+ }
+
+ public function open(string $savePath, string $sessionName): bool
+ {
+ parent::open($savePath, $sessionName);
+
+ return $this->handler->open($savePath, $sessionName);
+ }
+
+ protected function doRead(#[\SensitiveParameter] string $sessionId): string
+ {
+ return $this->handler->read($sessionId);
+ }
+
+ public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool
+ {
+ return $this->write($sessionId, $data);
+ }
+
+ protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool
+ {
+ return $this->handler->write($sessionId, $data);
+ }
+
+ public function destroy(#[\SensitiveParameter] string $sessionId): bool
+ {
+ $this->doDestroy = true;
+ $destroyed = parent::destroy($sessionId);
+
+ return $this->doDestroy ? $this->doDestroy($sessionId) : $destroyed;
+ }
+
+ protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool
+ {
+ $this->doDestroy = false;
+
+ return $this->handler->destroy($sessionId);
+ }
+
+ public function close(): bool
+ {
+ return $this->handler->close();
+ }
+
+ public function gc(int $maxlifetime): int|false
+ {
+ return $this->handler->gc($maxlifetime);
+ }
+}
diff --git a/vendor/symfony/http-foundation/Session/Storage/MetadataBag.php b/vendor/symfony/http-foundation/Session/Storage/MetadataBag.php
new file mode 100644
index 0000000..c9e0bdd
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/Storage/MetadataBag.php
@@ -0,0 +1,131 @@
+
+ *
+ * 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
+ */
+class MetadataBag implements SessionBagInterface
+{
+ public const CREATED = 'c';
+ public const UPDATED = 'u';
+ public const LIFETIME = 'l';
+
+ protected array $meta = [self::CREATED => 0, self::UPDATED => 0, self::LIFETIME => 0];
+
+ private string $name = '__metadata';
+ private int $lastUsed;
+
+ /**
+ * @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(
+ private string $storageKey = '_sf2_meta',
+ private int $updateThreshold = 0,
+ ) {
+ }
+
+ public function initialize(array &$array): void
+ {
+ $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.
+ */
+ public function getLifetime(): int
+ {
+ return $this->meta[self::LIFETIME];
+ }
+
+ /**
+ * Stamps a new session's metadata.
+ *
+ * @param int|null $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): void
+ {
+ $this->stampCreated($lifetime);
+ }
+
+ public function getStorageKey(): string
+ {
+ return $this->storageKey;
+ }
+
+ /**
+ * Gets the created timestamp metadata.
+ *
+ * @return int Unix timestamp
+ */
+ public function getCreated(): int
+ {
+ return $this->meta[self::CREATED];
+ }
+
+ /**
+ * Gets the last used metadata.
+ *
+ * @return int Unix timestamp
+ */
+ public function getLastUsed(): int
+ {
+ return $this->lastUsed;
+ }
+
+ public function clear(): mixed
+ {
+ // nothing to do
+ return null;
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ /**
+ * Sets name.
+ */
+ public function setName(string $name): void
+ {
+ $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/vendor/symfony/http-foundation/Session/Storage/MockArraySessionStorage.php b/vendor/symfony/http-foundation/Session/Storage/MockArraySessionStorage.php
new file mode 100644
index 0000000..a32a43d
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/Storage/MockArraySessionStorage.php
@@ -0,0 +1,188 @@
+
+ *
+ * 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
+ * @author Bulat Shakirzyanov
+ * @author Drak
+ */
+class MockArraySessionStorage implements SessionStorageInterface
+{
+ protected string $id = '';
+ protected bool $started = false;
+ protected bool $closed = false;
+ protected array $data = [];
+ protected MetadataBag $metadataBag;
+
+ /**
+ * @var SessionBagInterface[]
+ */
+ protected array $bags = [];
+
+ public function __construct(
+ protected string $name = 'MOCKSESSID',
+ ?MetadataBag $metaBag = null,
+ ) {
+ $this->setMetadataBag($metaBag);
+ }
+
+ public function setSessionData(array $array): void
+ {
+ $this->data = $array;
+ }
+
+ public function start(): bool
+ {
+ if ($this->started) {
+ return true;
+ }
+
+ if (!$this->id) {
+ $this->id = $this->generateId();
+ }
+
+ $this->loadSession();
+
+ return true;
+ }
+
+ public function regenerate(bool $destroy = false, ?int $lifetime = null): bool
+ {
+ if (!$this->started) {
+ $this->start();
+ }
+
+ $this->metadataBag->stampNew($lifetime);
+ $this->id = $this->generateId();
+
+ return true;
+ }
+
+ public function getId(): string
+ {
+ return $this->id;
+ }
+
+ public function setId(string $id): void
+ {
+ if ($this->started) {
+ throw new \LogicException('Cannot set session ID after the session has started.');
+ }
+
+ $this->id = $id;
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ public function setName(string $name): void
+ {
+ $this->name = $name;
+ }
+
+ public function save(): void
+ {
+ 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;
+ }
+
+ public function clear(): void
+ {
+ // 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();
+ }
+
+ public function registerBag(SessionBagInterface $bag): void
+ {
+ $this->bags[$bag->getName()] = $bag;
+ }
+
+ public function getBag(string $name): SessionBagInterface
+ {
+ 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];
+ }
+
+ public function isStarted(): bool
+ {
+ return $this->started;
+ }
+
+ public function setMetadataBag(?MetadataBag $bag): void
+ {
+ $this->metadataBag = $bag ?? new MetadataBag();
+ }
+
+ /**
+ * Gets the MetadataBag.
+ */
+ public function getMetadataBag(): MetadataBag
+ {
+ return $this->metadataBag;
+ }
+
+ /**
+ * Generates a session ID.
+ *
+ * This doesn't need to be particularly cryptographically secure since this is just
+ * a mock.
+ */
+ protected function generateId(): string
+ {
+ return bin2hex(random_bytes(16));
+ }
+
+ protected function loadSession(): void
+ {
+ $bags = array_merge($this->bags, [$this->metadataBag]);
+
+ foreach ($bags as $bag) {
+ $key = $bag->getStorageKey();
+ $this->data[$key] ??= [];
+ $bag->initialize($this->data[$key]);
+ }
+
+ $this->started = true;
+ $this->closed = false;
+ }
+}
diff --git a/vendor/symfony/http-foundation/Session/Storage/MockFileSessionStorage.php b/vendor/symfony/http-foundation/Session/Storage/MockFileSessionStorage.php
new file mode 100644
index 0000000..d7a9793
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/Storage/MockFileSessionStorage.php
@@ -0,0 +1,149 @@
+
+ *
+ * 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
+ */
+class MockFileSessionStorage extends MockArraySessionStorage
+{
+ private string $savePath;
+
+ /**
+ * @param string|null $savePath Path of directory to save session files
+ */
+ public function __construct(?string $savePath = null, string $name = 'MOCKSESSID', ?MetadataBag $metaBag = null)
+ {
+ $savePath ??= sys_get_temp_dir();
+
+ if (!is_dir($savePath) && !@mkdir($savePath, 0o777, 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);
+ }
+
+ public function start(): bool
+ {
+ if ($this->started) {
+ return true;
+ }
+
+ if (!$this->id) {
+ $this->id = $this->generateId();
+ }
+
+ $this->read();
+
+ $this->started = true;
+
+ return true;
+ }
+
+ public function regenerate(bool $destroy = false, ?int $lifetime = null): bool
+ {
+ if (!$this->started) {
+ $this->start();
+ }
+
+ if ($destroy) {
+ $this->destroy();
+ }
+
+ return parent::regenerate($destroy, $lifetime);
+ }
+
+ public function save(): void
+ {
+ 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 reused 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/vendor/symfony/http-foundation/Session/Storage/MockFileSessionStorageFactory.php b/vendor/symfony/http-foundation/Session/Storage/MockFileSessionStorageFactory.php
new file mode 100644
index 0000000..77ee7b6
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/Storage/MockFileSessionStorageFactory.php
@@ -0,0 +1,38 @@
+
+ *
+ * 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é
+ */
+class MockFileSessionStorageFactory implements SessionStorageFactoryInterface
+{
+ /**
+ * @see MockFileSessionStorage constructor.
+ */
+ public function __construct(
+ private ?string $savePath = null,
+ private string $name = 'MOCKSESSID',
+ private ?MetadataBag $metaBag = null,
+ ) {
+ }
+
+ public function createStorage(?Request $request): SessionStorageInterface
+ {
+ return new MockFileSessionStorage($this->savePath, $this->name, $this->metaBag);
+ }
+}
diff --git a/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorage.php b/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorage.php
new file mode 100644
index 0000000..4077160
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorage.php
@@ -0,0 +1,389 @@
+
+ *
+ * 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\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
+ */
+class NativeSessionStorage implements SessionStorageInterface
+{
+ /**
+ * @var SessionBagInterface[]
+ */
+ protected array $bags = [];
+ protected bool $started = false;
+ protected bool $closed = false;
+ protected AbstractProxy|\SessionHandlerInterface $saveHandler;
+ protected MetadataBag $metadataBag;
+
+ /**
+ * 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"
+ * serialize_handler, "php"
+ * use_strict_mode, "1"
+ * use_cookies, "1"
+ */
+ public function __construct(array $options = [], AbstractProxy|\SessionHandlerInterface|null $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.
+ */
+ public function getSaveHandler(): AbstractProxy|\SessionHandlerInterface
+ {
+ return $this->saveHandler;
+ }
+
+ public function start(): bool
+ {
+ 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_BOOL) && 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;
+ /*
+ * Explanation of the session ID regular expression: `/^[a-zA-Z0-9,-]{22,250}$/`.
+ *
+ * ---------- Part 1
+ *
+ * The part `[a-zA-Z0-9,-]` corresponds to the character range when PHP's `session.sid_bits_per_character` is set to 6.
+ * See https://php.net/session.configuration#ini.session.sid-bits-per-character
+ *
+ * ---------- Part 2
+ *
+ * The part `{22,250}` defines the acceptable length range for session IDs.
+ * See https://php.net/session.configuration#ini.session.sid-length
+ * Allowed values are integers between 22 and 256, but we use 250 for the max.
+ *
+ * Where does the 250 come from?
+ * - The length of Windows and Linux filenames is limited to 255 bytes. Then the max must not exceed 255.
+ * - The session filename prefix is `sess_`, a 5 bytes string. Then the max must not exceed 255 - 5 = 250.
+ *
+ * ---------- Conclusion
+ *
+ * The parts 1 and 2 prevent the warning below:
+ * `PHP Warning: SessionHandler::read(): Session ID is too long or contains illegal characters. Only the A-Z, a-z, 0-9, "-", and "," characters are allowed.`
+ *
+ * The part 2 prevents the warning below:
+ * `PHP Warning: SessionHandler::read(): open(filepath, O_RDWR) failed: No such file or directory (2).`
+ */
+ if ($sessionId && $this->saveHandler instanceof AbstractProxy && 'files' === $this->saveHandler->getSaveHandlerName() && !preg_match('/^[a-zA-Z0-9,-]{22,250}$/', $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.');
+ }
+
+ $this->loadSession();
+
+ return true;
+ }
+
+ public function getId(): string
+ {
+ return $this->saveHandler->getId();
+ }
+
+ public function setId(string $id): void
+ {
+ $this->saveHandler->setId($id);
+ }
+
+ public function getName(): string
+ {
+ return $this->saveHandler->getName();
+ }
+
+ public function setName(string $name): void
+ {
+ $this->saveHandler->setName($name);
+ }
+
+ public function regenerate(bool $destroy = false, ?int $lifetime = null): bool
+ {
+ // 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();
+ }
+
+ return session_regenerate_id($destroy);
+ }
+
+ public function save(): void
+ {
+ // 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 ($_SESSION && [$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', $handler::class);
+ }
+
+ 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;
+ }
+
+ public function clear(): void
+ {
+ // clear out the bags
+ foreach ($this->bags as $bag) {
+ $bag->clear();
+ }
+
+ // clear out the session
+ $_SESSION = [];
+
+ // reconnect the bags to the session
+ $this->loadSession();
+ }
+
+ public function registerBag(SessionBagInterface $bag): void
+ {
+ if ($this->started) {
+ throw new \LogicException('Cannot register a bag when the session is already started.');
+ }
+
+ $this->bags[$bag->getName()] = $bag;
+ }
+
+ public function getBag(string $name): SessionBagInterface
+ {
+ 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): void
+ {
+ $this->metadataBag = $metaBag ?? new MetadataBag();
+ }
+
+ /**
+ * Gets the MetadataBag.
+ */
+ public function getMetadataBag(): MetadataBag
+ {
+ return $this->metadataBag;
+ }
+
+ public function isStarted(): bool
+ {
+ 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): void
+ {
+ 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',
+ 'serialize_handler', 'use_strict_mode', 'use_cookies',
+ ]);
+
+ foreach ($options as $key => $value) {
+ if (isset($validOptions[$key])) {
+ if ('cookie_secure' === $key && 'auto' === $value) {
+ continue;
+ }
+ ini_set('session.'.$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
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function setSaveHandler(AbstractProxy|\SessionHandlerInterface|null $saveHandler): void
+ {
+ // 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): void
+ {
+ 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/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorageFactory.php b/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorageFactory.php
new file mode 100644
index 0000000..cb8c535
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorageFactory.php
@@ -0,0 +1,45 @@
+
+ *
+ * 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;
+use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy;
+
+// Help opcache.preload discover always-needed symbols
+class_exists(NativeSessionStorage::class);
+
+/**
+ * @author Jérémy Derussé
+ */
+class NativeSessionStorageFactory implements SessionStorageFactoryInterface
+{
+ /**
+ * @see NativeSessionStorage constructor.
+ */
+ public function __construct(
+ private array $options = [],
+ private AbstractProxy|\SessionHandlerInterface|null $handler = null,
+ private ?MetadataBag $metaBag = null,
+ private bool $secure = false,
+ ) {
+ }
+
+ public function createStorage(?Request $request): SessionStorageInterface
+ {
+ $storage = new NativeSessionStorage($this->options, $this->handler, $this->metaBag);
+ if ($this->secure && $request?->isSecure()) {
+ $storage->setOptions(['cookie_secure' => true]);
+ }
+
+ return $storage;
+ }
+}
diff --git a/vendor/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorage.php b/vendor/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorage.php
new file mode 100644
index 0000000..8a8c50c
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorage.php
@@ -0,0 +1,55 @@
+
+ *
+ * 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
+ */
+class PhpBridgeSessionStorage extends NativeSessionStorage
+{
+ public function __construct(AbstractProxy|\SessionHandlerInterface|null $handler = null, ?MetadataBag $metaBag = null)
+ {
+ if (!\extension_loaded('session')) {
+ throw new \LogicException('PHP extension "session" is required.');
+ }
+
+ $this->setMetadataBag($metaBag);
+ $this->setSaveHandler($handler);
+ }
+
+ public function start(): bool
+ {
+ if ($this->started) {
+ return true;
+ }
+
+ $this->loadSession();
+
+ return true;
+ }
+
+ public function clear(): void
+ {
+ // 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/vendor/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorageFactory.php b/vendor/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorageFactory.php
new file mode 100644
index 0000000..357e5c7
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorageFactory.php
@@ -0,0 +1,41 @@
+
+ *
+ * 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;
+use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy;
+
+// Help opcache.preload discover always-needed symbols
+class_exists(PhpBridgeSessionStorage::class);
+
+/**
+ * @author Jérémy Derussé
+ */
+class PhpBridgeSessionStorageFactory implements SessionStorageFactoryInterface
+{
+ public function __construct(
+ private AbstractProxy|\SessionHandlerInterface|null $handler = null,
+ private ?MetadataBag $metaBag = null,
+ private bool $secure = false,
+ ) {
+ }
+
+ public function createStorage(?Request $request): SessionStorageInterface
+ {
+ $storage = new PhpBridgeSessionStorage($this->handler, $this->metaBag);
+ if ($this->secure && $request?->isSecure()) {
+ $storage->setOptions(['cookie_secure' => true]);
+ }
+
+ return $storage;
+ }
+}
diff --git a/vendor/symfony/http-foundation/Session/Storage/Proxy/AbstractProxy.php b/vendor/symfony/http-foundation/Session/Storage/Proxy/AbstractProxy.php
new file mode 100644
index 0000000..c3a0278
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/Storage/Proxy/AbstractProxy.php
@@ -0,0 +1,98 @@
+
+ *
+ * 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
+ */
+abstract class AbstractProxy
+{
+ protected bool $wrapper = false;
+
+ protected ?string $saveHandlerName = null;
+
+ /**
+ * Gets the session.save_handler name.
+ */
+ public function getSaveHandlerName(): ?string
+ {
+ return $this->saveHandlerName;
+ }
+
+ /**
+ * Is this proxy handler and instance of \SessionHandlerInterface.
+ */
+ public function isSessionHandlerInterface(): bool
+ {
+ return $this instanceof \SessionHandlerInterface;
+ }
+
+ /**
+ * Returns true if this handler wraps an internal PHP session save handler using \SessionHandler.
+ */
+ public function isWrapper(): bool
+ {
+ return $this->wrapper;
+ }
+
+ /**
+ * Has a session started?
+ */
+ public function isActive(): bool
+ {
+ return \PHP_SESSION_ACTIVE === session_status();
+ }
+
+ /**
+ * Gets the session ID.
+ */
+ public function getId(): string
+ {
+ return session_id();
+ }
+
+ /**
+ * Sets the session ID.
+ *
+ * @throws \LogicException
+ */
+ public function setId(string $id): void
+ {
+ if ($this->isActive()) {
+ throw new \LogicException('Cannot change the ID of an active session.');
+ }
+
+ session_id($id);
+ }
+
+ /**
+ * Gets the session name.
+ */
+ public function getName(): string
+ {
+ return session_name();
+ }
+
+ /**
+ * Sets the session name.
+ *
+ * @throws \LogicException
+ */
+ public function setName(string $name): void
+ {
+ if ($this->isActive()) {
+ throw new \LogicException('Cannot change the name of an active session.');
+ }
+
+ session_name($name);
+ }
+}
diff --git a/vendor/symfony/http-foundation/Session/Storage/Proxy/SessionHandlerProxy.php b/vendor/symfony/http-foundation/Session/Storage/Proxy/SessionHandlerProxy.php
new file mode 100644
index 0000000..0316362
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/Storage/Proxy/SessionHandlerProxy.php
@@ -0,0 +1,74 @@
+
+ *
+ * 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;
+
+use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler;
+
+/**
+ * @author Drak
+ */
+class SessionHandlerProxy extends AbstractProxy implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface
+{
+ public function __construct(
+ protected \SessionHandlerInterface $handler,
+ ) {
+ $this->wrapper = $handler instanceof \SessionHandler;
+ $this->saveHandlerName = $this->wrapper || ($handler instanceof StrictSessionHandler && $handler->isWrapper()) ? \ini_get('session.save_handler') : 'user';
+ }
+
+ public function getHandler(): \SessionHandlerInterface
+ {
+ return $this->handler;
+ }
+
+ // \SessionHandlerInterface
+
+ public function open(string $savePath, string $sessionName): bool
+ {
+ return $this->handler->open($savePath, $sessionName);
+ }
+
+ public function close(): bool
+ {
+ return $this->handler->close();
+ }
+
+ public function read(#[\SensitiveParameter] string $sessionId): string|false
+ {
+ return $this->handler->read($sessionId);
+ }
+
+ public function write(#[\SensitiveParameter] string $sessionId, string $data): bool
+ {
+ return $this->handler->write($sessionId, $data);
+ }
+
+ public function destroy(#[\SensitiveParameter] string $sessionId): bool
+ {
+ return $this->handler->destroy($sessionId);
+ }
+
+ public function gc(int $maxlifetime): int|false
+ {
+ return $this->handler->gc($maxlifetime);
+ }
+
+ public function validateId(#[\SensitiveParameter] string $sessionId): bool
+ {
+ return !$this->handler instanceof \SessionUpdateTimestampHandlerInterface || $this->handler->validateId($sessionId);
+ }
+
+ public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool
+ {
+ return $this->handler instanceof \SessionUpdateTimestampHandlerInterface ? $this->handler->updateTimestamp($sessionId, $data) : $this->write($sessionId, $data);
+ }
+}
diff --git a/vendor/symfony/http-foundation/Session/Storage/SessionStorageFactoryInterface.php b/vendor/symfony/http-foundation/Session/Storage/SessionStorageFactoryInterface.php
new file mode 100644
index 0000000..d03f0da
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/Storage/SessionStorageFactoryInterface.php
@@ -0,0 +1,25 @@
+
+ *
+ * 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é
+ */
+interface SessionStorageFactoryInterface
+{
+ /**
+ * Creates a new instance of SessionStorageInterface.
+ */
+ public function createStorage(?Request $request): SessionStorageInterface;
+}
diff --git a/vendor/symfony/http-foundation/Session/Storage/SessionStorageInterface.php b/vendor/symfony/http-foundation/Session/Storage/SessionStorageInterface.php
new file mode 100644
index 0000000..c51850d
--- /dev/null
+++ b/vendor/symfony/http-foundation/Session/Storage/SessionStorageInterface.php
@@ -0,0 +1,116 @@
+
+ *
+ * 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
+ * @author Drak
+ */
+interface SessionStorageInterface
+{
+ /**
+ * Starts the session.
+ *
+ * @throws \RuntimeException if something goes wrong starting the session
+ */
+ public function start(): bool;
+
+ /**
+ * Checks if the session is started.
+ */
+ public function isStarted(): bool;
+
+ /**
+ * Returns the session ID.
+ */
+ public function getId(): string;
+
+ /**
+ * Sets the session ID.
+ */
+ public function setId(string $id): void;
+
+ /**
+ * Returns the session name.
+ */
+ public function getName(): string;
+
+ /**
+ * Sets the session name.
+ */
+ public function setName(string $name): void;
+
+ /**
+ * 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|null $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.
+ *
+ * @throws \RuntimeException If an error occurs while regenerating this storage
+ */
+ public function regenerate(bool $destroy = false, ?int $lifetime = null): bool;
+
+ /**
+ * 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(): void;
+
+ /**
+ * Clear all session data in memory.
+ */
+ public function clear(): void;
+
+ /**
+ * Gets a SessionBagInterface by name.
+ *
+ * @throws \InvalidArgumentException If the bag does not exist
+ */
+ public function getBag(string $name): SessionBagInterface;
+
+ /**
+ * Registers a SessionBagInterface for use.
+ */
+ public function registerBag(SessionBagInterface $bag): void;
+
+ public function getMetadataBag(): MetadataBag;
+}
diff --git a/vendor/symfony/http-foundation/StreamedJsonResponse.php b/vendor/symfony/http-foundation/StreamedJsonResponse.php
new file mode 100644
index 0000000..5b20ce9
--- /dev/null
+++ b/vendor/symfony/http-foundation/StreamedJsonResponse.php
@@ -0,0 +1,162 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation;
+
+/**
+ * StreamedJsonResponse represents a streamed HTTP response for JSON.
+ *
+ * A StreamedJsonResponse uses a structure and generics to create an
+ * efficient resource-saving JSON response.
+ *
+ * It is recommended to use flush() function after a specific number of items to directly stream the data.
+ *
+ * @see flush()
+ *
+ * @author Alexander Schranz
+ *
+ * Example usage:
+ *
+ * function loadArticles(): \Generator
+ * // some streamed loading
+ * yield ['title' => 'Article 1'];
+ * yield ['title' => 'Article 2'];
+ * yield ['title' => 'Article 3'];
+ * // recommended to use flush() after every specific number of items
+ * }),
+ *
+ * $response = new StreamedJsonResponse(
+ * // json structure with generators in which will be streamed
+ * [
+ * '_embedded' => [
+ * 'articles' => loadArticles(), // any generator which you want to stream as list of data
+ * ],
+ * ],
+ * );
+ */
+class StreamedJsonResponse extends StreamedResponse
+{
+ private const PLACEHOLDER = '__symfony_json__';
+
+ /**
+ * @param mixed[] $data JSON Data containing PHP generators which will be streamed as list of data or a Generator
+ * @param int $status The HTTP status code (200 "OK" by default)
+ * @param array $headers An array of HTTP headers
+ * @param int $encodingOptions Flags for the json_encode() function
+ */
+ public function __construct(
+ private readonly iterable $data,
+ int $status = 200,
+ array $headers = [],
+ private int $encodingOptions = JsonResponse::DEFAULT_ENCODING_OPTIONS,
+ ) {
+ parent::__construct($this->stream(...), $status, $headers);
+
+ if (!$this->headers->get('Content-Type')) {
+ $this->headers->set('Content-Type', 'application/json');
+ }
+ }
+
+ private function stream(): void
+ {
+ $jsonEncodingOptions = \JSON_THROW_ON_ERROR | $this->encodingOptions;
+ $keyEncodingOptions = $jsonEncodingOptions & ~\JSON_NUMERIC_CHECK;
+
+ $this->streamData($this->data, $jsonEncodingOptions, $keyEncodingOptions);
+ }
+
+ private function streamData(mixed $data, int $jsonEncodingOptions, int $keyEncodingOptions): void
+ {
+ if (\is_array($data)) {
+ $this->streamArray($data, $jsonEncodingOptions, $keyEncodingOptions);
+
+ return;
+ }
+
+ if (is_iterable($data) && !$data instanceof \JsonSerializable) {
+ $this->streamIterable($data, $jsonEncodingOptions, $keyEncodingOptions);
+
+ return;
+ }
+
+ echo json_encode($data, $jsonEncodingOptions);
+ }
+
+ private function streamArray(array $data, int $jsonEncodingOptions, int $keyEncodingOptions): void
+ {
+ $generators = [];
+
+ array_walk_recursive($data, function (&$item, $key) use (&$generators) {
+ if (self::PLACEHOLDER === $key) {
+ // if the placeholder is already in the structure it should be replaced with a new one that explode
+ // works like expected for the structure
+ $generators[] = $key;
+ }
+
+ // generators should be used but for better DX all kind of Traversable and objects are supported
+ if (\is_object($item)) {
+ $generators[] = $item;
+ $item = self::PLACEHOLDER;
+ } elseif (self::PLACEHOLDER === $item) {
+ // if the placeholder is already in the structure it should be replaced with a new one that explode
+ // works like expected for the structure
+ $generators[] = $item;
+ }
+ });
+
+ $jsonParts = explode('"'.self::PLACEHOLDER.'"', json_encode($data, $jsonEncodingOptions));
+
+ foreach ($generators as $index => $generator) {
+ // send first and between parts of the structure
+ echo $jsonParts[$index];
+
+ $this->streamData($generator, $jsonEncodingOptions, $keyEncodingOptions);
+ }
+
+ // send last part of the structure
+ echo $jsonParts[array_key_last($jsonParts)];
+ }
+
+ private function streamIterable(iterable $iterable, int $jsonEncodingOptions, int $keyEncodingOptions): void
+ {
+ $isFirstItem = true;
+ $startTag = '[';
+
+ foreach ($iterable as $key => $item) {
+ if ($isFirstItem) {
+ $isFirstItem = false;
+ // depending on the first elements key the generator is detected as a list or map
+ // we can not check for a whole list or map because that would hurt the performance
+ // of the streamed response which is the main goal of this response class
+ if (0 !== $key) {
+ $startTag = '{';
+ }
+
+ echo $startTag;
+ } else {
+ // if not first element of the generic, a separator is required between the elements
+ echo ',';
+ }
+
+ if ('{' === $startTag) {
+ echo json_encode((string) $key, $keyEncodingOptions).':';
+ }
+
+ $this->streamData($item, $jsonEncodingOptions, $keyEncodingOptions);
+ }
+
+ if ($isFirstItem) { // indicates that the generator was empty
+ echo '[';
+ }
+
+ echo '[' === $startTag ? ']' : '}';
+ }
+}
diff --git a/vendor/symfony/http-foundation/StreamedResponse.php b/vendor/symfony/http-foundation/StreamedResponse.php
new file mode 100644
index 0000000..4e755a7
--- /dev/null
+++ b/vendor/symfony/http-foundation/StreamedResponse.php
@@ -0,0 +1,150 @@
+
+ *
+ * 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 or an iterable of strings 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
+ */
+class StreamedResponse extends Response
+{
+ protected ?\Closure $callback = null;
+ protected bool $streamed = false;
+
+ private bool $headersSent = false;
+
+ /**
+ * @param callable|iterable|null $callbackOrChunks
+ * @param int $status The HTTP status code (200 "OK" by default)
+ */
+ public function __construct(callable|iterable|null $callbackOrChunks = null, int $status = 200, array $headers = [])
+ {
+ parent::__construct(null, $status, $headers);
+
+ if (\is_callable($callbackOrChunks)) {
+ $this->setCallback($callbackOrChunks);
+ } elseif ($callbackOrChunks) {
+ $this->setChunks($callbackOrChunks);
+ }
+ $this->streamed = false;
+ $this->headersSent = false;
+ }
+
+ /**
+ * @param iterable $chunks
+ */
+ public function setChunks(iterable $chunks): static
+ {
+ $this->callback = static function () use ($chunks): void {
+ foreach ($chunks as $chunk) {
+ echo $chunk;
+ @ob_flush();
+ flush();
+ }
+ };
+
+ return $this;
+ }
+
+ /**
+ * Sets the PHP callback associated with this Response.
+ *
+ * @return $this
+ */
+ public function setCallback(callable $callback): static
+ {
+ $this->callback = $callback(...);
+
+ return $this;
+ }
+
+ public function getCallback(): ?\Closure
+ {
+ if (!isset($this->callback)) {
+ return null;
+ }
+
+ return ($this->callback)(...);
+ }
+
+ /**
+ * This method only sends the headers once.
+ *
+ * @param positive-int|null $statusCode The status code to use, override the statusCode property if set and not null
+ *
+ * @return $this
+ */
+ public function sendHeaders(?int $statusCode = null): static
+ {
+ if ($this->headersSent) {
+ return $this;
+ }
+
+ if ($statusCode < 100 || $statusCode >= 200) {
+ $this->headersSent = true;
+ }
+
+ return parent::sendHeaders($statusCode);
+ }
+
+ /**
+ * This method only sends the content once.
+ *
+ * @return $this
+ */
+ public function sendContent(): static
+ {
+ if ($this->streamed) {
+ return $this;
+ }
+
+ $this->streamed = true;
+
+ if (!isset($this->callback)) {
+ throw new \LogicException('The Response callback must be set.');
+ }
+
+ ($this->callback)();
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ *
+ * @throws \LogicException when the content is not null
+ */
+ public function setContent(?string $content): static
+ {
+ if (null !== $content) {
+ throw new \LogicException('The content cannot be set on a StreamedResponse instance.');
+ }
+
+ $this->streamed = true;
+
+ return $this;
+ }
+
+ public function getContent(): string|false
+ {
+ return false;
+ }
+}
diff --git a/vendor/symfony/http-foundation/Test/Constraint/RequestAttributeValueSame.php b/vendor/symfony/http-foundation/Test/Constraint/RequestAttributeValueSame.php
new file mode 100644
index 0000000..c570848
--- /dev/null
+++ b/vendor/symfony/http-foundation/Test/Constraint/RequestAttributeValueSame.php
@@ -0,0 +1,45 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Test\Constraint;
+
+use PHPUnit\Framework\Constraint\Constraint;
+use Symfony\Component\HttpFoundation\Request;
+
+final class RequestAttributeValueSame extends Constraint
+{
+ public function __construct(
+ private string $name,
+ private string $value,
+ ) {
+ }
+
+ public function toString(): string
+ {
+ return \sprintf('has attribute "%s" with value "%s"', $this->name, $this->value);
+ }
+
+ /**
+ * @param Request $request
+ */
+ protected function matches($request): bool
+ {
+ return $this->value === $request->attributes->get($this->name);
+ }
+
+ /**
+ * @param Request $request
+ */
+ protected function failureDescription($request): string
+ {
+ return 'the Request '.$this->toString();
+ }
+}
diff --git a/vendor/symfony/http-foundation/Test/Constraint/ResponseCookieValueSame.php b/vendor/symfony/http-foundation/Test/Constraint/ResponseCookieValueSame.php
new file mode 100644
index 0000000..dbf9add
--- /dev/null
+++ b/vendor/symfony/http-foundation/Test/Constraint/ResponseCookieValueSame.php
@@ -0,0 +1,70 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Test\Constraint;
+
+use PHPUnit\Framework\Constraint\Constraint;
+use Symfony\Component\HttpFoundation\Cookie;
+use Symfony\Component\HttpFoundation\Response;
+
+final class ResponseCookieValueSame extends Constraint
+{
+ public function __construct(
+ private string $name,
+ private string $value,
+ private string $path = '/',
+ private ?string $domain = null,
+ ) {
+ }
+
+ public function toString(): string
+ {
+ $str = \sprintf('has cookie "%s"', $this->name);
+ if ('/' !== $this->path) {
+ $str .= \sprintf(' with path "%s"', $this->path);
+ }
+ if ($this->domain) {
+ $str .= \sprintf(' for domain "%s"', $this->domain);
+ }
+
+ return $str.\sprintf(' with value "%s"', $this->value);
+ }
+
+ /**
+ * @param Response $response
+ */
+ protected function matches($response): bool
+ {
+ $cookie = $this->getCookie($response);
+ if (!$cookie) {
+ return false;
+ }
+
+ return $this->value === (string) $cookie->getValue();
+ }
+
+ /**
+ * @param Response $response
+ */
+ protected function failureDescription($response): string
+ {
+ return 'the Response '.$this->toString();
+ }
+
+ protected function getCookie(Response $response): ?Cookie
+ {
+ $cookies = $response->headers->getCookies();
+
+ $filteredCookies = array_filter($cookies, fn (Cookie $cookie) => $cookie->getName() === $this->name && $cookie->getPath() === $this->path && $cookie->getDomain() === $this->domain);
+
+ return reset($filteredCookies) ?: null;
+ }
+}
diff --git a/vendor/symfony/http-foundation/Test/Constraint/ResponseFormatSame.php b/vendor/symfony/http-foundation/Test/Constraint/ResponseFormatSame.php
new file mode 100644
index 0000000..cc3655a
--- /dev/null
+++ b/vendor/symfony/http-foundation/Test/Constraint/ResponseFormatSame.php
@@ -0,0 +1,60 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Test\Constraint;
+
+use PHPUnit\Framework\Constraint\Constraint;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Asserts that the response is in the given format.
+ *
+ * @author Kévin Dunglas
+ */
+final class ResponseFormatSame extends Constraint
+{
+ public function __construct(
+ private Request $request,
+ private ?string $format,
+ private readonly bool $verbose = true,
+ ) {
+ }
+
+ public function toString(): string
+ {
+ return 'format is '.($this->format ?? 'null');
+ }
+
+ /**
+ * @param Response $response
+ */
+ protected function matches($response): bool
+ {
+ return $this->format === $this->request->getFormat($response->headers->get('Content-Type'));
+ }
+
+ /**
+ * @param Response $response
+ */
+ protected function failureDescription($response): string
+ {
+ return 'the Response '.$this->toString();
+ }
+
+ /**
+ * @param Response $response
+ */
+ protected function additionalFailureDescription($response): string
+ {
+ return $this->verbose ? (string) $response : explode("\r\n\r\n", (string) $response)[0];
+ }
+}
diff --git a/vendor/symfony/http-foundation/Test/Constraint/ResponseHasCookie.php b/vendor/symfony/http-foundation/Test/Constraint/ResponseHasCookie.php
new file mode 100644
index 0000000..0bc5803
--- /dev/null
+++ b/vendor/symfony/http-foundation/Test/Constraint/ResponseHasCookie.php
@@ -0,0 +1,64 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Test\Constraint;
+
+use PHPUnit\Framework\Constraint\Constraint;
+use Symfony\Component\HttpFoundation\Cookie;
+use Symfony\Component\HttpFoundation\Response;
+
+final class ResponseHasCookie extends Constraint
+{
+ public function __construct(
+ private string $name,
+ private string $path = '/',
+ private ?string $domain = null,
+ ) {
+ }
+
+ public function toString(): string
+ {
+ $str = \sprintf('has cookie "%s"', $this->name);
+ if ('/' !== $this->path) {
+ $str .= \sprintf(' with path "%s"', $this->path);
+ }
+ if ($this->domain) {
+ $str .= \sprintf(' for domain "%s"', $this->domain);
+ }
+
+ return $str;
+ }
+
+ /**
+ * @param Response $response
+ */
+ protected function matches($response): bool
+ {
+ return null !== $this->getCookie($response);
+ }
+
+ /**
+ * @param Response $response
+ */
+ protected function failureDescription($response): string
+ {
+ return 'the Response '.$this->toString();
+ }
+
+ private function getCookie(Response $response): ?Cookie
+ {
+ $cookies = $response->headers->getCookies();
+
+ $filteredCookies = array_filter($cookies, fn (Cookie $cookie) => $cookie->getName() === $this->name && $cookie->getPath() === $this->path && $cookie->getDomain() === $this->domain);
+
+ return reset($filteredCookies) ?: null;
+ }
+}
diff --git a/vendor/symfony/http-foundation/Test/Constraint/ResponseHasHeader.php b/vendor/symfony/http-foundation/Test/Constraint/ResponseHasHeader.php
new file mode 100644
index 0000000..52fd3c1
--- /dev/null
+++ b/vendor/symfony/http-foundation/Test/Constraint/ResponseHasHeader.php
@@ -0,0 +1,44 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Test\Constraint;
+
+use PHPUnit\Framework\Constraint\Constraint;
+use Symfony\Component\HttpFoundation\Response;
+
+final class ResponseHasHeader extends Constraint
+{
+ public function __construct(
+ private string $headerName,
+ ) {
+ }
+
+ public function toString(): string
+ {
+ return \sprintf('has header "%s"', $this->headerName);
+ }
+
+ /**
+ * @param Response $response
+ */
+ protected function matches($response): bool
+ {
+ return $response->headers->has($this->headerName);
+ }
+
+ /**
+ * @param Response $response
+ */
+ protected function failureDescription($response): string
+ {
+ return 'the Response '.$this->toString();
+ }
+}
diff --git a/vendor/symfony/http-foundation/Test/Constraint/ResponseHeaderLocationSame.php b/vendor/symfony/http-foundation/Test/Constraint/ResponseHeaderLocationSame.php
new file mode 100644
index 0000000..833ffd9
--- /dev/null
+++ b/vendor/symfony/http-foundation/Test/Constraint/ResponseHeaderLocationSame.php
@@ -0,0 +1,65 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Test\Constraint;
+
+use PHPUnit\Framework\Constraint\Constraint;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+final class ResponseHeaderLocationSame extends Constraint
+{
+ public function __construct(private Request $request, private string $expectedValue)
+ {
+ }
+
+ public function toString(): string
+ {
+ return \sprintf('has header "Location" matching "%s"', $this->expectedValue);
+ }
+
+ protected function matches($other): bool
+ {
+ if (!$other instanceof Response) {
+ return false;
+ }
+
+ $location = $other->headers->get('Location');
+
+ if (null === $location) {
+ return false;
+ }
+
+ return $this->toFullUrl($this->expectedValue) === $this->toFullUrl($location);
+ }
+
+ protected function failureDescription($other): string
+ {
+ return 'the Response '.$this->toString();
+ }
+
+ private function toFullUrl(string $url): string
+ {
+ if (null === parse_url($url, \PHP_URL_PATH)) {
+ $url .= '/';
+ }
+
+ if (str_starts_with($url, '//')) {
+ return \sprintf('%s:%s', $this->request->getScheme(), $url);
+ }
+
+ if (str_starts_with($url, '/')) {
+ return $this->request->getSchemeAndHttpHost().$url;
+ }
+
+ return $url;
+ }
+}
diff --git a/vendor/symfony/http-foundation/Test/Constraint/ResponseHeaderSame.php b/vendor/symfony/http-foundation/Test/Constraint/ResponseHeaderSame.php
new file mode 100644
index 0000000..f2ae27f
--- /dev/null
+++ b/vendor/symfony/http-foundation/Test/Constraint/ResponseHeaderSame.php
@@ -0,0 +1,45 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Test\Constraint;
+
+use PHPUnit\Framework\Constraint\Constraint;
+use Symfony\Component\HttpFoundation\Response;
+
+final class ResponseHeaderSame extends Constraint
+{
+ public function __construct(
+ private string $headerName,
+ private string $expectedValue,
+ ) {
+ }
+
+ public function toString(): string
+ {
+ return \sprintf('has header "%s" with value "%s"', $this->headerName, $this->expectedValue);
+ }
+
+ /**
+ * @param Response $response
+ */
+ protected function matches($response): bool
+ {
+ return $this->expectedValue === $response->headers->get($this->headerName, null);
+ }
+
+ /**
+ * @param Response $response
+ */
+ protected function failureDescription($response): string
+ {
+ return 'the Response '.$this->toString();
+ }
+}
diff --git a/vendor/symfony/http-foundation/Test/Constraint/ResponseIsRedirected.php b/vendor/symfony/http-foundation/Test/Constraint/ResponseIsRedirected.php
new file mode 100644
index 0000000..b7ae15e
--- /dev/null
+++ b/vendor/symfony/http-foundation/Test/Constraint/ResponseIsRedirected.php
@@ -0,0 +1,54 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Test\Constraint;
+
+use PHPUnit\Framework\Constraint\Constraint;
+use Symfony\Component\HttpFoundation\Response;
+
+final class ResponseIsRedirected extends Constraint
+{
+ /**
+ * @param bool $verbose If true, the entire response is printed on failure. If false, the response body is omitted.
+ */
+ public function __construct(private readonly bool $verbose = true)
+ {
+ }
+
+ public function toString(): string
+ {
+ return 'is redirected';
+ }
+
+ /**
+ * @param Response $response
+ */
+ protected function matches($response): bool
+ {
+ return $response->isRedirect();
+ }
+
+ /**
+ * @param Response $response
+ */
+ protected function failureDescription($response): string
+ {
+ return 'the Response '.$this->toString();
+ }
+
+ /**
+ * @param Response $response
+ */
+ protected function additionalFailureDescription($response): string
+ {
+ return $this->verbose ? (string) $response : explode("\r\n\r\n", (string) $response)[0];
+ }
+}
diff --git a/vendor/symfony/http-foundation/Test/Constraint/ResponseIsSuccessful.php b/vendor/symfony/http-foundation/Test/Constraint/ResponseIsSuccessful.php
new file mode 100644
index 0000000..94a65ed
--- /dev/null
+++ b/vendor/symfony/http-foundation/Test/Constraint/ResponseIsSuccessful.php
@@ -0,0 +1,54 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Test\Constraint;
+
+use PHPUnit\Framework\Constraint\Constraint;
+use Symfony\Component\HttpFoundation\Response;
+
+final class ResponseIsSuccessful extends Constraint
+{
+ /**
+ * @param bool $verbose If true, the entire response is printed on failure. If false, the response body is omitted.
+ */
+ public function __construct(private readonly bool $verbose = true)
+ {
+ }
+
+ public function toString(): string
+ {
+ return 'is successful';
+ }
+
+ /**
+ * @param Response $response
+ */
+ protected function matches($response): bool
+ {
+ return $response->isSuccessful();
+ }
+
+ /**
+ * @param Response $response
+ */
+ protected function failureDescription($response): string
+ {
+ return 'the Response '.$this->toString();
+ }
+
+ /**
+ * @param Response $response
+ */
+ protected function additionalFailureDescription($response): string
+ {
+ return $this->verbose ? (string) $response : explode("\r\n\r\n", (string) $response)[0];
+ }
+}
diff --git a/vendor/symfony/http-foundation/Test/Constraint/ResponseIsUnprocessable.php b/vendor/symfony/http-foundation/Test/Constraint/ResponseIsUnprocessable.php
new file mode 100644
index 0000000..799d558
--- /dev/null
+++ b/vendor/symfony/http-foundation/Test/Constraint/ResponseIsUnprocessable.php
@@ -0,0 +1,54 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Test\Constraint;
+
+use PHPUnit\Framework\Constraint\Constraint;
+use Symfony\Component\HttpFoundation\Response;
+
+final class ResponseIsUnprocessable extends Constraint
+{
+ /**
+ * @param bool $verbose If true, the entire response is printed on failure. If false, the response body is omitted.
+ */
+ public function __construct(private readonly bool $verbose = true)
+ {
+ }
+
+ public function toString(): string
+ {
+ return 'is unprocessable';
+ }
+
+ /**
+ * @param Response $other
+ */
+ protected function matches($other): bool
+ {
+ return Response::HTTP_UNPROCESSABLE_ENTITY === $other->getStatusCode();
+ }
+
+ /**
+ * @param Response $other
+ */
+ protected function failureDescription($other): string
+ {
+ return 'the Response '.$this->toString();
+ }
+
+ /**
+ * @param Response $response
+ */
+ protected function additionalFailureDescription($response): string
+ {
+ return $this->verbose ? (string) $response : explode("\r\n\r\n", (string) $response)[0];
+ }
+}
diff --git a/vendor/symfony/http-foundation/Test/Constraint/ResponseStatusCodeSame.php b/vendor/symfony/http-foundation/Test/Constraint/ResponseStatusCodeSame.php
new file mode 100644
index 0000000..1223608
--- /dev/null
+++ b/vendor/symfony/http-foundation/Test/Constraint/ResponseStatusCodeSame.php
@@ -0,0 +1,53 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Test\Constraint;
+
+use PHPUnit\Framework\Constraint\Constraint;
+use Symfony\Component\HttpFoundation\Response;
+
+final class ResponseStatusCodeSame extends Constraint
+{
+ public function __construct(
+ private int $statusCode,
+ private readonly bool $verbose = true,
+ ) {
+ }
+
+ public function toString(): string
+ {
+ return 'status code is '.$this->statusCode;
+ }
+
+ /**
+ * @param Response $response
+ */
+ protected function matches($response): bool
+ {
+ return $this->statusCode === $response->getStatusCode();
+ }
+
+ /**
+ * @param Response $response
+ */
+ protected function failureDescription($response): string
+ {
+ return 'the Response '.$this->toString();
+ }
+
+ /**
+ * @param Response $response
+ */
+ protected function additionalFailureDescription($response): string
+ {
+ return $this->verbose ? (string) $response : explode("\r\n\r\n", (string) $response)[0];
+ }
+}
diff --git a/vendor/symfony/http-foundation/UriSigner.php b/vendor/symfony/http-foundation/UriSigner.php
new file mode 100644
index 0000000..d441e47
--- /dev/null
+++ b/vendor/symfony/http-foundation/UriSigner.php
@@ -0,0 +1,206 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation;
+
+use Psr\Clock\ClockInterface;
+use Symfony\Component\HttpFoundation\Exception\ExpiredSignedUriException;
+use Symfony\Component\HttpFoundation\Exception\LogicException;
+use Symfony\Component\HttpFoundation\Exception\SignedUriException;
+use Symfony\Component\HttpFoundation\Exception\UnsignedUriException;
+use Symfony\Component\HttpFoundation\Exception\UnverifiedSignedUriException;
+
+/**
+ * @author Fabien Potencier
+ */
+class UriSigner
+{
+ private const STATUS_VALID = 1;
+ private const STATUS_INVALID = 2;
+ private const STATUS_MISSING = 3;
+ private const STATUS_EXPIRED = 4;
+
+ /**
+ * @param string $hashParameter Query string parameter to use
+ * @param string $expirationParameter Query string parameter to use for expiration
+ */
+ public function __construct(
+ #[\SensitiveParameter] private string $secret,
+ private string $hashParameter = '_hash',
+ private string $expirationParameter = '_expiration',
+ private ?ClockInterface $clock = null,
+ ) {
+ if (!$secret) {
+ throw new \InvalidArgumentException('A non-empty secret is required.');
+ }
+ }
+
+ /**
+ * Signs a URI.
+ *
+ * The given URI is signed by adding the query string parameter
+ * which value depends on the URI and the secret.
+ *
+ * @param \DateTimeInterface|\DateInterval|int|null $expiration The expiration for the given URI.
+ * If $expiration is a \DateTimeInterface, it's expected to be the exact date + time.
+ * If $expiration is a \DateInterval, the interval is added to "now" to get the date + time.
+ * If $expiration is an int, it's expected to be a timestamp in seconds of the exact date + time.
+ * If $expiration is null, no expiration.
+ *
+ * The expiration is added as a query string parameter.
+ */
+ public function sign(string $uri, \DateTimeInterface|\DateInterval|int|null $expiration = null): string
+ {
+ $url = parse_url($uri);
+ $params = [];
+
+ if (isset($url['query'])) {
+ parse_str($url['query'], $params);
+ }
+
+ if (isset($params[$this->hashParameter])) {
+ throw new LogicException(\sprintf('URI query parameter conflict: parameter name "%s" is reserved.', $this->hashParameter));
+ }
+
+ if (isset($params[$this->expirationParameter])) {
+ throw new LogicException(\sprintf('URI query parameter conflict: parameter name "%s" is reserved.', $this->expirationParameter));
+ }
+
+ if (null !== $expiration) {
+ $params[$this->expirationParameter] = $this->getExpirationTime($expiration);
+ }
+
+ $uri = $this->buildUrl($url, $params);
+ $params[$this->hashParameter] = $this->computeHash($uri);
+
+ return $this->buildUrl($url, $params);
+ }
+
+ /**
+ * Checks that a URI contains the correct hash.
+ * Also checks if the URI has not expired (If you used expiration during signing).
+ */
+ public function check(string $uri): bool
+ {
+ return self::STATUS_VALID === $this->doVerify($uri);
+ }
+
+ public function checkRequest(Request $request): bool
+ {
+ return self::STATUS_VALID === $this->doVerify(self::normalize($request));
+ }
+
+ /**
+ * Verify a Request or string URI.
+ *
+ * @throws UnsignedUriException If the URI is not signed
+ * @throws UnverifiedSignedUriException If the signature is invalid
+ * @throws ExpiredSignedUriException If the URI has expired
+ * @throws SignedUriException
+ */
+ public function verify(Request|string $uri): void
+ {
+ $uri = self::normalize($uri);
+ $status = $this->doVerify($uri);
+
+ match ($status) {
+ self::STATUS_VALID => null,
+ self::STATUS_INVALID => throw new UnverifiedSignedUriException(),
+ self::STATUS_EXPIRED => throw new ExpiredSignedUriException(),
+ default => throw new UnsignedUriException(),
+ };
+ }
+
+ private function computeHash(string $uri): string
+ {
+ return strtr(rtrim(base64_encode(hash_hmac('sha256', $uri, $this->secret, true)), '='), ['/' => '_', '+' => '-']);
+ }
+
+ private function buildUrl(array $url, array $params = []): string
+ {
+ ksort($params, \SORT_STRING);
+ $url['query'] = http_build_query($params, '', '&');
+
+ $scheme = isset($url['scheme']) ? $url['scheme'].'://' : '';
+ $host = $url['host'] ?? '';
+ $port = isset($url['port']) ? ':'.$url['port'] : '';
+ $user = $url['user'] ?? '';
+ $pass = isset($url['pass']) ? ':'.$url['pass'] : '';
+ $pass = ($user || $pass) ? "$pass@" : '';
+ $path = $url['path'] ?? '';
+ $query = $url['query'] ? '?'.$url['query'] : '';
+ $fragment = isset($url['fragment']) ? '#'.$url['fragment'] : '';
+
+ return $scheme.$user.$pass.$host.$port.$path.$query.$fragment;
+ }
+
+ private function getExpirationTime(\DateTimeInterface|\DateInterval|int $expiration): string
+ {
+ if ($expiration instanceof \DateTimeInterface) {
+ return $expiration->format('U');
+ }
+
+ if ($expiration instanceof \DateInterval) {
+ return $this->now()->add($expiration)->format('U');
+ }
+
+ return (string) $expiration;
+ }
+
+ private function now(): \DateTimeImmutable
+ {
+ return $this->clock?->now() ?? \DateTimeImmutable::createFromFormat('U', time());
+ }
+
+ /**
+ * @return self::STATUS_*
+ */
+ private function doVerify(string $uri): int
+ {
+ $url = parse_url($uri);
+ $params = [];
+
+ if (isset($url['query'])) {
+ parse_str($url['query'], $params);
+ }
+
+ if (empty($params[$this->hashParameter])) {
+ return self::STATUS_MISSING;
+ }
+
+ $hash = $params[$this->hashParameter];
+ unset($params[$this->hashParameter]);
+
+ if (!hash_equals($this->computeHash($this->buildUrl($url, $params)), strtr(rtrim($hash, '='), ['/' => '_', '+' => '-']))) {
+ return self::STATUS_INVALID;
+ }
+
+ if (!$expiration = $params[$this->expirationParameter] ?? false) {
+ return self::STATUS_VALID;
+ }
+
+ if ($this->now()->getTimestamp() < $expiration) {
+ return self::STATUS_VALID;
+ }
+
+ return self::STATUS_EXPIRED;
+ }
+
+ private static function normalize(Request|string $uri): string
+ {
+ if ($uri instanceof Request) {
+ $qs = ($qs = $uri->server->get('QUERY_STRING')) ? '?'.$qs : '';
+ $uri = $uri->getSchemeAndHttpHost().$uri->getBaseUrl().$uri->getPathInfo().$qs;
+ }
+
+ return $uri;
+ }
+}
diff --git a/vendor/symfony/http-foundation/UrlHelper.php b/vendor/symfony/http-foundation/UrlHelper.php
new file mode 100644
index 0000000..f971cf6
--- /dev/null
+++ b/vendor/symfony/http-foundation/UrlHelper.php
@@ -0,0 +1,108 @@
+
+ *
+ * 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;
+use Symfony\Component\Routing\RequestContextAwareInterface;
+
+/**
+ * A helper service for manipulating URLs within and outside the request scope.
+ *
+ * @author Valentin Udaltsov
+ */
+final class UrlHelper
+{
+ public function __construct(
+ private RequestStack $requestStack,
+ private RequestContextAwareInterface|RequestContext|null $requestContext = null,
+ ) {
+ }
+
+ public function getAbsoluteUrl(string $path): string
+ {
+ if (str_contains($path, '://') || str_starts_with($path, '//')) {
+ 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, '://') || str_starts_with($path, '//')) {
+ return $path;
+ }
+
+ if (null === $request = $this->requestStack->getMainRequest()) {
+ return $path;
+ }
+
+ return $request->getRelativeUriForPath($path);
+ }
+
+ private function getAbsoluteUrlFromContext(string $path): string
+ {
+ if (null === $context = $this->requestContext) {
+ return $path;
+ }
+
+ if ($context instanceof RequestContextAwareInterface) {
+ $context = $context->getContext();
+ }
+
+ if ('' === $host = $context->getHost()) {
+ return $path;
+ }
+
+ $scheme = $context->getScheme();
+ $port = '';
+
+ if ('http' === $scheme && 80 !== $context->getHttpPort()) {
+ $port = ':'.$context->getHttpPort();
+ } elseif ('https' === $scheme && 443 !== $context->getHttpsPort()) {
+ $port = ':'.$context->getHttpsPort();
+ }
+
+ if ('#' === $path[0]) {
+ $queryString = $context->getQueryString();
+ $path = $context->getPathInfo().($queryString ? '?'.$queryString : '').$path;
+ } elseif ('?' === $path[0]) {
+ $path = $context->getPathInfo().$path;
+ }
+
+ if ('/' !== $path[0]) {
+ $path = rtrim($context->getBaseUrl(), '/').'/'.$path;
+ }
+
+ return $scheme.'://'.$host.$port.$path;
+ }
+}
diff --git a/vendor/symfony/http-foundation/composer.json b/vendor/symfony/http-foundation/composer.json
new file mode 100644
index 0000000..6c54d43
--- /dev/null
+++ b/vendor/symfony/http-foundation/composer.json
@@ -0,0 +1,43 @@
+{
+ "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": ">=8.4",
+ "symfony/polyfill-mbstring": "^1.1"
+ },
+ "require-dev": {
+ "doctrine/dbal": "^4.3",
+ "predis/predis": "^1.1|^2.0",
+ "symfony/cache": "^7.4|^8.0",
+ "symfony/clock": "^7.4|^8.0",
+ "symfony/dependency-injection": "^7.4|^8.0",
+ "symfony/expression-language": "^7.4|^8.0",
+ "symfony/http-kernel": "^7.4|^8.0",
+ "symfony/mime": "^7.4|^8.0",
+ "symfony/rate-limiter": "^7.4|^8.0"
+ },
+ "conflict": {
+ "doctrine/dbal": "<4.3"
+ },
+ "autoload": {
+ "psr-4": { "Symfony\\Component\\HttpFoundation\\": "" },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "minimum-stability": "dev"
+}
diff --git a/vendor/symfony/polyfill-mbstring/LICENSE b/vendor/symfony/polyfill-mbstring/LICENSE
new file mode 100644
index 0000000..6e3afce
--- /dev/null
+++ b/vendor/symfony/polyfill-mbstring/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2015-present 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/vendor/symfony/polyfill-mbstring/Mbstring.php b/vendor/symfony/polyfill-mbstring/Mbstring.php
new file mode 100644
index 0000000..3d45c9d
--- /dev/null
+++ b/vendor/symfony/polyfill-mbstring/Mbstring.php
@@ -0,0 +1,1045 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Polyfill\Mbstring;
+
+/**
+ * Partial mbstring implementation in PHP, iconv based, UTF-8 centric.
+ *
+ * Implemented:
+ * - mb_chr - Returns a specific character from its Unicode code point
+ * - mb_convert_encoding - Convert character encoding
+ * - mb_convert_variables - Convert character code in variable(s)
+ * - mb_decode_mimeheader - Decode string in MIME header field
+ * - mb_encode_mimeheader - Encode string for MIME header XXX NATIVE IMPLEMENTATION IS REALLY BUGGED
+ * - mb_decode_numericentity - Decode HTML numeric string reference to character
+ * - mb_encode_numericentity - Encode character to HTML numeric string reference
+ * - mb_convert_case - Perform case folding on a string
+ * - mb_detect_encoding - Detect character encoding
+ * - mb_get_info - Get internal settings of mbstring
+ * - mb_http_input - Detect HTTP input character encoding
+ * - mb_http_output - Set/Get HTTP output character encoding
+ * - mb_internal_encoding - Set/Get internal character encoding
+ * - mb_list_encodings - Returns an array of all supported encodings
+ * - mb_ord - Returns the Unicode code point of a character
+ * - mb_output_handler - Callback function converts character encoding in output buffer
+ * - mb_scrub - Replaces ill-formed byte sequences with substitute characters
+ * - mb_strlen - Get string length
+ * - mb_strpos - Find position of first occurrence of string in a string
+ * - mb_strrpos - Find position of last occurrence of a string in a string
+ * - mb_str_split - Convert a string to an array
+ * - mb_strtolower - Make a string lowercase
+ * - mb_strtoupper - Make a string uppercase
+ * - mb_substitute_character - Set/Get substitution character
+ * - mb_substr - Get part of string
+ * - mb_stripos - Finds position of first occurrence of a string within another, case insensitive
+ * - mb_stristr - Finds first occurrence of a string within another, case insensitive
+ * - mb_strrchr - Finds the last occurrence of a character in a string within another
+ * - mb_strrichr - Finds the last occurrence of a character in a string within another, case insensitive
+ * - mb_strripos - Finds position of last occurrence of a string within another, case insensitive
+ * - mb_strstr - Finds first occurrence of a string within another
+ * - mb_strwidth - Return width of string
+ * - mb_substr_count - Count the number of substring occurrences
+ * - mb_ucfirst - Make a string's first character uppercase
+ * - mb_lcfirst - Make a string's first character lowercase
+ * - mb_trim - Strip whitespace (or other characters) from the beginning and end of a string
+ * - mb_ltrim - Strip whitespace (or other characters) from the beginning of a string
+ * - mb_rtrim - Strip whitespace (or other characters) from the end of a string
+ *
+ * Not implemented:
+ * - mb_convert_kana - Convert "kana" one from another ("zen-kaku", "han-kaku" and more)
+ * - mb_ereg_* - Regular expression with multibyte support
+ * - mb_parse_str - Parse GET/POST/COOKIE data and set global variable
+ * - mb_preferred_mime_name - Get MIME charset string
+ * - mb_regex_encoding - Returns current encoding for multibyte regex as string
+ * - mb_regex_set_options - Set/Get the default options for mbregex functions
+ * - mb_send_mail - Send encoded mail
+ * - mb_split - Split multibyte string using regular expression
+ * - mb_strcut - Get part of string
+ * - mb_strimwidth - Get truncated string with specified width
+ *
+ * @author Nicolas Grekas
+ *
+ * @internal
+ */
+final class Mbstring
+{
+ public const MB_CASE_FOLD = \PHP_INT_MAX;
+
+ private const SIMPLE_CASE_FOLD = [
+ ['µ', 'ſ', "\xCD\x85", 'ς', "\xCF\x90", "\xCF\x91", "\xCF\x95", "\xCF\x96", "\xCF\xB0", "\xCF\xB1", "\xCF\xB5", "\xE1\xBA\x9B", "\xE1\xBE\xBE"],
+ ['μ', 's', 'ι', 'σ', 'β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', "\xE1\xB9\xA1", 'ι'],
+ ];
+
+ private static $encodingList = ['ASCII', 'UTF-8'];
+ private static $language = 'neutral';
+ private static $internalEncoding = 'UTF-8';
+
+ public static function mb_convert_encoding($s, $toEncoding, $fromEncoding = null)
+ {
+ if (\is_array($s)) {
+ $r = [];
+ foreach ($s as $str) {
+ $r[] = self::mb_convert_encoding($str, $toEncoding, $fromEncoding);
+ }
+
+ return $r;
+ }
+
+ if (\is_array($fromEncoding) || (null !== $fromEncoding && false !== strpos($fromEncoding, ','))) {
+ $fromEncoding = self::mb_detect_encoding($s, $fromEncoding);
+ } else {
+ $fromEncoding = self::getEncoding($fromEncoding);
+ }
+
+ $toEncoding = self::getEncoding($toEncoding);
+
+ if ('BASE64' === $fromEncoding) {
+ $s = base64_decode($s);
+ $fromEncoding = $toEncoding;
+ }
+
+ if ('BASE64' === $toEncoding) {
+ return base64_encode($s);
+ }
+
+ if ('HTML-ENTITIES' === $toEncoding || 'HTML' === $toEncoding) {
+ if ('HTML-ENTITIES' === $fromEncoding || 'HTML' === $fromEncoding) {
+ $fromEncoding = 'Windows-1252';
+ }
+ if ('UTF-8' !== $fromEncoding) {
+ $s = iconv($fromEncoding, 'UTF-8//IGNORE', $s);
+ }
+
+ return preg_replace_callback('/[\x80-\xFF]+/', [__CLASS__, 'html_encoding_callback'], $s);
+ }
+
+ if ('HTML-ENTITIES' === $fromEncoding) {
+ $s = html_entity_decode($s, \ENT_COMPAT, 'UTF-8');
+ $fromEncoding = 'UTF-8';
+ }
+
+ return iconv($fromEncoding, $toEncoding.'//IGNORE', $s);
+ }
+
+ public static function mb_convert_variables($toEncoding, $fromEncoding, &...$vars)
+ {
+ $ok = true;
+ array_walk_recursive($vars, function (&$v) use (&$ok, $toEncoding, $fromEncoding) {
+ if (false === $v = self::mb_convert_encoding($v, $toEncoding, $fromEncoding)) {
+ $ok = false;
+ }
+ });
+
+ return $ok ? $fromEncoding : false;
+ }
+
+ public static function mb_decode_mimeheader($s)
+ {
+ return iconv_mime_decode($s, 2, self::$internalEncoding);
+ }
+
+ public static function mb_encode_mimeheader($s, $charset = null, $transferEncoding = null, $linefeed = null, $indent = null)
+ {
+ trigger_error('mb_encode_mimeheader() is bugged. Please use iconv_mime_encode() instead', \E_USER_WARNING);
+ }
+
+ public static function mb_decode_numericentity($s, $convmap, $encoding = null)
+ {
+ if (null !== $s && !\is_scalar($s) && !(\is_object($s) && method_exists($s, '__toString'))) {
+ trigger_error('mb_decode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', \E_USER_WARNING);
+
+ return null;
+ }
+
+ if (!\is_array($convmap) || (80000 > \PHP_VERSION_ID && !$convmap)) {
+ return false;
+ }
+
+ if (null !== $encoding && !\is_scalar($encoding)) {
+ trigger_error('mb_decode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', \E_USER_WARNING);
+
+ return ''; // Instead of null (cf. mb_encode_numericentity).
+ }
+
+ $s = (string) $s;
+ if ('' === $s) {
+ return '';
+ }
+
+ $encoding = self::getEncoding($encoding);
+
+ if ('UTF-8' === $encoding) {
+ $encoding = null;
+ if (!preg_match('//u', $s)) {
+ $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s);
+ }
+ } else {
+ $s = iconv($encoding, 'UTF-8//IGNORE', $s);
+ }
+
+ $cnt = floor(\count($convmap) / 4) * 4;
+
+ for ($i = 0; $i < $cnt; $i += 4) {
+ // collector_decode_htmlnumericentity ignores $convmap[$i + 3]
+ $convmap[$i] += $convmap[$i + 2];
+ $convmap[$i + 1] += $convmap[$i + 2];
+ }
+
+ $s = preg_replace_callback('/(?:0*([0-9]+)|x0*([0-9a-fA-F]+))(?!&);?/', function (array $m) use ($cnt, $convmap) {
+ $c = isset($m[2]) ? (int) hexdec($m[2]) : $m[1];
+ for ($i = 0; $i < $cnt; $i += 4) {
+ if ($c >= $convmap[$i] && $c <= $convmap[$i + 1]) {
+ return self::mb_chr($c - $convmap[$i + 2]);
+ }
+ }
+
+ return $m[0];
+ }, $s);
+
+ if (null === $encoding) {
+ return $s;
+ }
+
+ return iconv('UTF-8', $encoding.'//IGNORE', $s);
+ }
+
+ public static function mb_encode_numericentity($s, $convmap, $encoding = null, $is_hex = false)
+ {
+ if (null !== $s && !\is_scalar($s) && !(\is_object($s) && method_exists($s, '__toString'))) {
+ trigger_error('mb_encode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', \E_USER_WARNING);
+
+ return null;
+ }
+
+ if (!\is_array($convmap) || (80000 > \PHP_VERSION_ID && !$convmap)) {
+ return false;
+ }
+
+ if (null !== $encoding && !\is_scalar($encoding)) {
+ trigger_error('mb_encode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', \E_USER_WARNING);
+
+ return null; // Instead of '' (cf. mb_decode_numericentity).
+ }
+
+ if (null !== $is_hex && !\is_scalar($is_hex)) {
+ trigger_error('mb_encode_numericentity() expects parameter 4 to be boolean, '.\gettype($s).' given', \E_USER_WARNING);
+
+ return null;
+ }
+
+ $s = (string) $s;
+ if ('' === $s) {
+ return '';
+ }
+
+ $encoding = self::getEncoding($encoding);
+
+ if ('UTF-8' === $encoding) {
+ $encoding = null;
+ if (!preg_match('//u', $s)) {
+ $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s);
+ }
+ } else {
+ $s = iconv($encoding, 'UTF-8//IGNORE', $s);
+ }
+
+ static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4];
+
+ $cnt = floor(\count($convmap) / 4) * 4;
+ $i = 0;
+ $len = \strlen($s);
+ $result = '';
+
+ while ($i < $len) {
+ $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"];
+ $uchr = substr($s, $i, $ulen);
+ $i += $ulen;
+ $c = self::mb_ord($uchr);
+
+ for ($j = 0; $j < $cnt; $j += 4) {
+ if ($c >= $convmap[$j] && $c <= $convmap[$j + 1]) {
+ $cOffset = ($c + $convmap[$j + 2]) & $convmap[$j + 3];
+ $result .= $is_hex ? sprintf('%X;', $cOffset) : ''.$cOffset.';';
+ continue 2;
+ }
+ }
+ $result .= $uchr;
+ }
+
+ if (null === $encoding) {
+ return $result;
+ }
+
+ return iconv('UTF-8', $encoding.'//IGNORE', $result);
+ }
+
+ public static function mb_convert_case($s, $mode, $encoding = null)
+ {
+ $s = (string) $s;
+ if ('' === $s) {
+ return '';
+ }
+
+ $encoding = self::getEncoding($encoding);
+
+ if ('UTF-8' === $encoding) {
+ $encoding = null;
+ if (!preg_match('//u', $s)) {
+ $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s);
+ }
+ } else {
+ $s = iconv($encoding, 'UTF-8//IGNORE', $s);
+ }
+
+ if (\MB_CASE_TITLE == $mode) {
+ static $titleRegexp = null;
+ if (null === $titleRegexp) {
+ $titleRegexp = self::getData('titleCaseRegexp');
+ }
+ $s = preg_replace_callback($titleRegexp, [__CLASS__, 'title_case'], $s);
+ } else {
+ if (\MB_CASE_UPPER == $mode) {
+ static $upper = null;
+ if (null === $upper) {
+ $upper = self::getData('upperCase');
+ }
+ $map = $upper;
+ } else {
+ if (self::MB_CASE_FOLD === $mode) {
+ static $caseFolding = null;
+ if (null === $caseFolding) {
+ $caseFolding = self::getData('caseFolding');
+ }
+ $s = strtr($s, $caseFolding);
+ }
+
+ static $lower = null;
+ if (null === $lower) {
+ $lower = self::getData('lowerCase');
+ }
+ $map = $lower;
+ }
+
+ static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4];
+
+ $i = 0;
+ $len = \strlen($s);
+
+ while ($i < $len) {
+ $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"];
+ $uchr = substr($s, $i, $ulen);
+ $i += $ulen;
+
+ if (isset($map[$uchr])) {
+ $uchr = $map[$uchr];
+ $nlen = \strlen($uchr);
+
+ if ($nlen == $ulen) {
+ $nlen = $i;
+ do {
+ $s[--$nlen] = $uchr[--$ulen];
+ } while ($ulen);
+ } else {
+ $s = substr_replace($s, $uchr, $i - $ulen, $ulen);
+ $len += $nlen - $ulen;
+ $i += $nlen - $ulen;
+ }
+ }
+ }
+ }
+
+ if (null === $encoding) {
+ return $s;
+ }
+
+ return iconv('UTF-8', $encoding.'//IGNORE', $s);
+ }
+
+ public static function mb_internal_encoding($encoding = null)
+ {
+ if (null === $encoding) {
+ return self::$internalEncoding;
+ }
+
+ $normalizedEncoding = self::getEncoding($encoding);
+
+ if ('UTF-8' === $normalizedEncoding || false !== @iconv($normalizedEncoding, $normalizedEncoding, ' ')) {
+ self::$internalEncoding = $normalizedEncoding;
+
+ return true;
+ }
+
+ if (80000 > \PHP_VERSION_ID) {
+ return false;
+ }
+
+ throw new \ValueError(sprintf('Argument #1 ($encoding) must be a valid encoding, "%s" given', $encoding));
+ }
+
+ public static function mb_language($lang = null)
+ {
+ if (null === $lang) {
+ return self::$language;
+ }
+
+ switch ($normalizedLang = strtolower($lang)) {
+ case 'uni':
+ case 'neutral':
+ self::$language = $normalizedLang;
+
+ return true;
+ }
+
+ if (80000 > \PHP_VERSION_ID) {
+ return false;
+ }
+
+ throw new \ValueError(sprintf('Argument #1 ($language) must be a valid language, "%s" given', $lang));
+ }
+
+ public static function mb_list_encodings()
+ {
+ return ['UTF-8'];
+ }
+
+ public static function mb_encoding_aliases($encoding)
+ {
+ switch (strtoupper($encoding)) {
+ case 'UTF8':
+ case 'UTF-8':
+ return ['utf8'];
+ }
+
+ return false;
+ }
+
+ public static function mb_check_encoding($var = null, $encoding = null)
+ {
+ if (null === $encoding) {
+ if (null === $var) {
+ return false;
+ }
+ $encoding = self::$internalEncoding;
+ }
+
+ if (!\is_array($var)) {
+ return self::mb_detect_encoding($var, [$encoding]) || false !== @iconv($encoding, $encoding, $var);
+ }
+
+ foreach ($var as $key => $value) {
+ if (!self::mb_check_encoding($key, $encoding)) {
+ return false;
+ }
+ if (!self::mb_check_encoding($value, $encoding)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public static function mb_detect_encoding($str, $encodingList = null, $strict = false)
+ {
+ if (null === $encodingList) {
+ $encodingList = self::$encodingList;
+ } else {
+ if (!\is_array($encodingList)) {
+ $encodingList = array_map('trim', explode(',', $encodingList));
+ }
+ $encodingList = array_map('strtoupper', $encodingList);
+ }
+
+ foreach ($encodingList as $enc) {
+ switch ($enc) {
+ case 'ASCII':
+ if (!preg_match('/[\x80-\xFF]/', $str)) {
+ return $enc;
+ }
+ break;
+
+ case 'UTF8':
+ case 'UTF-8':
+ if (preg_match('//u', $str)) {
+ return 'UTF-8';
+ }
+ break;
+
+ default:
+ if (0 === strncmp($enc, 'ISO-8859-', 9)) {
+ return $enc;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public static function mb_detect_order($encodingList = null)
+ {
+ if (null === $encodingList) {
+ return self::$encodingList;
+ }
+
+ if (!\is_array($encodingList)) {
+ $encodingList = array_map('trim', explode(',', $encodingList));
+ }
+ $encodingList = array_map('strtoupper', $encodingList);
+
+ foreach ($encodingList as $enc) {
+ switch ($enc) {
+ default:
+ if (strncmp($enc, 'ISO-8859-', 9)) {
+ return false;
+ }
+ // no break
+ case 'ASCII':
+ case 'UTF8':
+ case 'UTF-8':
+ }
+ }
+
+ self::$encodingList = $encodingList;
+
+ return true;
+ }
+
+ public static function mb_strlen($s, $encoding = null)
+ {
+ $encoding = self::getEncoding($encoding);
+ if ('CP850' === $encoding || 'ASCII' === $encoding) {
+ return \strlen($s);
+ }
+
+ return @iconv_strlen($s, $encoding);
+ }
+
+ public static function mb_strpos($haystack, $needle, $offset = 0, $encoding = null)
+ {
+ $encoding = self::getEncoding($encoding);
+ if ('CP850' === $encoding || 'ASCII' === $encoding) {
+ return strpos($haystack, $needle, $offset);
+ }
+
+ $needle = (string) $needle;
+ if ('' === $needle) {
+ if (80000 > \PHP_VERSION_ID) {
+ trigger_error(__METHOD__.': Empty delimiter', \E_USER_WARNING);
+
+ return false;
+ }
+
+ return 0;
+ }
+
+ return iconv_strpos($haystack, $needle, $offset, $encoding);
+ }
+
+ public static function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null)
+ {
+ $encoding = self::getEncoding($encoding);
+ if ('CP850' === $encoding || 'ASCII' === $encoding) {
+ return strrpos($haystack, $needle, $offset);
+ }
+
+ if ($offset != (int) $offset) {
+ $offset = 0;
+ } elseif ($offset = (int) $offset) {
+ if ($offset < 0) {
+ if (0 > $offset += self::mb_strlen($needle)) {
+ $haystack = self::mb_substr($haystack, 0, $offset, $encoding);
+ }
+ $offset = 0;
+ } else {
+ $haystack = self::mb_substr($haystack, $offset, 2147483647, $encoding);
+ }
+ }
+
+ $pos = '' !== $needle || 80000 > \PHP_VERSION_ID
+ ? iconv_strrpos($haystack, $needle, $encoding)
+ : self::mb_strlen($haystack, $encoding);
+
+ return false !== $pos ? $offset + $pos : false;
+ }
+
+ public static function mb_str_split($string, $split_length = 1, $encoding = null)
+ {
+ if (null !== $string && !\is_scalar($string) && !(\is_object($string) && method_exists($string, '__toString'))) {
+ trigger_error('mb_str_split() expects parameter 1 to be string, '.\gettype($string).' given', \E_USER_WARNING);
+
+ return null;
+ }
+
+ if (1 > $split_length = (int) $split_length) {
+ if (80000 > \PHP_VERSION_ID) {
+ trigger_error('The length of each segment must be greater than zero', \E_USER_WARNING);
+
+ return false;
+ }
+
+ throw new \ValueError('Argument #2 ($length) must be greater than 0');
+ }
+
+ if (null === $encoding) {
+ $encoding = mb_internal_encoding();
+ }
+
+ if ('UTF-8' === $encoding = self::getEncoding($encoding)) {
+ $rx = '/(';
+ while (65535 < $split_length) {
+ $rx .= '.{65535}';
+ $split_length -= 65535;
+ }
+ $rx .= '.{'.$split_length.'})/us';
+
+ return preg_split($rx, $string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY);
+ }
+
+ $result = [];
+ $length = mb_strlen($string, $encoding);
+
+ for ($i = 0; $i < $length; $i += $split_length) {
+ $result[] = mb_substr($string, $i, $split_length, $encoding);
+ }
+
+ return $result;
+ }
+
+ public static function mb_strtolower($s, $encoding = null)
+ {
+ return self::mb_convert_case($s, \MB_CASE_LOWER, $encoding);
+ }
+
+ public static function mb_strtoupper($s, $encoding = null)
+ {
+ return self::mb_convert_case($s, \MB_CASE_UPPER, $encoding);
+ }
+
+ public static function mb_substitute_character($c = null)
+ {
+ if (null === $c) {
+ return 'none';
+ }
+ if (0 === strcasecmp($c, 'none')) {
+ return true;
+ }
+ 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');
+ }
+
+ public static function mb_substr($s, $start, $length = null, $encoding = null)
+ {
+ $encoding = self::getEncoding($encoding);
+ if ('CP850' === $encoding || 'ASCII' === $encoding) {
+ return (string) substr($s, $start, null === $length ? 2147483647 : $length);
+ }
+
+ if ($start < 0) {
+ $start = iconv_strlen($s, $encoding) + $start;
+ if ($start < 0) {
+ $start = 0;
+ }
+ }
+
+ if (null === $length) {
+ $length = 2147483647;
+ } elseif ($length < 0) {
+ $length = iconv_strlen($s, $encoding) + $length - $start;
+ if ($length < 0) {
+ return '';
+ }
+ }
+
+ return (string) iconv_substr($s, $start, $length, $encoding);
+ }
+
+ public static function mb_stripos($haystack, $needle, $offset = 0, $encoding = null)
+ {
+ [$haystack, $needle] = str_replace(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1], [
+ self::mb_convert_case($haystack, \MB_CASE_LOWER, $encoding),
+ self::mb_convert_case($needle, \MB_CASE_LOWER, $encoding),
+ ]);
+
+ return self::mb_strpos($haystack, $needle, $offset, $encoding);
+ }
+
+ public static function mb_stristr($haystack, $needle, $part = false, $encoding = null)
+ {
+ $pos = self::mb_stripos($haystack, $needle, 0, $encoding);
+
+ return self::getSubpart($pos, $part, $haystack, $encoding);
+ }
+
+ public static function mb_strrchr($haystack, $needle, $part = false, $encoding = null)
+ {
+ $encoding = self::getEncoding($encoding);
+ if ('CP850' === $encoding || 'ASCII' === $encoding) {
+ $pos = strrpos($haystack, $needle);
+ } else {
+ $needle = self::mb_substr($needle, 0, 1, $encoding);
+ $pos = iconv_strrpos($haystack, $needle, $encoding);
+ }
+
+ return self::getSubpart($pos, $part, $haystack, $encoding);
+ }
+
+ public static function mb_strrichr($haystack, $needle, $part = false, $encoding = null)
+ {
+ $needle = self::mb_substr($needle, 0, 1, $encoding);
+ $pos = self::mb_strripos($haystack, $needle, $encoding);
+
+ return self::getSubpart($pos, $part, $haystack, $encoding);
+ }
+
+ public static function mb_strripos($haystack, $needle, $offset = 0, $encoding = null)
+ {
+ $haystack = self::mb_convert_case($haystack, \MB_CASE_LOWER, $encoding);
+ $needle = self::mb_convert_case($needle, \MB_CASE_LOWER, $encoding);
+
+ $haystack = str_replace(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1], $haystack);
+ $needle = str_replace(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1], $needle);
+
+ return self::mb_strrpos($haystack, $needle, $offset, $encoding);
+ }
+
+ public static function mb_strstr($haystack, $needle, $part = false, $encoding = null)
+ {
+ $pos = strpos($haystack, $needle);
+ if (false === $pos) {
+ return false;
+ }
+ if ($part) {
+ return substr($haystack, 0, $pos);
+ }
+
+ return substr($haystack, $pos);
+ }
+
+ public static function mb_get_info($type = 'all')
+ {
+ $info = [
+ 'internal_encoding' => self::$internalEncoding,
+ 'http_output' => 'pass',
+ 'http_output_conv_mimetypes' => '^(text/|application/xhtml\+xml)',
+ 'func_overload' => 0,
+ 'func_overload_list' => 'no overload',
+ 'mail_charset' => 'UTF-8',
+ 'mail_header_encoding' => 'BASE64',
+ 'mail_body_encoding' => 'BASE64',
+ 'illegal_chars' => 0,
+ 'encoding_translation' => 'Off',
+ 'language' => self::$language,
+ 'detect_order' => self::$encodingList,
+ 'substitute_character' => 'none',
+ 'strict_detection' => 'Off',
+ ];
+
+ if ('all' === $type) {
+ return $info;
+ }
+ if (isset($info[$type])) {
+ return $info[$type];
+ }
+
+ return false;
+ }
+
+ public static function mb_http_input($type = '')
+ {
+ return false;
+ }
+
+ public static function mb_http_output($encoding = null)
+ {
+ return null !== $encoding ? 'pass' === $encoding : 'pass';
+ }
+
+ public static function mb_strwidth($s, $encoding = null)
+ {
+ $encoding = self::getEncoding($encoding);
+
+ if ('UTF-8' !== $encoding) {
+ $s = iconv($encoding, 'UTF-8//IGNORE', $s);
+ }
+
+ $s = preg_replace('/[\x{1100}-\x{115F}\x{2329}\x{232A}\x{2E80}-\x{303E}\x{3040}-\x{A4CF}\x{AC00}-\x{D7A3}\x{F900}-\x{FAFF}\x{FE10}-\x{FE19}\x{FE30}-\x{FE6F}\x{FF00}-\x{FF60}\x{FFE0}-\x{FFE6}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}]/u', '', $s, -1, $wide);
+
+ return ($wide << 1) + iconv_strlen($s, 'UTF-8');
+ }
+
+ public static function mb_substr_count($haystack, $needle, $encoding = null)
+ {
+ return substr_count($haystack, $needle);
+ }
+
+ public static function mb_output_handler($contents, $status)
+ {
+ return $contents;
+ }
+
+ public static function mb_chr($code, $encoding = null)
+ {
+ if (0x80 > $code %= 0x200000) {
+ $s = \chr($code);
+ } elseif (0x800 > $code) {
+ $s = \chr(0xC0 | $code >> 6).\chr(0x80 | $code & 0x3F);
+ } elseif (0x10000 > $code) {
+ $s = \chr(0xE0 | $code >> 12).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F);
+ } else {
+ $s = \chr(0xF0 | $code >> 18).\chr(0x80 | $code >> 12 & 0x3F).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F);
+ }
+
+ if ('UTF-8' !== $encoding = self::getEncoding($encoding)) {
+ $s = mb_convert_encoding($s, $encoding, 'UTF-8');
+ }
+
+ return $s;
+ }
+
+ public static function mb_ord($s, $encoding = null)
+ {
+ if ('UTF-8' !== $encoding = self::getEncoding($encoding)) {
+ $s = mb_convert_encoding($s, 'UTF-8', $encoding);
+ }
+
+ if (1 === \strlen($s)) {
+ return \ord($s);
+ }
+
+ $code = ($s = unpack('C*', substr($s, 0, 4))) ? $s[1] : 0;
+ if (0xF0 <= $code) {
+ return (($code - 0xF0) << 18) + (($s[2] - 0x80) << 12) + (($s[3] - 0x80) << 6) + $s[4] - 0x80;
+ }
+ if (0xE0 <= $code) {
+ return (($code - 0xE0) << 12) + (($s[2] - 0x80) << 6) + $s[3] - 0x80;
+ }
+ if (0xC0 <= $code) {
+ return (($code - 0xC0) << 6) + $s[2] - 0x80;
+ }
+
+ return $code;
+ }
+
+ public static function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = \STR_PAD_RIGHT, ?string $encoding = null): string
+ {
+ if (!\in_array($pad_type, [\STR_PAD_RIGHT, \STR_PAD_LEFT, \STR_PAD_BOTH], true)) {
+ throw new \ValueError('mb_str_pad(): Argument #4 ($pad_type) must be STR_PAD_LEFT, STR_PAD_RIGHT, or STR_PAD_BOTH');
+ }
+
+ if (null === $encoding) {
+ $encoding = self::mb_internal_encoding();
+ } else {
+ self::assertEncoding($encoding, 'mb_str_pad(): Argument #5 ($encoding) must be a valid encoding, "%s" given');
+ }
+
+ if (self::mb_strlen($pad_string, $encoding) <= 0) {
+ throw new \ValueError('mb_str_pad(): Argument #3 ($pad_string) must be a non-empty string');
+ }
+
+ $paddingRequired = $length - self::mb_strlen($string, $encoding);
+
+ if ($paddingRequired < 1) {
+ return $string;
+ }
+
+ switch ($pad_type) {
+ case \STR_PAD_LEFT:
+ return self::mb_substr(str_repeat($pad_string, $paddingRequired), 0, $paddingRequired, $encoding).$string;
+ case \STR_PAD_RIGHT:
+ return $string.self::mb_substr(str_repeat($pad_string, $paddingRequired), 0, $paddingRequired, $encoding);
+ default:
+ $leftPaddingLength = floor($paddingRequired / 2);
+ $rightPaddingLength = $paddingRequired - $leftPaddingLength;
+
+ return self::mb_substr(str_repeat($pad_string, $leftPaddingLength), 0, $leftPaddingLength, $encoding).$string.self::mb_substr(str_repeat($pad_string, $rightPaddingLength), 0, $rightPaddingLength, $encoding);
+ }
+ }
+
+ public static function mb_ucfirst(string $string, ?string $encoding = null): string
+ {
+ if (null === $encoding) {
+ $encoding = self::mb_internal_encoding();
+ } else {
+ self::assertEncoding($encoding, 'mb_ucfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given');
+ }
+
+ $firstChar = mb_substr($string, 0, 1, $encoding);
+ $firstChar = mb_convert_case($firstChar, \MB_CASE_TITLE, $encoding);
+
+ return $firstChar.mb_substr($string, 1, null, $encoding);
+ }
+
+ public static function mb_lcfirst(string $string, ?string $encoding = null): string
+ {
+ if (null === $encoding) {
+ $encoding = self::mb_internal_encoding();
+ } else {
+ self::assertEncoding($encoding, 'mb_lcfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given');
+ }
+
+ $firstChar = mb_substr($string, 0, 1, $encoding);
+ $firstChar = mb_convert_case($firstChar, \MB_CASE_LOWER, $encoding);
+
+ return $firstChar.mb_substr($string, 1, null, $encoding);
+ }
+
+ private static function getSubpart($pos, $part, $haystack, $encoding)
+ {
+ if (false === $pos) {
+ return false;
+ }
+ if ($part) {
+ return self::mb_substr($haystack, 0, $pos, $encoding);
+ }
+
+ return self::mb_substr($haystack, $pos, null, $encoding);
+ }
+
+ private static function html_encoding_callback(array $m)
+ {
+ $i = 1;
+ $entities = '';
+ $m = unpack('C*', htmlentities($m[0], \ENT_COMPAT, 'UTF-8'));
+
+ while (isset($m[$i])) {
+ if (0x80 > $m[$i]) {
+ $entities .= \chr($m[$i++]);
+ continue;
+ }
+ if (0xF0 <= $m[$i]) {
+ $c = (($m[$i++] - 0xF0) << 18) + (($m[$i++] - 0x80) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80;
+ } elseif (0xE0 <= $m[$i]) {
+ $c = (($m[$i++] - 0xE0) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80;
+ } else {
+ $c = (($m[$i++] - 0xC0) << 6) + $m[$i++] - 0x80;
+ }
+
+ $entities .= ''.$c.';';
+ }
+
+ return $entities;
+ }
+
+ private static function title_case(array $s)
+ {
+ return self::mb_convert_case($s[1], \MB_CASE_UPPER, 'UTF-8').self::mb_convert_case($s[2], \MB_CASE_LOWER, 'UTF-8');
+ }
+
+ private static function getData($file)
+ {
+ if (file_exists($file = __DIR__.'/Resources/unidata/'.$file.'.php')) {
+ return require $file;
+ }
+
+ return false;
+ }
+
+ private static function getEncoding($encoding)
+ {
+ if (null === $encoding) {
+ return self::$internalEncoding;
+ }
+
+ if ('UTF-8' === $encoding) {
+ return 'UTF-8';
+ }
+
+ $encoding = strtoupper($encoding);
+
+ if ('8BIT' === $encoding || 'BINARY' === $encoding) {
+ return 'CP850';
+ }
+
+ if ('UTF8' === $encoding) {
+ return 'UTF-8';
+ }
+
+ return $encoding;
+ }
+
+ public static function mb_trim(string $string, ?string $characters = null, ?string $encoding = null): string
+ {
+ return self::mb_internal_trim('{^[%s]+|[%1$s]+$}Du', $string, $characters, $encoding, __FUNCTION__);
+ }
+
+ public static function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null): string
+ {
+ return self::mb_internal_trim('{^[%s]+}Du', $string, $characters, $encoding, __FUNCTION__);
+ }
+
+ public static function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null): string
+ {
+ return self::mb_internal_trim('{[%s]+$}D', $string, $characters, $encoding, __FUNCTION__);
+ }
+
+ private static function mb_internal_trim(string $regex, string $string, ?string $characters, ?string $encoding, string $function): string
+ {
+ if (null === $encoding) {
+ $encoding = self::mb_internal_encoding();
+ } else {
+ self::assertEncoding($encoding, $function.'(): Argument #3 ($encoding) must be a valid encoding, "%s" given');
+ }
+
+ if ('' === $characters) {
+ return null === $encoding ? $string : self::mb_convert_encoding($string, $encoding);
+ }
+
+ if ('UTF-8' === $encoding) {
+ $encoding = null;
+ if (!preg_match('//u', $string)) {
+ $string = @iconv('UTF-8', 'UTF-8//IGNORE', $string);
+ }
+ if (null !== $characters && !preg_match('//u', $characters)) {
+ $characters = @iconv('UTF-8', 'UTF-8//IGNORE', $characters);
+ }
+ } else {
+ $string = iconv($encoding, 'UTF-8//IGNORE', $string);
+
+ if (null !== $characters) {
+ $characters = iconv($encoding, 'UTF-8//IGNORE', $characters);
+ }
+ }
+
+ if (null === $characters) {
+ $characters = "\\0 \f\n\r\t\v\u{00A0}\u{1680}\u{2000}\u{2001}\u{2002}\u{2003}\u{2004}\u{2005}\u{2006}\u{2007}\u{2008}\u{2009}\u{200A}\u{2028}\u{2029}\u{202F}\u{205F}\u{3000}\u{0085}\u{180E}";
+ } else {
+ $characters = preg_quote($characters);
+ }
+
+ $string = preg_replace(sprintf($regex, $characters), '', $string);
+
+ if (null === $encoding) {
+ return $string;
+ }
+
+ return iconv('UTF-8', $encoding.'//IGNORE', $string);
+ }
+
+ private static function assertEncoding(string $encoding, string $errorFormat): void
+ {
+ try {
+ $validEncoding = @self::mb_check_encoding('', $encoding);
+ } catch (\ValueError $e) {
+ throw new \ValueError(sprintf($errorFormat, $encoding));
+ }
+
+ // BC for PHP 7.3 and lower
+ if (!$validEncoding) {
+ throw new \ValueError(sprintf($errorFormat, $encoding));
+ }
+ }
+}
diff --git a/vendor/symfony/polyfill-mbstring/README.md b/vendor/symfony/polyfill-mbstring/README.md
new file mode 100644
index 0000000..478b40d
--- /dev/null
+++ b/vendor/symfony/polyfill-mbstring/README.md
@@ -0,0 +1,13 @@
+Symfony Polyfill / Mbstring
+===========================
+
+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/main/README.md).
+
+License
+=======
+
+This library is released under the [MIT license](LICENSE).
diff --git a/vendor/symfony/polyfill-mbstring/Resources/unidata/caseFolding.php b/vendor/symfony/polyfill-mbstring/Resources/unidata/caseFolding.php
new file mode 100644
index 0000000..512bba0
--- /dev/null
+++ b/vendor/symfony/polyfill-mbstring/Resources/unidata/caseFolding.php
@@ -0,0 +1,119 @@
+ 'i̇',
+ 'µ' => 'μ',
+ 'ſ' => 's',
+ 'ͅ' => 'ι',
+ 'ς' => 'σ',
+ 'ϐ' => 'β',
+ 'ϑ' => 'θ',
+ 'ϕ' => 'φ',
+ 'ϖ' => 'π',
+ 'ϰ' => 'κ',
+ 'ϱ' => 'ρ',
+ 'ϵ' => 'ε',
+ 'ẛ' => 'ṡ',
+ 'ι' => 'ι',
+ 'ß' => 'ss',
+ 'ʼn' => 'ʼn',
+ 'ǰ' => 'ǰ',
+ 'ΐ' => 'ΐ',
+ 'ΰ' => 'ΰ',
+ 'և' => 'եւ',
+ 'ẖ' => 'ẖ',
+ 'ẗ' => 'ẗ',
+ 'ẘ' => 'ẘ',
+ 'ẙ' => 'ẙ',
+ 'ẚ' => 'aʾ',
+ 'ẞ' => 'ss',
+ 'ὐ' => 'ὐ',
+ 'ὒ' => 'ὒ',
+ 'ὔ' => 'ὔ',
+ 'ὖ' => 'ὖ',
+ 'ᾀ' => 'ἀι',
+ 'ᾁ' => 'ἁι',
+ 'ᾂ' => 'ἂι',
+ 'ᾃ' => 'ἃι',
+ 'ᾄ' => 'ἄι',
+ 'ᾅ' => 'ἅι',
+ 'ᾆ' => 'ἆι',
+ 'ᾇ' => 'ἇι',
+ 'ᾈ' => 'ἀι',
+ 'ᾉ' => 'ἁι',
+ 'ᾊ' => 'ἂι',
+ 'ᾋ' => 'ἃι',
+ 'ᾌ' => 'ἄι',
+ 'ᾍ' => 'ἅι',
+ 'ᾎ' => 'ἆι',
+ 'ᾏ' => 'ἇι',
+ 'ᾐ' => 'ἠι',
+ 'ᾑ' => 'ἡι',
+ 'ᾒ' => 'ἢι',
+ 'ᾓ' => 'ἣι',
+ 'ᾔ' => 'ἤι',
+ 'ᾕ' => 'ἥι',
+ 'ᾖ' => 'ἦι',
+ 'ᾗ' => 'ἧι',
+ 'ᾘ' => 'ἠι',
+ 'ᾙ' => 'ἡι',
+ 'ᾚ' => 'ἢι',
+ 'ᾛ' => 'ἣι',
+ 'ᾜ' => 'ἤι',
+ 'ᾝ' => 'ἥι',
+ 'ᾞ' => 'ἦι',
+ 'ᾟ' => 'ἧι',
+ 'ᾠ' => 'ὠι',
+ 'ᾡ' => 'ὡι',
+ 'ᾢ' => 'ὢι',
+ 'ᾣ' => 'ὣι',
+ 'ᾤ' => 'ὤι',
+ 'ᾥ' => 'ὥι',
+ 'ᾦ' => 'ὦι',
+ 'ᾧ' => 'ὧι',
+ 'ᾨ' => 'ὠι',
+ 'ᾩ' => 'ὡι',
+ 'ᾪ' => 'ὢι',
+ 'ᾫ' => 'ὣι',
+ 'ᾬ' => 'ὤι',
+ 'ᾭ' => 'ὥι',
+ 'ᾮ' => 'ὦι',
+ 'ᾯ' => 'ὧι',
+ 'ᾲ' => 'ὰι',
+ 'ᾳ' => 'αι',
+ 'ᾴ' => 'άι',
+ 'ᾶ' => 'ᾶ',
+ 'ᾷ' => 'ᾶι',
+ 'ᾼ' => 'αι',
+ 'ῂ' => 'ὴι',
+ 'ῃ' => 'ηι',
+ 'ῄ' => 'ήι',
+ 'ῆ' => 'ῆ',
+ 'ῇ' => 'ῆι',
+ 'ῌ' => 'ηι',
+ 'ῒ' => 'ῒ',
+ 'ῖ' => 'ῖ',
+ 'ῗ' => 'ῗ',
+ 'ῢ' => 'ῢ',
+ 'ῤ' => 'ῤ',
+ 'ῦ' => 'ῦ',
+ 'ῧ' => 'ῧ',
+ 'ῲ' => 'ὼι',
+ 'ῳ' => 'ωι',
+ 'ῴ' => 'ώι',
+ 'ῶ' => 'ῶ',
+ 'ῷ' => 'ῶι',
+ 'ῼ' => 'ωι',
+ 'ff' => 'ff',
+ 'fi' => 'fi',
+ 'fl' => 'fl',
+ 'ffi' => 'ffi',
+ 'ffl' => 'ffl',
+ 'ſt' => 'st',
+ 'st' => 'st',
+ 'ﬓ' => 'մն',
+ 'ﬔ' => 'մե',
+ 'ﬕ' => 'մի',
+ 'ﬖ' => 'վն',
+ 'ﬗ' => 'մխ',
+];
diff --git a/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php b/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php
new file mode 100644
index 0000000..fac60b0
--- /dev/null
+++ b/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php
@@ -0,0 +1,1397 @@
+ 'a',
+ 'B' => 'b',
+ 'C' => 'c',
+ 'D' => 'd',
+ 'E' => 'e',
+ 'F' => 'f',
+ 'G' => 'g',
+ 'H' => 'h',
+ 'I' => 'i',
+ 'J' => 'j',
+ 'K' => 'k',
+ 'L' => 'l',
+ 'M' => 'm',
+ 'N' => 'n',
+ 'O' => 'o',
+ 'P' => 'p',
+ 'Q' => 'q',
+ 'R' => 'r',
+ 'S' => 's',
+ 'T' => 't',
+ 'U' => 'u',
+ 'V' => 'v',
+ 'W' => 'w',
+ 'X' => 'x',
+ 'Y' => 'y',
+ 'Z' => 'z',
+ 'À' => 'à',
+ 'Á' => 'á',
+ 'Â' => 'â',
+ 'Ã' => 'ã',
+ 'Ä' => 'ä',
+ 'Å' => 'å',
+ 'Æ' => 'æ',
+ 'Ç' => 'ç',
+ 'È' => 'è',
+ 'É' => 'é',
+ 'Ê' => 'ê',
+ 'Ë' => 'ë',
+ 'Ì' => 'ì',
+ 'Í' => 'í',
+ 'Î' => 'î',
+ 'Ï' => 'ï',
+ 'Ð' => 'ð',
+ 'Ñ' => 'ñ',
+ 'Ò' => 'ò',
+ 'Ó' => 'ó',
+ 'Ô' => 'ô',
+ 'Õ' => 'õ',
+ 'Ö' => 'ö',
+ 'Ø' => 'ø',
+ 'Ù' => 'ù',
+ 'Ú' => 'ú',
+ 'Û' => 'û',
+ 'Ü' => 'ü',
+ 'Ý' => 'ý',
+ 'Þ' => 'þ',
+ 'Ā' => 'ā',
+ 'Ă' => 'ă',
+ 'Ą' => 'ą',
+ 'Ć' => 'ć',
+ 'Ĉ' => 'ĉ',
+ 'Ċ' => 'ċ',
+ 'Č' => 'č',
+ 'Ď' => 'ď',
+ 'Đ' => 'đ',
+ 'Ē' => 'ē',
+ 'Ĕ' => 'ĕ',
+ 'Ė' => 'ė',
+ 'Ę' => 'ę',
+ 'Ě' => 'ě',
+ 'Ĝ' => 'ĝ',
+ 'Ğ' => 'ğ',
+ 'Ġ' => 'ġ',
+ 'Ģ' => 'ģ',
+ 'Ĥ' => 'ĥ',
+ 'Ħ' => 'ħ',
+ 'Ĩ' => 'ĩ',
+ 'Ī' => 'ī',
+ 'Ĭ' => 'ĭ',
+ 'Į' => 'į',
+ 'İ' => 'i̇',
+ 'IJ' => 'ij',
+ 'Ĵ' => 'ĵ',
+ 'Ķ' => 'ķ',
+ 'Ĺ' => 'ĺ',
+ 'Ļ' => 'ļ',
+ 'Ľ' => 'ľ',
+ 'Ŀ' => 'ŀ',
+ 'Ł' => 'ł',
+ 'Ń' => 'ń',
+ 'Ņ' => 'ņ',
+ 'Ň' => 'ň',
+ 'Ŋ' => 'ŋ',
+ 'Ō' => 'ō',
+ 'Ŏ' => 'ŏ',
+ 'Ő' => 'ő',
+ 'Œ' => 'œ',
+ 'Ŕ' => 'ŕ',
+ 'Ŗ' => 'ŗ',
+ 'Ř' => 'ř',
+ 'Ś' => 'ś',
+ 'Ŝ' => 'ŝ',
+ 'Ş' => 'ş',
+ 'Š' => 'š',
+ 'Ţ' => 'ţ',
+ 'Ť' => 'ť',
+ 'Ŧ' => 'ŧ',
+ 'Ũ' => 'ũ',
+ 'Ū' => 'ū',
+ 'Ŭ' => 'ŭ',
+ 'Ů' => 'ů',
+ 'Ű' => 'ű',
+ 'Ų' => 'ų',
+ 'Ŵ' => 'ŵ',
+ 'Ŷ' => 'ŷ',
+ 'Ÿ' => 'ÿ',
+ 'Ź' => 'ź',
+ 'Ż' => 'ż',
+ 'Ž' => 'ž',
+ 'Ɓ' => 'ɓ',
+ 'Ƃ' => 'ƃ',
+ 'Ƅ' => 'ƅ',
+ 'Ɔ' => 'ɔ',
+ 'Ƈ' => 'ƈ',
+ 'Ɖ' => 'ɖ',
+ 'Ɗ' => 'ɗ',
+ 'Ƌ' => 'ƌ',
+ 'Ǝ' => 'ǝ',
+ 'Ə' => 'ə',
+ 'Ɛ' => 'ɛ',
+ 'Ƒ' => 'ƒ',
+ 'Ɠ' => 'ɠ',
+ 'Ɣ' => 'ɣ',
+ 'Ɩ' => 'ɩ',
+ 'Ɨ' => 'ɨ',
+ 'Ƙ' => 'ƙ',
+ 'Ɯ' => 'ɯ',
+ 'Ɲ' => 'ɲ',
+ 'Ɵ' => 'ɵ',
+ 'Ơ' => 'ơ',
+ 'Ƣ' => 'ƣ',
+ 'Ƥ' => 'ƥ',
+ 'Ʀ' => 'ʀ',
+ 'Ƨ' => 'ƨ',
+ 'Ʃ' => 'ʃ',
+ 'Ƭ' => 'ƭ',
+ 'Ʈ' => 'ʈ',
+ 'Ư' => 'ư',
+ 'Ʊ' => 'ʊ',
+ 'Ʋ' => 'ʋ',
+ 'Ƴ' => 'ƴ',
+ 'Ƶ' => 'ƶ',
+ 'Ʒ' => 'ʒ',
+ 'Ƹ' => 'ƹ',
+ 'Ƽ' => 'ƽ',
+ 'DŽ' => 'dž',
+ 'Dž' => 'dž',
+ 'LJ' => 'lj',
+ 'Lj' => 'lj',
+ 'NJ' => 'nj',
+ 'Nj' => 'nj',
+ 'Ǎ' => 'ǎ',
+ 'Ǐ' => 'ǐ',
+ 'Ǒ' => 'ǒ',
+ 'Ǔ' => 'ǔ',
+ 'Ǖ' => 'ǖ',
+ 'Ǘ' => 'ǘ',
+ 'Ǚ' => 'ǚ',
+ 'Ǜ' => 'ǜ',
+ 'Ǟ' => 'ǟ',
+ 'Ǡ' => 'ǡ',
+ 'Ǣ' => 'ǣ',
+ 'Ǥ' => 'ǥ',
+ 'Ǧ' => 'ǧ',
+ 'Ǩ' => 'ǩ',
+ 'Ǫ' => 'ǫ',
+ 'Ǭ' => 'ǭ',
+ 'Ǯ' => 'ǯ',
+ 'DZ' => 'dz',
+ 'Dz' => 'dz',
+ 'Ǵ' => 'ǵ',
+ 'Ƕ' => 'ƕ',
+ 'Ƿ' => 'ƿ',
+ 'Ǹ' => 'ǹ',
+ 'Ǻ' => 'ǻ',
+ 'Ǽ' => 'ǽ',
+ 'Ǿ' => 'ǿ',
+ 'Ȁ' => 'ȁ',
+ 'Ȃ' => 'ȃ',
+ 'Ȅ' => 'ȅ',
+ 'Ȇ' => 'ȇ',
+ 'Ȉ' => 'ȉ',
+ 'Ȋ' => 'ȋ',
+ 'Ȍ' => 'ȍ',
+ 'Ȏ' => 'ȏ',
+ 'Ȑ' => 'ȑ',
+ 'Ȓ' => 'ȓ',
+ 'Ȕ' => 'ȕ',
+ 'Ȗ' => 'ȗ',
+ 'Ș' => 'ș',
+ 'Ț' => 'ț',
+ 'Ȝ' => 'ȝ',
+ 'Ȟ' => 'ȟ',
+ 'Ƞ' => 'ƞ',
+ 'Ȣ' => 'ȣ',
+ 'Ȥ' => 'ȥ',
+ 'Ȧ' => 'ȧ',
+ 'Ȩ' => 'ȩ',
+ 'Ȫ' => 'ȫ',
+ 'Ȭ' => 'ȭ',
+ 'Ȯ' => 'ȯ',
+ 'Ȱ' => 'ȱ',
+ 'Ȳ' => 'ȳ',
+ 'Ⱥ' => 'ⱥ',
+ 'Ȼ' => 'ȼ',
+ 'Ƚ' => 'ƚ',
+ 'Ⱦ' => 'ⱦ',
+ 'Ɂ' => 'ɂ',
+ 'Ƀ' => 'ƀ',
+ 'Ʉ' => 'ʉ',
+ 'Ʌ' => 'ʌ',
+ 'Ɇ' => 'ɇ',
+ 'Ɉ' => 'ɉ',
+ 'Ɋ' => 'ɋ',
+ 'Ɍ' => 'ɍ',
+ 'Ɏ' => 'ɏ',
+ 'Ͱ' => 'ͱ',
+ 'Ͳ' => 'ͳ',
+ 'Ͷ' => 'ͷ',
+ 'Ϳ' => 'ϳ',
+ 'Ά' => 'ά',
+ 'Έ' => 'έ',
+ 'Ή' => 'ή',
+ 'Ί' => 'ί',
+ 'Ό' => 'ό',
+ 'Ύ' => 'ύ',
+ 'Ώ' => 'ώ',
+ 'Α' => 'α',
+ 'Β' => 'β',
+ 'Γ' => 'γ',
+ 'Δ' => 'δ',
+ 'Ε' => 'ε',
+ 'Ζ' => 'ζ',
+ 'Η' => 'η',
+ 'Θ' => 'θ',
+ 'Ι' => 'ι',
+ 'Κ' => 'κ',
+ 'Λ' => 'λ',
+ 'Μ' => 'μ',
+ 'Ν' => 'ν',
+ 'Ξ' => 'ξ',
+ 'Ο' => 'ο',
+ 'Π' => 'π',
+ 'Ρ' => 'ρ',
+ 'Σ' => 'σ',
+ 'Τ' => 'τ',
+ 'Υ' => 'υ',
+ 'Φ' => 'φ',
+ 'Χ' => 'χ',
+ 'Ψ' => 'ψ',
+ 'Ω' => 'ω',
+ 'Ϊ' => 'ϊ',
+ 'Ϋ' => 'ϋ',
+ 'Ϗ' => 'ϗ',
+ 'Ϙ' => 'ϙ',
+ 'Ϛ' => 'ϛ',
+ 'Ϝ' => 'ϝ',
+ 'Ϟ' => 'ϟ',
+ 'Ϡ' => 'ϡ',
+ 'Ϣ' => 'ϣ',
+ 'Ϥ' => 'ϥ',
+ 'Ϧ' => 'ϧ',
+ 'Ϩ' => 'ϩ',
+ 'Ϫ' => 'ϫ',
+ 'Ϭ' => 'ϭ',
+ 'Ϯ' => 'ϯ',
+ 'ϴ' => 'θ',
+ 'Ϸ' => 'ϸ',
+ 'Ϲ' => 'ϲ',
+ 'Ϻ' => 'ϻ',
+ 'Ͻ' => 'ͻ',
+ 'Ͼ' => 'ͼ',
+ 'Ͽ' => 'ͽ',
+ 'Ѐ' => 'ѐ',
+ 'Ё' => 'ё',
+ 'Ђ' => 'ђ',
+ 'Ѓ' => 'ѓ',
+ 'Є' => 'є',
+ 'Ѕ' => 'ѕ',
+ 'І' => 'і',
+ 'Ї' => 'ї',
+ 'Ј' => 'ј',
+ 'Љ' => 'љ',
+ 'Њ' => 'њ',
+ 'Ћ' => 'ћ',
+ 'Ќ' => 'ќ',
+ 'Ѝ' => 'ѝ',
+ 'Ў' => 'ў',
+ 'Џ' => 'џ',
+ 'А' => 'а',
+ 'Б' => 'б',
+ 'В' => 'в',
+ 'Г' => 'г',
+ 'Д' => 'д',
+ 'Е' => 'е',
+ 'Ж' => 'ж',
+ 'З' => 'з',
+ 'И' => 'и',
+ 'Й' => 'й',
+ 'К' => 'к',
+ 'Л' => 'л',
+ 'М' => 'м',
+ 'Н' => 'н',
+ 'О' => 'о',
+ 'П' => 'п',
+ 'Р' => 'р',
+ 'С' => 'с',
+ 'Т' => 'т',
+ 'У' => 'у',
+ 'Ф' => 'ф',
+ 'Х' => 'х',
+ 'Ц' => 'ц',
+ 'Ч' => 'ч',
+ 'Ш' => 'ш',
+ 'Щ' => 'щ',
+ 'Ъ' => 'ъ',
+ 'Ы' => 'ы',
+ 'Ь' => 'ь',
+ 'Э' => 'э',
+ 'Ю' => 'ю',
+ 'Я' => 'я',
+ 'Ѡ' => 'ѡ',
+ 'Ѣ' => 'ѣ',
+ 'Ѥ' => 'ѥ',
+ 'Ѧ' => 'ѧ',
+ 'Ѩ' => 'ѩ',
+ 'Ѫ' => 'ѫ',
+ 'Ѭ' => 'ѭ',
+ 'Ѯ' => 'ѯ',
+ 'Ѱ' => 'ѱ',
+ 'Ѳ' => 'ѳ',
+ 'Ѵ' => 'ѵ',
+ 'Ѷ' => 'ѷ',
+ 'Ѹ' => 'ѹ',
+ 'Ѻ' => 'ѻ',
+ 'Ѽ' => 'ѽ',
+ 'Ѿ' => 'ѿ',
+ 'Ҁ' => 'ҁ',
+ 'Ҋ' => 'ҋ',
+ 'Ҍ' => 'ҍ',
+ 'Ҏ' => 'ҏ',
+ 'Ґ' => 'ґ',
+ 'Ғ' => 'ғ',
+ 'Ҕ' => 'ҕ',
+ 'Җ' => 'җ',
+ 'Ҙ' => 'ҙ',
+ 'Қ' => 'қ',
+ 'Ҝ' => 'ҝ',
+ 'Ҟ' => 'ҟ',
+ 'Ҡ' => 'ҡ',
+ 'Ң' => 'ң',
+ 'Ҥ' => 'ҥ',
+ 'Ҧ' => 'ҧ',
+ 'Ҩ' => 'ҩ',
+ 'Ҫ' => 'ҫ',
+ 'Ҭ' => 'ҭ',
+ 'Ү' => 'ү',
+ 'Ұ' => 'ұ',
+ 'Ҳ' => 'ҳ',
+ 'Ҵ' => 'ҵ',
+ 'Ҷ' => 'ҷ',
+ 'Ҹ' => 'ҹ',
+ 'Һ' => 'һ',
+ 'Ҽ' => 'ҽ',
+ 'Ҿ' => 'ҿ',
+ 'Ӏ' => 'ӏ',
+ 'Ӂ' => 'ӂ',
+ 'Ӄ' => 'ӄ',
+ 'Ӆ' => 'ӆ',
+ 'Ӈ' => 'ӈ',
+ 'Ӊ' => 'ӊ',
+ 'Ӌ' => 'ӌ',
+ 'Ӎ' => 'ӎ',
+ 'Ӑ' => 'ӑ',
+ 'Ӓ' => 'ӓ',
+ 'Ӕ' => 'ӕ',
+ 'Ӗ' => 'ӗ',
+ 'Ә' => 'ә',
+ 'Ӛ' => 'ӛ',
+ 'Ӝ' => 'ӝ',
+ 'Ӟ' => 'ӟ',
+ 'Ӡ' => 'ӡ',
+ 'Ӣ' => 'ӣ',
+ 'Ӥ' => 'ӥ',
+ 'Ӧ' => 'ӧ',
+ 'Ө' => 'ө',
+ 'Ӫ' => 'ӫ',
+ 'Ӭ' => 'ӭ',
+ 'Ӯ' => 'ӯ',
+ 'Ӱ' => 'ӱ',
+ 'Ӳ' => 'ӳ',
+ 'Ӵ' => 'ӵ',
+ 'Ӷ' => 'ӷ',
+ 'Ӹ' => 'ӹ',
+ 'Ӻ' => 'ӻ',
+ 'Ӽ' => 'ӽ',
+ 'Ӿ' => 'ӿ',
+ 'Ԁ' => 'ԁ',
+ 'Ԃ' => 'ԃ',
+ 'Ԅ' => 'ԅ',
+ 'Ԇ' => 'ԇ',
+ 'Ԉ' => 'ԉ',
+ 'Ԋ' => 'ԋ',
+ 'Ԍ' => 'ԍ',
+ 'Ԏ' => 'ԏ',
+ 'Ԑ' => 'ԑ',
+ 'Ԓ' => 'ԓ',
+ 'Ԕ' => 'ԕ',
+ 'Ԗ' => 'ԗ',
+ 'Ԙ' => 'ԙ',
+ 'Ԛ' => 'ԛ',
+ 'Ԝ' => 'ԝ',
+ 'Ԟ' => 'ԟ',
+ 'Ԡ' => 'ԡ',
+ 'Ԣ' => 'ԣ',
+ 'Ԥ' => 'ԥ',
+ 'Ԧ' => 'ԧ',
+ 'Ԩ' => 'ԩ',
+ 'Ԫ' => 'ԫ',
+ 'Ԭ' => 'ԭ',
+ 'Ԯ' => 'ԯ',
+ 'Ա' => 'ա',
+ 'Բ' => 'բ',
+ 'Գ' => 'գ',
+ 'Դ' => 'դ',
+ 'Ե' => 'ե',
+ 'Զ' => 'զ',
+ 'Է' => 'է',
+ 'Ը' => 'ը',
+ 'Թ' => 'թ',
+ 'Ժ' => 'ժ',
+ 'Ի' => 'ի',
+ 'Լ' => 'լ',
+ 'Խ' => 'խ',
+ 'Ծ' => 'ծ',
+ 'Կ' => 'կ',
+ 'Հ' => 'հ',
+ 'Ձ' => 'ձ',
+ 'Ղ' => 'ղ',
+ 'Ճ' => 'ճ',
+ 'Մ' => 'մ',
+ 'Յ' => 'յ',
+ 'Ն' => 'ն',
+ 'Շ' => 'շ',
+ 'Ո' => 'ո',
+ 'Չ' => 'չ',
+ 'Պ' => 'պ',
+ 'Ջ' => 'ջ',
+ 'Ռ' => 'ռ',
+ 'Ս' => 'ս',
+ 'Վ' => 'վ',
+ 'Տ' => 'տ',
+ 'Ր' => 'ր',
+ 'Ց' => 'ց',
+ 'Ւ' => 'ւ',
+ 'Փ' => 'փ',
+ 'Ք' => 'ք',
+ 'Օ' => 'օ',
+ 'Ֆ' => 'ֆ',
+ 'Ⴀ' => 'ⴀ',
+ 'Ⴁ' => 'ⴁ',
+ 'Ⴂ' => 'ⴂ',
+ 'Ⴃ' => 'ⴃ',
+ 'Ⴄ' => 'ⴄ',
+ 'Ⴅ' => 'ⴅ',
+ 'Ⴆ' => 'ⴆ',
+ 'Ⴇ' => 'ⴇ',
+ 'Ⴈ' => 'ⴈ',
+ 'Ⴉ' => 'ⴉ',
+ 'Ⴊ' => 'ⴊ',
+ 'Ⴋ' => 'ⴋ',
+ 'Ⴌ' => 'ⴌ',
+ 'Ⴍ' => 'ⴍ',
+ 'Ⴎ' => 'ⴎ',
+ 'Ⴏ' => 'ⴏ',
+ 'Ⴐ' => 'ⴐ',
+ 'Ⴑ' => 'ⴑ',
+ 'Ⴒ' => 'ⴒ',
+ 'Ⴓ' => 'ⴓ',
+ 'Ⴔ' => 'ⴔ',
+ 'Ⴕ' => 'ⴕ',
+ 'Ⴖ' => 'ⴖ',
+ 'Ⴗ' => 'ⴗ',
+ 'Ⴘ' => 'ⴘ',
+ 'Ⴙ' => 'ⴙ',
+ 'Ⴚ' => 'ⴚ',
+ 'Ⴛ' => 'ⴛ',
+ 'Ⴜ' => 'ⴜ',
+ 'Ⴝ' => 'ⴝ',
+ 'Ⴞ' => 'ⴞ',
+ 'Ⴟ' => 'ⴟ',
+ 'Ⴠ' => 'ⴠ',
+ 'Ⴡ' => 'ⴡ',
+ 'Ⴢ' => 'ⴢ',
+ 'Ⴣ' => 'ⴣ',
+ 'Ⴤ' => 'ⴤ',
+ 'Ⴥ' => 'ⴥ',
+ 'Ⴧ' => 'ⴧ',
+ 'Ⴭ' => 'ⴭ',
+ 'Ꭰ' => 'ꭰ',
+ 'Ꭱ' => 'ꭱ',
+ 'Ꭲ' => 'ꭲ',
+ 'Ꭳ' => 'ꭳ',
+ 'Ꭴ' => 'ꭴ',
+ 'Ꭵ' => 'ꭵ',
+ 'Ꭶ' => 'ꭶ',
+ 'Ꭷ' => 'ꭷ',
+ 'Ꭸ' => 'ꭸ',
+ 'Ꭹ' => 'ꭹ',
+ 'Ꭺ' => 'ꭺ',
+ 'Ꭻ' => 'ꭻ',
+ 'Ꭼ' => 'ꭼ',
+ 'Ꭽ' => 'ꭽ',
+ 'Ꭾ' => 'ꭾ',
+ 'Ꭿ' => 'ꭿ',
+ 'Ꮀ' => 'ꮀ',
+ 'Ꮁ' => 'ꮁ',
+ 'Ꮂ' => 'ꮂ',
+ 'Ꮃ' => 'ꮃ',
+ 'Ꮄ' => 'ꮄ',
+ 'Ꮅ' => 'ꮅ',
+ 'Ꮆ' => 'ꮆ',
+ 'Ꮇ' => 'ꮇ',
+ 'Ꮈ' => 'ꮈ',
+ 'Ꮉ' => 'ꮉ',
+ 'Ꮊ' => 'ꮊ',
+ 'Ꮋ' => 'ꮋ',
+ 'Ꮌ' => 'ꮌ',
+ 'Ꮍ' => 'ꮍ',
+ 'Ꮎ' => 'ꮎ',
+ 'Ꮏ' => 'ꮏ',
+ 'Ꮐ' => 'ꮐ',
+ 'Ꮑ' => 'ꮑ',
+ 'Ꮒ' => 'ꮒ',
+ 'Ꮓ' => 'ꮓ',
+ 'Ꮔ' => 'ꮔ',
+ 'Ꮕ' => 'ꮕ',
+ 'Ꮖ' => 'ꮖ',
+ 'Ꮗ' => 'ꮗ',
+ 'Ꮘ' => 'ꮘ',
+ 'Ꮙ' => 'ꮙ',
+ 'Ꮚ' => 'ꮚ',
+ 'Ꮛ' => 'ꮛ',
+ 'Ꮜ' => 'ꮜ',
+ 'Ꮝ' => 'ꮝ',
+ 'Ꮞ' => 'ꮞ',
+ 'Ꮟ' => 'ꮟ',
+ 'Ꮠ' => 'ꮠ',
+ 'Ꮡ' => 'ꮡ',
+ 'Ꮢ' => 'ꮢ',
+ 'Ꮣ' => 'ꮣ',
+ 'Ꮤ' => 'ꮤ',
+ 'Ꮥ' => 'ꮥ',
+ 'Ꮦ' => 'ꮦ',
+ 'Ꮧ' => 'ꮧ',
+ 'Ꮨ' => 'ꮨ',
+ 'Ꮩ' => 'ꮩ',
+ 'Ꮪ' => 'ꮪ',
+ 'Ꮫ' => 'ꮫ',
+ 'Ꮬ' => 'ꮬ',
+ 'Ꮭ' => 'ꮭ',
+ 'Ꮮ' => 'ꮮ',
+ 'Ꮯ' => 'ꮯ',
+ 'Ꮰ' => 'ꮰ',
+ 'Ꮱ' => 'ꮱ',
+ 'Ꮲ' => 'ꮲ',
+ 'Ꮳ' => 'ꮳ',
+ 'Ꮴ' => 'ꮴ',
+ 'Ꮵ' => 'ꮵ',
+ 'Ꮶ' => 'ꮶ',
+ 'Ꮷ' => 'ꮷ',
+ 'Ꮸ' => 'ꮸ',
+ 'Ꮹ' => 'ꮹ',
+ 'Ꮺ' => 'ꮺ',
+ 'Ꮻ' => 'ꮻ',
+ 'Ꮼ' => 'ꮼ',
+ 'Ꮽ' => 'ꮽ',
+ 'Ꮾ' => 'ꮾ',
+ 'Ꮿ' => 'ꮿ',
+ 'Ᏸ' => 'ᏸ',
+ 'Ᏹ' => 'ᏹ',
+ 'Ᏺ' => 'ᏺ',
+ 'Ᏻ' => 'ᏻ',
+ 'Ᏼ' => 'ᏼ',
+ 'Ᏽ' => 'ᏽ',
+ 'Ა' => 'ა',
+ 'Ბ' => 'ბ',
+ 'Გ' => 'გ',
+ 'Დ' => 'დ',
+ 'Ე' => 'ე',
+ 'Ვ' => 'ვ',
+ 'Ზ' => 'ზ',
+ 'Თ' => 'თ',
+ 'Ი' => 'ი',
+ 'Კ' => 'კ',
+ 'Ლ' => 'ლ',
+ 'Მ' => 'მ',
+ 'Ნ' => 'ნ',
+ 'Ო' => 'ო',
+ 'Პ' => 'პ',
+ 'Ჟ' => 'ჟ',
+ 'Რ' => 'რ',
+ 'Ს' => 'ს',
+ 'Ტ' => 'ტ',
+ 'Უ' => 'უ',
+ 'Ფ' => 'ფ',
+ 'Ქ' => 'ქ',
+ 'Ღ' => 'ღ',
+ 'Ყ' => 'ყ',
+ 'Შ' => 'შ',
+ 'Ჩ' => 'ჩ',
+ 'Ც' => 'ც',
+ 'Ძ' => 'ძ',
+ 'Წ' => 'წ',
+ 'Ჭ' => 'ჭ',
+ 'Ხ' => 'ხ',
+ 'Ჯ' => 'ჯ',
+ 'Ჰ' => 'ჰ',
+ 'Ჱ' => 'ჱ',
+ 'Ჲ' => 'ჲ',
+ 'Ჳ' => 'ჳ',
+ 'Ჴ' => 'ჴ',
+ 'Ჵ' => 'ჵ',
+ 'Ჶ' => 'ჶ',
+ 'Ჷ' => 'ჷ',
+ 'Ჸ' => 'ჸ',
+ 'Ჹ' => 'ჹ',
+ 'Ჺ' => 'ჺ',
+ 'Ჽ' => 'ჽ',
+ 'Ჾ' => 'ჾ',
+ 'Ჿ' => 'ჿ',
+ 'Ḁ' => 'ḁ',
+ 'Ḃ' => 'ḃ',
+ 'Ḅ' => 'ḅ',
+ 'Ḇ' => 'ḇ',
+ 'Ḉ' => 'ḉ',
+ 'Ḋ' => 'ḋ',
+ 'Ḍ' => 'ḍ',
+ 'Ḏ' => 'ḏ',
+ 'Ḑ' => 'ḑ',
+ 'Ḓ' => 'ḓ',
+ 'Ḕ' => 'ḕ',
+ 'Ḗ' => 'ḗ',
+ 'Ḙ' => 'ḙ',
+ 'Ḛ' => 'ḛ',
+ 'Ḝ' => 'ḝ',
+ 'Ḟ' => 'ḟ',
+ 'Ḡ' => 'ḡ',
+ 'Ḣ' => 'ḣ',
+ 'Ḥ' => 'ḥ',
+ 'Ḧ' => 'ḧ',
+ 'Ḩ' => 'ḩ',
+ 'Ḫ' => 'ḫ',
+ 'Ḭ' => 'ḭ',
+ 'Ḯ' => 'ḯ',
+ 'Ḱ' => 'ḱ',
+ 'Ḳ' => 'ḳ',
+ 'Ḵ' => 'ḵ',
+ 'Ḷ' => 'ḷ',
+ 'Ḹ' => 'ḹ',
+ 'Ḻ' => 'ḻ',
+ 'Ḽ' => 'ḽ',
+ 'Ḿ' => 'ḿ',
+ 'Ṁ' => 'ṁ',
+ 'Ṃ' => 'ṃ',
+ 'Ṅ' => 'ṅ',
+ 'Ṇ' => 'ṇ',
+ 'Ṉ' => 'ṉ',
+ 'Ṋ' => 'ṋ',
+ 'Ṍ' => 'ṍ',
+ 'Ṏ' => 'ṏ',
+ 'Ṑ' => 'ṑ',
+ 'Ṓ' => 'ṓ',
+ 'Ṕ' => 'ṕ',
+ 'Ṗ' => 'ṗ',
+ 'Ṙ' => 'ṙ',
+ 'Ṛ' => 'ṛ',
+ 'Ṝ' => 'ṝ',
+ 'Ṟ' => 'ṟ',
+ 'Ṡ' => 'ṡ',
+ 'Ṣ' => 'ṣ',
+ 'Ṥ' => 'ṥ',
+ 'Ṧ' => 'ṧ',
+ 'Ṩ' => 'ṩ',
+ 'Ṫ' => 'ṫ',
+ 'Ṭ' => 'ṭ',
+ 'Ṯ' => 'ṯ',
+ 'Ṱ' => 'ṱ',
+ 'Ṳ' => 'ṳ',
+ 'Ṵ' => 'ṵ',
+ 'Ṷ' => 'ṷ',
+ 'Ṹ' => 'ṹ',
+ 'Ṻ' => 'ṻ',
+ 'Ṽ' => 'ṽ',
+ 'Ṿ' => 'ṿ',
+ 'Ẁ' => 'ẁ',
+ 'Ẃ' => 'ẃ',
+ 'Ẅ' => 'ẅ',
+ 'Ẇ' => 'ẇ',
+ 'Ẉ' => 'ẉ',
+ 'Ẋ' => 'ẋ',
+ 'Ẍ' => 'ẍ',
+ 'Ẏ' => 'ẏ',
+ 'Ẑ' => 'ẑ',
+ 'Ẓ' => 'ẓ',
+ 'Ẕ' => 'ẕ',
+ 'ẞ' => 'ß',
+ 'Ạ' => 'ạ',
+ 'Ả' => 'ả',
+ 'Ấ' => 'ấ',
+ 'Ầ' => 'ầ',
+ 'Ẩ' => 'ẩ',
+ 'Ẫ' => 'ẫ',
+ 'Ậ' => 'ậ',
+ 'Ắ' => 'ắ',
+ 'Ằ' => 'ằ',
+ 'Ẳ' => 'ẳ',
+ 'Ẵ' => 'ẵ',
+ 'Ặ' => 'ặ',
+ 'Ẹ' => 'ẹ',
+ 'Ẻ' => 'ẻ',
+ 'Ẽ' => 'ẽ',
+ 'Ế' => 'ế',
+ 'Ề' => 'ề',
+ 'Ể' => 'ể',
+ 'Ễ' => 'ễ',
+ 'Ệ' => 'ệ',
+ 'Ỉ' => 'ỉ',
+ 'Ị' => 'ị',
+ 'Ọ' => 'ọ',
+ 'Ỏ' => 'ỏ',
+ 'Ố' => 'ố',
+ 'Ồ' => 'ồ',
+ 'Ổ' => 'ổ',
+ 'Ỗ' => 'ỗ',
+ 'Ộ' => 'ộ',
+ 'Ớ' => 'ớ',
+ 'Ờ' => 'ờ',
+ 'Ở' => 'ở',
+ 'Ỡ' => 'ỡ',
+ 'Ợ' => 'ợ',
+ 'Ụ' => 'ụ',
+ 'Ủ' => 'ủ',
+ 'Ứ' => 'ứ',
+ 'Ừ' => 'ừ',
+ 'Ử' => 'ử',
+ 'Ữ' => 'ữ',
+ 'Ự' => 'ự',
+ 'Ỳ' => 'ỳ',
+ 'Ỵ' => 'ỵ',
+ 'Ỷ' => 'ỷ',
+ 'Ỹ' => 'ỹ',
+ 'Ỻ' => 'ỻ',
+ 'Ỽ' => 'ỽ',
+ 'Ỿ' => 'ỿ',
+ 'Ἀ' => 'ἀ',
+ 'Ἁ' => 'ἁ',
+ 'Ἂ' => 'ἂ',
+ 'Ἃ' => 'ἃ',
+ 'Ἄ' => 'ἄ',
+ 'Ἅ' => 'ἅ',
+ 'Ἆ' => 'ἆ',
+ 'Ἇ' => 'ἇ',
+ 'Ἐ' => 'ἐ',
+ 'Ἑ' => 'ἑ',
+ 'Ἒ' => 'ἒ',
+ 'Ἓ' => 'ἓ',
+ 'Ἔ' => 'ἔ',
+ 'Ἕ' => 'ἕ',
+ 'Ἠ' => 'ἠ',
+ 'Ἡ' => 'ἡ',
+ 'Ἢ' => 'ἢ',
+ 'Ἣ' => 'ἣ',
+ 'Ἤ' => 'ἤ',
+ 'Ἥ' => 'ἥ',
+ 'Ἦ' => 'ἦ',
+ 'Ἧ' => 'ἧ',
+ 'Ἰ' => 'ἰ',
+ 'Ἱ' => 'ἱ',
+ 'Ἲ' => 'ἲ',
+ 'Ἳ' => 'ἳ',
+ 'Ἴ' => 'ἴ',
+ 'Ἵ' => 'ἵ',
+ 'Ἶ' => 'ἶ',
+ 'Ἷ' => 'ἷ',
+ 'Ὀ' => 'ὀ',
+ 'Ὁ' => 'ὁ',
+ 'Ὂ' => 'ὂ',
+ 'Ὃ' => 'ὃ',
+ 'Ὄ' => 'ὄ',
+ 'Ὅ' => 'ὅ',
+ 'Ὑ' => 'ὑ',
+ 'Ὓ' => 'ὓ',
+ 'Ὕ' => 'ὕ',
+ 'Ὗ' => 'ὗ',
+ 'Ὠ' => 'ὠ',
+ 'Ὡ' => 'ὡ',
+ 'Ὢ' => 'ὢ',
+ 'Ὣ' => 'ὣ',
+ 'Ὤ' => 'ὤ',
+ 'Ὥ' => 'ὥ',
+ 'Ὦ' => 'ὦ',
+ 'Ὧ' => 'ὧ',
+ 'ᾈ' => 'ᾀ',
+ 'ᾉ' => 'ᾁ',
+ 'ᾊ' => 'ᾂ',
+ 'ᾋ' => 'ᾃ',
+ 'ᾌ' => 'ᾄ',
+ 'ᾍ' => 'ᾅ',
+ 'ᾎ' => 'ᾆ',
+ 'ᾏ' => 'ᾇ',
+ 'ᾘ' => 'ᾐ',
+ 'ᾙ' => 'ᾑ',
+ 'ᾚ' => 'ᾒ',
+ 'ᾛ' => 'ᾓ',
+ 'ᾜ' => 'ᾔ',
+ 'ᾝ' => 'ᾕ',
+ 'ᾞ' => 'ᾖ',
+ 'ᾟ' => 'ᾗ',
+ 'ᾨ' => 'ᾠ',
+ 'ᾩ' => 'ᾡ',
+ 'ᾪ' => 'ᾢ',
+ 'ᾫ' => 'ᾣ',
+ 'ᾬ' => 'ᾤ',
+ 'ᾭ' => 'ᾥ',
+ 'ᾮ' => 'ᾦ',
+ 'ᾯ' => 'ᾧ',
+ 'Ᾰ' => 'ᾰ',
+ 'Ᾱ' => 'ᾱ',
+ 'Ὰ' => 'ὰ',
+ 'Ά' => 'ά',
+ 'ᾼ' => 'ᾳ',
+ 'Ὲ' => 'ὲ',
+ 'Έ' => 'έ',
+ 'Ὴ' => 'ὴ',
+ 'Ή' => 'ή',
+ 'ῌ' => 'ῃ',
+ 'Ῐ' => 'ῐ',
+ 'Ῑ' => 'ῑ',
+ 'Ὶ' => 'ὶ',
+ 'Ί' => 'ί',
+ 'Ῠ' => 'ῠ',
+ 'Ῡ' => 'ῡ',
+ 'Ὺ' => 'ὺ',
+ 'Ύ' => 'ύ',
+ 'Ῥ' => 'ῥ',
+ 'Ὸ' => 'ὸ',
+ 'Ό' => 'ό',
+ 'Ὼ' => 'ὼ',
+ 'Ώ' => 'ώ',
+ 'ῼ' => 'ῳ',
+ 'Ω' => 'ω',
+ 'K' => 'k',
+ 'Å' => 'å',
+ 'Ⅎ' => 'ⅎ',
+ 'Ⅰ' => 'ⅰ',
+ 'Ⅱ' => 'ⅱ',
+ 'Ⅲ' => 'ⅲ',
+ 'Ⅳ' => 'ⅳ',
+ 'Ⅴ' => 'ⅴ',
+ 'Ⅵ' => 'ⅵ',
+ 'Ⅶ' => 'ⅶ',
+ 'Ⅷ' => 'ⅷ',
+ 'Ⅸ' => 'ⅸ',
+ 'Ⅹ' => 'ⅹ',
+ 'Ⅺ' => 'ⅺ',
+ 'Ⅻ' => 'ⅻ',
+ 'Ⅼ' => 'ⅼ',
+ 'Ⅽ' => 'ⅽ',
+ 'Ⅾ' => 'ⅾ',
+ 'Ⅿ' => 'ⅿ',
+ 'Ↄ' => 'ↄ',
+ 'Ⓐ' => 'ⓐ',
+ 'Ⓑ' => 'ⓑ',
+ 'Ⓒ' => 'ⓒ',
+ 'Ⓓ' => 'ⓓ',
+ 'Ⓔ' => 'ⓔ',
+ 'Ⓕ' => 'ⓕ',
+ 'Ⓖ' => 'ⓖ',
+ 'Ⓗ' => 'ⓗ',
+ 'Ⓘ' => 'ⓘ',
+ 'Ⓙ' => 'ⓙ',
+ 'Ⓚ' => 'ⓚ',
+ 'Ⓛ' => 'ⓛ',
+ 'Ⓜ' => 'ⓜ',
+ 'Ⓝ' => 'ⓝ',
+ 'Ⓞ' => 'ⓞ',
+ 'Ⓟ' => 'ⓟ',
+ 'Ⓠ' => 'ⓠ',
+ 'Ⓡ' => 'ⓡ',
+ 'Ⓢ' => 'ⓢ',
+ 'Ⓣ' => 'ⓣ',
+ 'Ⓤ' => 'ⓤ',
+ 'Ⓥ' => 'ⓥ',
+ 'Ⓦ' => 'ⓦ',
+ 'Ⓧ' => 'ⓧ',
+ 'Ⓨ' => 'ⓨ',
+ 'Ⓩ' => 'ⓩ',
+ 'Ⰰ' => 'ⰰ',
+ 'Ⰱ' => 'ⰱ',
+ 'Ⰲ' => 'ⰲ',
+ 'Ⰳ' => 'ⰳ',
+ 'Ⰴ' => 'ⰴ',
+ 'Ⰵ' => 'ⰵ',
+ 'Ⰶ' => 'ⰶ',
+ 'Ⰷ' => 'ⰷ',
+ 'Ⰸ' => 'ⰸ',
+ 'Ⰹ' => 'ⰹ',
+ 'Ⰺ' => 'ⰺ',
+ 'Ⰻ' => 'ⰻ',
+ 'Ⰼ' => 'ⰼ',
+ 'Ⰽ' => 'ⰽ',
+ 'Ⰾ' => 'ⰾ',
+ 'Ⰿ' => 'ⰿ',
+ 'Ⱀ' => 'ⱀ',
+ 'Ⱁ' => 'ⱁ',
+ 'Ⱂ' => 'ⱂ',
+ 'Ⱃ' => 'ⱃ',
+ 'Ⱄ' => 'ⱄ',
+ 'Ⱅ' => 'ⱅ',
+ 'Ⱆ' => 'ⱆ',
+ 'Ⱇ' => 'ⱇ',
+ 'Ⱈ' => 'ⱈ',
+ 'Ⱉ' => 'ⱉ',
+ 'Ⱊ' => 'ⱊ',
+ 'Ⱋ' => 'ⱋ',
+ 'Ⱌ' => 'ⱌ',
+ 'Ⱍ' => 'ⱍ',
+ 'Ⱎ' => 'ⱎ',
+ 'Ⱏ' => 'ⱏ',
+ 'Ⱐ' => 'ⱐ',
+ 'Ⱑ' => 'ⱑ',
+ 'Ⱒ' => 'ⱒ',
+ 'Ⱓ' => 'ⱓ',
+ 'Ⱔ' => 'ⱔ',
+ 'Ⱕ' => 'ⱕ',
+ 'Ⱖ' => 'ⱖ',
+ 'Ⱗ' => 'ⱗ',
+ 'Ⱘ' => 'ⱘ',
+ 'Ⱙ' => 'ⱙ',
+ 'Ⱚ' => 'ⱚ',
+ 'Ⱛ' => 'ⱛ',
+ 'Ⱜ' => 'ⱜ',
+ 'Ⱝ' => 'ⱝ',
+ 'Ⱞ' => 'ⱞ',
+ 'Ⱡ' => 'ⱡ',
+ 'Ɫ' => 'ɫ',
+ 'Ᵽ' => 'ᵽ',
+ 'Ɽ' => 'ɽ',
+ 'Ⱨ' => 'ⱨ',
+ 'Ⱪ' => 'ⱪ',
+ 'Ⱬ' => 'ⱬ',
+ 'Ɑ' => 'ɑ',
+ 'Ɱ' => 'ɱ',
+ 'Ɐ' => 'ɐ',
+ 'Ɒ' => 'ɒ',
+ 'Ⱳ' => 'ⱳ',
+ 'Ⱶ' => 'ⱶ',
+ 'Ȿ' => 'ȿ',
+ 'Ɀ' => 'ɀ',
+ 'Ⲁ' => 'ⲁ',
+ 'Ⲃ' => 'ⲃ',
+ 'Ⲅ' => 'ⲅ',
+ 'Ⲇ' => 'ⲇ',
+ 'Ⲉ' => 'ⲉ',
+ 'Ⲋ' => 'ⲋ',
+ 'Ⲍ' => 'ⲍ',
+ 'Ⲏ' => 'ⲏ',
+ 'Ⲑ' => 'ⲑ',
+ 'Ⲓ' => 'ⲓ',
+ 'Ⲕ' => 'ⲕ',
+ 'Ⲗ' => 'ⲗ',
+ 'Ⲙ' => 'ⲙ',
+ 'Ⲛ' => 'ⲛ',
+ 'Ⲝ' => 'ⲝ',
+ 'Ⲟ' => 'ⲟ',
+ 'Ⲡ' => 'ⲡ',
+ 'Ⲣ' => 'ⲣ',
+ 'Ⲥ' => 'ⲥ',
+ 'Ⲧ' => 'ⲧ',
+ 'Ⲩ' => 'ⲩ',
+ 'Ⲫ' => 'ⲫ',
+ 'Ⲭ' => 'ⲭ',
+ 'Ⲯ' => 'ⲯ',
+ 'Ⲱ' => 'ⲱ',
+ 'Ⲳ' => 'ⲳ',
+ 'Ⲵ' => 'ⲵ',
+ 'Ⲷ' => 'ⲷ',
+ 'Ⲹ' => 'ⲹ',
+ 'Ⲻ' => 'ⲻ',
+ 'Ⲽ' => 'ⲽ',
+ 'Ⲿ' => 'ⲿ',
+ 'Ⳁ' => 'ⳁ',
+ 'Ⳃ' => 'ⳃ',
+ 'Ⳅ' => 'ⳅ',
+ 'Ⳇ' => 'ⳇ',
+ 'Ⳉ' => 'ⳉ',
+ 'Ⳋ' => 'ⳋ',
+ 'Ⳍ' => 'ⳍ',
+ 'Ⳏ' => 'ⳏ',
+ 'Ⳑ' => 'ⳑ',
+ 'Ⳓ' => 'ⳓ',
+ 'Ⳕ' => 'ⳕ',
+ 'Ⳗ' => 'ⳗ',
+ 'Ⳙ' => 'ⳙ',
+ 'Ⳛ' => 'ⳛ',
+ 'Ⳝ' => 'ⳝ',
+ 'Ⳟ' => 'ⳟ',
+ 'Ⳡ' => 'ⳡ',
+ 'Ⳣ' => 'ⳣ',
+ 'Ⳬ' => 'ⳬ',
+ 'Ⳮ' => 'ⳮ',
+ 'Ⳳ' => 'ⳳ',
+ 'Ꙁ' => 'ꙁ',
+ 'Ꙃ' => 'ꙃ',
+ 'Ꙅ' => 'ꙅ',
+ 'Ꙇ' => 'ꙇ',
+ 'Ꙉ' => 'ꙉ',
+ 'Ꙋ' => 'ꙋ',
+ 'Ꙍ' => 'ꙍ',
+ 'Ꙏ' => 'ꙏ',
+ 'Ꙑ' => 'ꙑ',
+ 'Ꙓ' => 'ꙓ',
+ 'Ꙕ' => 'ꙕ',
+ 'Ꙗ' => 'ꙗ',
+ 'Ꙙ' => 'ꙙ',
+ 'Ꙛ' => 'ꙛ',
+ 'Ꙝ' => 'ꙝ',
+ 'Ꙟ' => 'ꙟ',
+ 'Ꙡ' => 'ꙡ',
+ 'Ꙣ' => 'ꙣ',
+ 'Ꙥ' => 'ꙥ',
+ 'Ꙧ' => 'ꙧ',
+ 'Ꙩ' => 'ꙩ',
+ 'Ꙫ' => 'ꙫ',
+ 'Ꙭ' => 'ꙭ',
+ 'Ꚁ' => 'ꚁ',
+ 'Ꚃ' => 'ꚃ',
+ 'Ꚅ' => 'ꚅ',
+ 'Ꚇ' => 'ꚇ',
+ 'Ꚉ' => 'ꚉ',
+ 'Ꚋ' => 'ꚋ',
+ 'Ꚍ' => 'ꚍ',
+ 'Ꚏ' => 'ꚏ',
+ 'Ꚑ' => 'ꚑ',
+ 'Ꚓ' => 'ꚓ',
+ 'Ꚕ' => 'ꚕ',
+ 'Ꚗ' => 'ꚗ',
+ 'Ꚙ' => 'ꚙ',
+ 'Ꚛ' => 'ꚛ',
+ 'Ꜣ' => 'ꜣ',
+ 'Ꜥ' => 'ꜥ',
+ 'Ꜧ' => 'ꜧ',
+ 'Ꜩ' => 'ꜩ',
+ 'Ꜫ' => 'ꜫ',
+ 'Ꜭ' => 'ꜭ',
+ 'Ꜯ' => 'ꜯ',
+ 'Ꜳ' => 'ꜳ',
+ 'Ꜵ' => 'ꜵ',
+ 'Ꜷ' => 'ꜷ',
+ 'Ꜹ' => 'ꜹ',
+ 'Ꜻ' => 'ꜻ',
+ 'Ꜽ' => 'ꜽ',
+ 'Ꜿ' => 'ꜿ',
+ 'Ꝁ' => 'ꝁ',
+ 'Ꝃ' => 'ꝃ',
+ 'Ꝅ' => 'ꝅ',
+ 'Ꝇ' => 'ꝇ',
+ 'Ꝉ' => 'ꝉ',
+ 'Ꝋ' => 'ꝋ',
+ 'Ꝍ' => 'ꝍ',
+ 'Ꝏ' => 'ꝏ',
+ 'Ꝑ' => 'ꝑ',
+ 'Ꝓ' => 'ꝓ',
+ 'Ꝕ' => 'ꝕ',
+ 'Ꝗ' => 'ꝗ',
+ 'Ꝙ' => 'ꝙ',
+ 'Ꝛ' => 'ꝛ',
+ 'Ꝝ' => 'ꝝ',
+ 'Ꝟ' => 'ꝟ',
+ 'Ꝡ' => 'ꝡ',
+ 'Ꝣ' => 'ꝣ',
+ 'Ꝥ' => 'ꝥ',
+ 'Ꝧ' => 'ꝧ',
+ 'Ꝩ' => 'ꝩ',
+ 'Ꝫ' => 'ꝫ',
+ 'Ꝭ' => 'ꝭ',
+ 'Ꝯ' => 'ꝯ',
+ 'Ꝺ' => 'ꝺ',
+ 'Ꝼ' => 'ꝼ',
+ 'Ᵹ' => 'ᵹ',
+ 'Ꝿ' => 'ꝿ',
+ 'Ꞁ' => 'ꞁ',
+ 'Ꞃ' => 'ꞃ',
+ 'Ꞅ' => 'ꞅ',
+ 'Ꞇ' => 'ꞇ',
+ 'Ꞌ' => 'ꞌ',
+ 'Ɥ' => 'ɥ',
+ 'Ꞑ' => 'ꞑ',
+ 'Ꞓ' => 'ꞓ',
+ 'Ꞗ' => 'ꞗ',
+ 'Ꞙ' => 'ꞙ',
+ 'Ꞛ' => 'ꞛ',
+ 'Ꞝ' => 'ꞝ',
+ 'Ꞟ' => 'ꞟ',
+ 'Ꞡ' => 'ꞡ',
+ 'Ꞣ' => 'ꞣ',
+ 'Ꞥ' => 'ꞥ',
+ 'Ꞧ' => 'ꞧ',
+ 'Ꞩ' => 'ꞩ',
+ 'Ɦ' => 'ɦ',
+ 'Ɜ' => 'ɜ',
+ 'Ɡ' => 'ɡ',
+ 'Ɬ' => 'ɬ',
+ 'Ɪ' => 'ɪ',
+ 'Ʞ' => 'ʞ',
+ 'Ʇ' => 'ʇ',
+ 'Ʝ' => 'ʝ',
+ 'Ꭓ' => 'ꭓ',
+ 'Ꞵ' => 'ꞵ',
+ 'Ꞷ' => 'ꞷ',
+ 'Ꞹ' => 'ꞹ',
+ 'Ꞻ' => 'ꞻ',
+ 'Ꞽ' => 'ꞽ',
+ 'Ꞿ' => 'ꞿ',
+ 'Ꟃ' => 'ꟃ',
+ 'Ꞔ' => 'ꞔ',
+ 'Ʂ' => 'ʂ',
+ 'Ᶎ' => 'ᶎ',
+ 'Ꟈ' => 'ꟈ',
+ 'Ꟊ' => 'ꟊ',
+ 'Ꟶ' => 'ꟶ',
+ 'A' => 'a',
+ 'B' => 'b',
+ 'C' => 'c',
+ 'D' => 'd',
+ 'E' => 'e',
+ 'F' => 'f',
+ 'G' => 'g',
+ 'H' => 'h',
+ 'I' => 'i',
+ 'J' => 'j',
+ 'K' => 'k',
+ 'L' => 'l',
+ 'M' => 'm',
+ 'N' => 'n',
+ 'O' => 'o',
+ 'P' => 'p',
+ 'Q' => 'q',
+ 'R' => 'r',
+ 'S' => 's',
+ 'T' => 't',
+ 'U' => 'u',
+ 'V' => 'v',
+ 'W' => 'w',
+ 'X' => 'x',
+ 'Y' => 'y',
+ 'Z' => 'z',
+ '𐐀' => '𐐨',
+ '𐐁' => '𐐩',
+ '𐐂' => '𐐪',
+ '𐐃' => '𐐫',
+ '𐐄' => '𐐬',
+ '𐐅' => '𐐭',
+ '𐐆' => '𐐮',
+ '𐐇' => '𐐯',
+ '𐐈' => '𐐰',
+ '𐐉' => '𐐱',
+ '𐐊' => '𐐲',
+ '𐐋' => '𐐳',
+ '𐐌' => '𐐴',
+ '𐐍' => '𐐵',
+ '𐐎' => '𐐶',
+ '𐐏' => '𐐷',
+ '𐐐' => '𐐸',
+ '𐐑' => '𐐹',
+ '𐐒' => '𐐺',
+ '𐐓' => '𐐻',
+ '𐐔' => '𐐼',
+ '𐐕' => '𐐽',
+ '𐐖' => '𐐾',
+ '𐐗' => '𐐿',
+ '𐐘' => '𐑀',
+ '𐐙' => '𐑁',
+ '𐐚' => '𐑂',
+ '𐐛' => '𐑃',
+ '𐐜' => '𐑄',
+ '𐐝' => '𐑅',
+ '𐐞' => '𐑆',
+ '𐐟' => '𐑇',
+ '𐐠' => '𐑈',
+ '𐐡' => '𐑉',
+ '𐐢' => '𐑊',
+ '𐐣' => '𐑋',
+ '𐐤' => '𐑌',
+ '𐐥' => '𐑍',
+ '𐐦' => '𐑎',
+ '𐐧' => '𐑏',
+ '𐒰' => '𐓘',
+ '𐒱' => '𐓙',
+ '𐒲' => '𐓚',
+ '𐒳' => '𐓛',
+ '𐒴' => '𐓜',
+ '𐒵' => '𐓝',
+ '𐒶' => '𐓞',
+ '𐒷' => '𐓟',
+ '𐒸' => '𐓠',
+ '𐒹' => '𐓡',
+ '𐒺' => '𐓢',
+ '𐒻' => '𐓣',
+ '𐒼' => '𐓤',
+ '𐒽' => '𐓥',
+ '𐒾' => '𐓦',
+ '𐒿' => '𐓧',
+ '𐓀' => '𐓨',
+ '𐓁' => '𐓩',
+ '𐓂' => '𐓪',
+ '𐓃' => '𐓫',
+ '𐓄' => '𐓬',
+ '𐓅' => '𐓭',
+ '𐓆' => '𐓮',
+ '𐓇' => '𐓯',
+ '𐓈' => '𐓰',
+ '𐓉' => '𐓱',
+ '𐓊' => '𐓲',
+ '𐓋' => '𐓳',
+ '𐓌' => '𐓴',
+ '𐓍' => '𐓵',
+ '𐓎' => '𐓶',
+ '𐓏' => '𐓷',
+ '𐓐' => '𐓸',
+ '𐓑' => '𐓹',
+ '𐓒' => '𐓺',
+ '𐓓' => '𐓻',
+ '𐲀' => '𐳀',
+ '𐲁' => '𐳁',
+ '𐲂' => '𐳂',
+ '𐲃' => '𐳃',
+ '𐲄' => '𐳄',
+ '𐲅' => '𐳅',
+ '𐲆' => '𐳆',
+ '𐲇' => '𐳇',
+ '𐲈' => '𐳈',
+ '𐲉' => '𐳉',
+ '𐲊' => '𐳊',
+ '𐲋' => '𐳋',
+ '𐲌' => '𐳌',
+ '𐲍' => '𐳍',
+ '𐲎' => '𐳎',
+ '𐲏' => '𐳏',
+ '𐲐' => '𐳐',
+ '𐲑' => '𐳑',
+ '𐲒' => '𐳒',
+ '𐲓' => '𐳓',
+ '𐲔' => '𐳔',
+ '𐲕' => '𐳕',
+ '𐲖' => '𐳖',
+ '𐲗' => '𐳗',
+ '𐲘' => '𐳘',
+ '𐲙' => '𐳙',
+ '𐲚' => '𐳚',
+ '𐲛' => '𐳛',
+ '𐲜' => '𐳜',
+ '𐲝' => '𐳝',
+ '𐲞' => '𐳞',
+ '𐲟' => '𐳟',
+ '𐲠' => '𐳠',
+ '𐲡' => '𐳡',
+ '𐲢' => '𐳢',
+ '𐲣' => '𐳣',
+ '𐲤' => '𐳤',
+ '𐲥' => '𐳥',
+ '𐲦' => '𐳦',
+ '𐲧' => '𐳧',
+ '𐲨' => '𐳨',
+ '𐲩' => '𐳩',
+ '𐲪' => '𐳪',
+ '𐲫' => '𐳫',
+ '𐲬' => '𐳬',
+ '𐲭' => '𐳭',
+ '𐲮' => '𐳮',
+ '𐲯' => '𐳯',
+ '𐲰' => '𐳰',
+ '𐲱' => '𐳱',
+ '𐲲' => '𐳲',
+ '𑢠' => '𑣀',
+ '𑢡' => '𑣁',
+ '𑢢' => '𑣂',
+ '𑢣' => '𑣃',
+ '𑢤' => '𑣄',
+ '𑢥' => '𑣅',
+ '𑢦' => '𑣆',
+ '𑢧' => '𑣇',
+ '𑢨' => '𑣈',
+ '𑢩' => '𑣉',
+ '𑢪' => '𑣊',
+ '𑢫' => '𑣋',
+ '𑢬' => '𑣌',
+ '𑢭' => '𑣍',
+ '𑢮' => '𑣎',
+ '𑢯' => '𑣏',
+ '𑢰' => '𑣐',
+ '𑢱' => '𑣑',
+ '𑢲' => '𑣒',
+ '𑢳' => '𑣓',
+ '𑢴' => '𑣔',
+ '𑢵' => '𑣕',
+ '𑢶' => '𑣖',
+ '𑢷' => '𑣗',
+ '𑢸' => '𑣘',
+ '𑢹' => '𑣙',
+ '𑢺' => '𑣚',
+ '𑢻' => '𑣛',
+ '𑢼' => '𑣜',
+ '𑢽' => '𑣝',
+ '𑢾' => '𑣞',
+ '𑢿' => '𑣟',
+ '𖹀' => '𖹠',
+ '𖹁' => '𖹡',
+ '𖹂' => '𖹢',
+ '𖹃' => '𖹣',
+ '𖹄' => '𖹤',
+ '𖹅' => '𖹥',
+ '𖹆' => '𖹦',
+ '𖹇' => '𖹧',
+ '𖹈' => '𖹨',
+ '𖹉' => '𖹩',
+ '𖹊' => '𖹪',
+ '𖹋' => '𖹫',
+ '𖹌' => '𖹬',
+ '𖹍' => '𖹭',
+ '𖹎' => '𖹮',
+ '𖹏' => '𖹯',
+ '𖹐' => '𖹰',
+ '𖹑' => '𖹱',
+ '𖹒' => '𖹲',
+ '𖹓' => '𖹳',
+ '𖹔' => '𖹴',
+ '𖹕' => '𖹵',
+ '𖹖' => '𖹶',
+ '𖹗' => '𖹷',
+ '𖹘' => '𖹸',
+ '𖹙' => '𖹹',
+ '𖹚' => '𖹺',
+ '𖹛' => '𖹻',
+ '𖹜' => '𖹼',
+ '𖹝' => '𖹽',
+ '𖹞' => '𖹾',
+ '𖹟' => '𖹿',
+ '𞤀' => '𞤢',
+ '𞤁' => '𞤣',
+ '𞤂' => '𞤤',
+ '𞤃' => '𞤥',
+ '𞤄' => '𞤦',
+ '𞤅' => '𞤧',
+ '𞤆' => '𞤨',
+ '𞤇' => '𞤩',
+ '𞤈' => '𞤪',
+ '𞤉' => '𞤫',
+ '𞤊' => '𞤬',
+ '𞤋' => '𞤭',
+ '𞤌' => '𞤮',
+ '𞤍' => '𞤯',
+ '𞤎' => '𞤰',
+ '𞤏' => '𞤱',
+ '𞤐' => '𞤲',
+ '𞤑' => '𞤳',
+ '𞤒' => '𞤴',
+ '𞤓' => '𞤵',
+ '𞤔' => '𞤶',
+ '𞤕' => '𞤷',
+ '𞤖' => '𞤸',
+ '𞤗' => '𞤹',
+ '𞤘' => '𞤺',
+ '𞤙' => '𞤻',
+ '𞤚' => '𞤼',
+ '𞤛' => '𞤽',
+ '𞤜' => '𞤾',
+ '𞤝' => '𞤿',
+ '𞤞' => '𞥀',
+ '𞤟' => '𞥁',
+ '𞤠' => '𞥂',
+ '𞤡' => '𞥃',
+);
diff --git a/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php b/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php
new file mode 100644
index 0000000..2a8f6e7
--- /dev/null
+++ b/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php
@@ -0,0 +1,5 @@
+ 'A',
+ 'b' => 'B',
+ 'c' => 'C',
+ 'd' => 'D',
+ 'e' => 'E',
+ 'f' => 'F',
+ 'g' => 'G',
+ 'h' => 'H',
+ 'i' => 'I',
+ 'j' => 'J',
+ 'k' => 'K',
+ 'l' => 'L',
+ 'm' => 'M',
+ 'n' => 'N',
+ 'o' => 'O',
+ 'p' => 'P',
+ 'q' => 'Q',
+ 'r' => 'R',
+ 's' => 'S',
+ 't' => 'T',
+ 'u' => 'U',
+ 'v' => 'V',
+ 'w' => 'W',
+ 'x' => 'X',
+ 'y' => 'Y',
+ 'z' => 'Z',
+ 'µ' => 'Μ',
+ 'à' => 'À',
+ 'á' => 'Á',
+ 'â' => 'Â',
+ 'ã' => 'Ã',
+ 'ä' => 'Ä',
+ 'å' => 'Å',
+ 'æ' => 'Æ',
+ 'ç' => 'Ç',
+ 'è' => 'È',
+ 'é' => 'É',
+ 'ê' => 'Ê',
+ 'ë' => 'Ë',
+ 'ì' => 'Ì',
+ 'í' => 'Í',
+ 'î' => 'Î',
+ 'ï' => 'Ï',
+ 'ð' => 'Ð',
+ 'ñ' => 'Ñ',
+ 'ò' => 'Ò',
+ 'ó' => 'Ó',
+ 'ô' => 'Ô',
+ 'õ' => 'Õ',
+ 'ö' => 'Ö',
+ 'ø' => 'Ø',
+ 'ù' => 'Ù',
+ 'ú' => 'Ú',
+ 'û' => 'Û',
+ 'ü' => 'Ü',
+ 'ý' => 'Ý',
+ 'þ' => 'Þ',
+ 'ÿ' => 'Ÿ',
+ 'ā' => 'Ā',
+ 'ă' => 'Ă',
+ 'ą' => 'Ą',
+ 'ć' => 'Ć',
+ 'ĉ' => 'Ĉ',
+ 'ċ' => 'Ċ',
+ 'č' => 'Č',
+ 'ď' => 'Ď',
+ 'đ' => 'Đ',
+ 'ē' => 'Ē',
+ 'ĕ' => 'Ĕ',
+ 'ė' => 'Ė',
+ 'ę' => 'Ę',
+ 'ě' => 'Ě',
+ 'ĝ' => 'Ĝ',
+ 'ğ' => 'Ğ',
+ 'ġ' => 'Ġ',
+ 'ģ' => 'Ģ',
+ 'ĥ' => 'Ĥ',
+ 'ħ' => 'Ħ',
+ 'ĩ' => 'Ĩ',
+ 'ī' => 'Ī',
+ 'ĭ' => 'Ĭ',
+ 'į' => 'Į',
+ 'ı' => 'I',
+ 'ij' => 'IJ',
+ 'ĵ' => 'Ĵ',
+ 'ķ' => 'Ķ',
+ 'ĺ' => 'Ĺ',
+ 'ļ' => 'Ļ',
+ 'ľ' => 'Ľ',
+ 'ŀ' => 'Ŀ',
+ 'ł' => 'Ł',
+ 'ń' => 'Ń',
+ 'ņ' => 'Ņ',
+ 'ň' => 'Ň',
+ 'ŋ' => 'Ŋ',
+ 'ō' => 'Ō',
+ 'ŏ' => 'Ŏ',
+ 'ő' => 'Ő',
+ 'œ' => 'Œ',
+ 'ŕ' => 'Ŕ',
+ 'ŗ' => 'Ŗ',
+ 'ř' => 'Ř',
+ 'ś' => 'Ś',
+ 'ŝ' => 'Ŝ',
+ 'ş' => 'Ş',
+ 'š' => 'Š',
+ 'ţ' => 'Ţ',
+ 'ť' => 'Ť',
+ 'ŧ' => 'Ŧ',
+ 'ũ' => 'Ũ',
+ 'ū' => 'Ū',
+ 'ŭ' => 'Ŭ',
+ 'ů' => 'Ů',
+ 'ű' => 'Ű',
+ 'ų' => 'Ų',
+ 'ŵ' => 'Ŵ',
+ 'ŷ' => 'Ŷ',
+ 'ź' => 'Ź',
+ 'ż' => 'Ż',
+ 'ž' => 'Ž',
+ 'ſ' => 'S',
+ 'ƀ' => 'Ƀ',
+ 'ƃ' => 'Ƃ',
+ 'ƅ' => 'Ƅ',
+ 'ƈ' => 'Ƈ',
+ 'ƌ' => 'Ƌ',
+ 'ƒ' => 'Ƒ',
+ 'ƕ' => 'Ƕ',
+ 'ƙ' => 'Ƙ',
+ 'ƚ' => 'Ƚ',
+ 'ƞ' => 'Ƞ',
+ 'ơ' => 'Ơ',
+ 'ƣ' => 'Ƣ',
+ 'ƥ' => 'Ƥ',
+ 'ƨ' => 'Ƨ',
+ 'ƭ' => 'Ƭ',
+ 'ư' => 'Ư',
+ 'ƴ' => 'Ƴ',
+ 'ƶ' => 'Ƶ',
+ 'ƹ' => 'Ƹ',
+ 'ƽ' => 'Ƽ',
+ 'ƿ' => 'Ƿ',
+ 'Dž' => 'DŽ',
+ 'dž' => 'DŽ',
+ 'Lj' => 'LJ',
+ 'lj' => 'LJ',
+ 'Nj' => 'NJ',
+ 'nj' => 'NJ',
+ 'ǎ' => 'Ǎ',
+ 'ǐ' => 'Ǐ',
+ 'ǒ' => 'Ǒ',
+ 'ǔ' => 'Ǔ',
+ 'ǖ' => 'Ǖ',
+ 'ǘ' => 'Ǘ',
+ 'ǚ' => 'Ǚ',
+ 'ǜ' => 'Ǜ',
+ 'ǝ' => 'Ǝ',
+ 'ǟ' => 'Ǟ',
+ 'ǡ' => 'Ǡ',
+ 'ǣ' => 'Ǣ',
+ 'ǥ' => 'Ǥ',
+ 'ǧ' => 'Ǧ',
+ 'ǩ' => 'Ǩ',
+ 'ǫ' => 'Ǫ',
+ 'ǭ' => 'Ǭ',
+ 'ǯ' => 'Ǯ',
+ 'Dz' => 'DZ',
+ 'dz' => 'DZ',
+ 'ǵ' => 'Ǵ',
+ 'ǹ' => 'Ǹ',
+ 'ǻ' => 'Ǻ',
+ 'ǽ' => 'Ǽ',
+ 'ǿ' => 'Ǿ',
+ 'ȁ' => 'Ȁ',
+ 'ȃ' => 'Ȃ',
+ 'ȅ' => 'Ȅ',
+ 'ȇ' => 'Ȇ',
+ 'ȉ' => 'Ȉ',
+ 'ȋ' => 'Ȋ',
+ 'ȍ' => 'Ȍ',
+ 'ȏ' => 'Ȏ',
+ 'ȑ' => 'Ȑ',
+ 'ȓ' => 'Ȓ',
+ 'ȕ' => 'Ȕ',
+ 'ȗ' => 'Ȗ',
+ 'ș' => 'Ș',
+ 'ț' => 'Ț',
+ 'ȝ' => 'Ȝ',
+ 'ȟ' => 'Ȟ',
+ 'ȣ' => 'Ȣ',
+ 'ȥ' => 'Ȥ',
+ 'ȧ' => 'Ȧ',
+ 'ȩ' => 'Ȩ',
+ 'ȫ' => 'Ȫ',
+ 'ȭ' => 'Ȭ',
+ 'ȯ' => 'Ȯ',
+ 'ȱ' => 'Ȱ',
+ 'ȳ' => 'Ȳ',
+ 'ȼ' => 'Ȼ',
+ 'ȿ' => 'Ȿ',
+ 'ɀ' => 'Ɀ',
+ 'ɂ' => 'Ɂ',
+ 'ɇ' => 'Ɇ',
+ 'ɉ' => 'Ɉ',
+ 'ɋ' => 'Ɋ',
+ 'ɍ' => 'Ɍ',
+ 'ɏ' => 'Ɏ',
+ 'ɐ' => 'Ɐ',
+ 'ɑ' => 'Ɑ',
+ 'ɒ' => 'Ɒ',
+ 'ɓ' => 'Ɓ',
+ 'ɔ' => 'Ɔ',
+ 'ɖ' => 'Ɖ',
+ 'ɗ' => 'Ɗ',
+ 'ə' => 'Ə',
+ 'ɛ' => 'Ɛ',
+ 'ɜ' => 'Ɜ',
+ 'ɠ' => 'Ɠ',
+ 'ɡ' => 'Ɡ',
+ 'ɣ' => 'Ɣ',
+ 'ɥ' => 'Ɥ',
+ 'ɦ' => 'Ɦ',
+ 'ɨ' => 'Ɨ',
+ 'ɩ' => 'Ɩ',
+ 'ɪ' => 'Ɪ',
+ 'ɫ' => 'Ɫ',
+ 'ɬ' => 'Ɬ',
+ 'ɯ' => 'Ɯ',
+ 'ɱ' => 'Ɱ',
+ 'ɲ' => 'Ɲ',
+ 'ɵ' => 'Ɵ',
+ 'ɽ' => 'Ɽ',
+ 'ʀ' => 'Ʀ',
+ 'ʂ' => 'Ʂ',
+ 'ʃ' => 'Ʃ',
+ 'ʇ' => 'Ʇ',
+ 'ʈ' => 'Ʈ',
+ 'ʉ' => 'Ʉ',
+ 'ʊ' => 'Ʊ',
+ 'ʋ' => 'Ʋ',
+ 'ʌ' => 'Ʌ',
+ 'ʒ' => 'Ʒ',
+ 'ʝ' => 'Ʝ',
+ 'ʞ' => 'Ʞ',
+ 'ͅ' => 'Ι',
+ 'ͱ' => 'Ͱ',
+ 'ͳ' => 'Ͳ',
+ 'ͷ' => 'Ͷ',
+ 'ͻ' => 'Ͻ',
+ 'ͼ' => 'Ͼ',
+ 'ͽ' => 'Ͽ',
+ 'ά' => 'Ά',
+ 'έ' => 'Έ',
+ 'ή' => 'Ή',
+ 'ί' => 'Ί',
+ 'α' => 'Α',
+ 'β' => 'Β',
+ 'γ' => 'Γ',
+ 'δ' => 'Δ',
+ 'ε' => 'Ε',
+ 'ζ' => 'Ζ',
+ 'η' => 'Η',
+ 'θ' => 'Θ',
+ 'ι' => 'Ι',
+ 'κ' => 'Κ',
+ 'λ' => 'Λ',
+ 'μ' => 'Μ',
+ 'ν' => 'Ν',
+ 'ξ' => 'Ξ',
+ 'ο' => 'Ο',
+ 'π' => 'Π',
+ 'ρ' => 'Ρ',
+ 'ς' => 'Σ',
+ 'σ' => 'Σ',
+ 'τ' => 'Τ',
+ 'υ' => 'Υ',
+ 'φ' => 'Φ',
+ 'χ' => 'Χ',
+ 'ψ' => 'Ψ',
+ 'ω' => 'Ω',
+ 'ϊ' => 'Ϊ',
+ 'ϋ' => 'Ϋ',
+ 'ό' => 'Ό',
+ 'ύ' => 'Ύ',
+ 'ώ' => 'Ώ',
+ 'ϐ' => 'Β',
+ 'ϑ' => 'Θ',
+ 'ϕ' => 'Φ',
+ 'ϖ' => 'Π',
+ 'ϗ' => 'Ϗ',
+ 'ϙ' => 'Ϙ',
+ 'ϛ' => 'Ϛ',
+ 'ϝ' => 'Ϝ',
+ 'ϟ' => 'Ϟ',
+ 'ϡ' => 'Ϡ',
+ 'ϣ' => 'Ϣ',
+ 'ϥ' => 'Ϥ',
+ 'ϧ' => 'Ϧ',
+ 'ϩ' => 'Ϩ',
+ 'ϫ' => 'Ϫ',
+ 'ϭ' => 'Ϭ',
+ 'ϯ' => 'Ϯ',
+ 'ϰ' => 'Κ',
+ 'ϱ' => 'Ρ',
+ 'ϲ' => 'Ϲ',
+ 'ϳ' => 'Ϳ',
+ 'ϵ' => 'Ε',
+ 'ϸ' => 'Ϸ',
+ 'ϻ' => 'Ϻ',
+ 'а' => 'А',
+ 'б' => 'Б',
+ 'в' => 'В',
+ 'г' => 'Г',
+ 'д' => 'Д',
+ 'е' => 'Е',
+ 'ж' => 'Ж',
+ 'з' => 'З',
+ 'и' => 'И',
+ 'й' => 'Й',
+ 'к' => 'К',
+ 'л' => 'Л',
+ 'м' => 'М',
+ 'н' => 'Н',
+ 'о' => 'О',
+ 'п' => 'П',
+ 'р' => 'Р',
+ 'с' => 'С',
+ 'т' => 'Т',
+ 'у' => 'У',
+ 'ф' => 'Ф',
+ 'х' => 'Х',
+ 'ц' => 'Ц',
+ 'ч' => 'Ч',
+ 'ш' => 'Ш',
+ 'щ' => 'Щ',
+ 'ъ' => 'Ъ',
+ 'ы' => 'Ы',
+ 'ь' => 'Ь',
+ 'э' => 'Э',
+ 'ю' => 'Ю',
+ 'я' => 'Я',
+ 'ѐ' => 'Ѐ',
+ 'ё' => 'Ё',
+ 'ђ' => 'Ђ',
+ 'ѓ' => 'Ѓ',
+ 'є' => 'Є',
+ 'ѕ' => 'Ѕ',
+ 'і' => 'І',
+ 'ї' => 'Ї',
+ 'ј' => 'Ј',
+ 'љ' => 'Љ',
+ 'њ' => 'Њ',
+ 'ћ' => 'Ћ',
+ 'ќ' => 'Ќ',
+ 'ѝ' => 'Ѝ',
+ 'ў' => 'Ў',
+ 'џ' => 'Џ',
+ 'ѡ' => 'Ѡ',
+ 'ѣ' => 'Ѣ',
+ 'ѥ' => 'Ѥ',
+ 'ѧ' => 'Ѧ',
+ 'ѩ' => 'Ѩ',
+ 'ѫ' => 'Ѫ',
+ 'ѭ' => 'Ѭ',
+ 'ѯ' => 'Ѯ',
+ 'ѱ' => 'Ѱ',
+ 'ѳ' => 'Ѳ',
+ 'ѵ' => 'Ѵ',
+ 'ѷ' => 'Ѷ',
+ 'ѹ' => 'Ѹ',
+ 'ѻ' => 'Ѻ',
+ 'ѽ' => 'Ѽ',
+ 'ѿ' => 'Ѿ',
+ 'ҁ' => 'Ҁ',
+ 'ҋ' => 'Ҋ',
+ 'ҍ' => 'Ҍ',
+ 'ҏ' => 'Ҏ',
+ 'ґ' => 'Ґ',
+ 'ғ' => 'Ғ',
+ 'ҕ' => 'Ҕ',
+ 'җ' => 'Җ',
+ 'ҙ' => 'Ҙ',
+ 'қ' => 'Қ',
+ 'ҝ' => 'Ҝ',
+ 'ҟ' => 'Ҟ',
+ 'ҡ' => 'Ҡ',
+ 'ң' => 'Ң',
+ 'ҥ' => 'Ҥ',
+ 'ҧ' => 'Ҧ',
+ 'ҩ' => 'Ҩ',
+ 'ҫ' => 'Ҫ',
+ 'ҭ' => 'Ҭ',
+ 'ү' => 'Ү',
+ 'ұ' => 'Ұ',
+ 'ҳ' => 'Ҳ',
+ 'ҵ' => 'Ҵ',
+ 'ҷ' => 'Ҷ',
+ 'ҹ' => 'Ҹ',
+ 'һ' => 'Һ',
+ 'ҽ' => 'Ҽ',
+ 'ҿ' => 'Ҿ',
+ 'ӂ' => 'Ӂ',
+ 'ӄ' => 'Ӄ',
+ 'ӆ' => 'Ӆ',
+ 'ӈ' => 'Ӈ',
+ 'ӊ' => 'Ӊ',
+ 'ӌ' => 'Ӌ',
+ 'ӎ' => 'Ӎ',
+ 'ӏ' => 'Ӏ',
+ 'ӑ' => 'Ӑ',
+ 'ӓ' => 'Ӓ',
+ 'ӕ' => 'Ӕ',
+ 'ӗ' => 'Ӗ',
+ 'ә' => 'Ә',
+ 'ӛ' => 'Ӛ',
+ 'ӝ' => 'Ӝ',
+ 'ӟ' => 'Ӟ',
+ 'ӡ' => 'Ӡ',
+ 'ӣ' => 'Ӣ',
+ 'ӥ' => 'Ӥ',
+ 'ӧ' => 'Ӧ',
+ 'ө' => 'Ө',
+ 'ӫ' => 'Ӫ',
+ 'ӭ' => 'Ӭ',
+ 'ӯ' => 'Ӯ',
+ 'ӱ' => 'Ӱ',
+ 'ӳ' => 'Ӳ',
+ 'ӵ' => 'Ӵ',
+ 'ӷ' => 'Ӷ',
+ 'ӹ' => 'Ӹ',
+ 'ӻ' => 'Ӻ',
+ 'ӽ' => 'Ӽ',
+ 'ӿ' => 'Ӿ',
+ 'ԁ' => 'Ԁ',
+ 'ԃ' => 'Ԃ',
+ 'ԅ' => 'Ԅ',
+ 'ԇ' => 'Ԇ',
+ 'ԉ' => 'Ԉ',
+ 'ԋ' => 'Ԋ',
+ 'ԍ' => 'Ԍ',
+ 'ԏ' => 'Ԏ',
+ 'ԑ' => 'Ԑ',
+ 'ԓ' => 'Ԓ',
+ 'ԕ' => 'Ԕ',
+ 'ԗ' => 'Ԗ',
+ 'ԙ' => 'Ԙ',
+ 'ԛ' => 'Ԛ',
+ 'ԝ' => 'Ԝ',
+ 'ԟ' => 'Ԟ',
+ 'ԡ' => 'Ԡ',
+ 'ԣ' => 'Ԣ',
+ 'ԥ' => 'Ԥ',
+ 'ԧ' => 'Ԧ',
+ 'ԩ' => 'Ԩ',
+ 'ԫ' => 'Ԫ',
+ 'ԭ' => 'Ԭ',
+ 'ԯ' => 'Ԯ',
+ 'ա' => 'Ա',
+ 'բ' => 'Բ',
+ 'գ' => 'Գ',
+ 'դ' => 'Դ',
+ 'ե' => 'Ե',
+ 'զ' => 'Զ',
+ 'է' => 'Է',
+ 'ը' => 'Ը',
+ 'թ' => 'Թ',
+ 'ժ' => 'Ժ',
+ 'ի' => 'Ի',
+ 'լ' => 'Լ',
+ 'խ' => 'Խ',
+ 'ծ' => 'Ծ',
+ 'կ' => 'Կ',
+ 'հ' => 'Հ',
+ 'ձ' => 'Ձ',
+ 'ղ' => 'Ղ',
+ 'ճ' => 'Ճ',
+ 'մ' => 'Մ',
+ 'յ' => 'Յ',
+ 'ն' => 'Ն',
+ 'շ' => 'Շ',
+ 'ո' => 'Ո',
+ 'չ' => 'Չ',
+ 'պ' => 'Պ',
+ 'ջ' => 'Ջ',
+ 'ռ' => 'Ռ',
+ 'ս' => 'Ս',
+ 'վ' => 'Վ',
+ 'տ' => 'Տ',
+ 'ր' => 'Ր',
+ 'ց' => 'Ց',
+ 'ւ' => 'Ւ',
+ 'փ' => 'Փ',
+ 'ք' => 'Ք',
+ 'օ' => 'Օ',
+ 'ֆ' => 'Ֆ',
+ 'ა' => 'Ა',
+ 'ბ' => 'Ბ',
+ 'გ' => 'Გ',
+ 'დ' => 'Დ',
+ 'ე' => 'Ე',
+ 'ვ' => 'Ვ',
+ 'ზ' => 'Ზ',
+ 'თ' => 'Თ',
+ 'ი' => 'Ი',
+ 'კ' => 'Კ',
+ 'ლ' => 'Ლ',
+ 'მ' => 'Მ',
+ 'ნ' => 'Ნ',
+ 'ო' => 'Ო',
+ 'პ' => 'Პ',
+ 'ჟ' => 'Ჟ',
+ 'რ' => 'Რ',
+ 'ს' => 'Ს',
+ 'ტ' => 'Ტ',
+ 'უ' => 'Უ',
+ 'ფ' => 'Ფ',
+ 'ქ' => 'Ქ',
+ 'ღ' => 'Ღ',
+ 'ყ' => 'Ყ',
+ 'შ' => 'Შ',
+ 'ჩ' => 'Ჩ',
+ 'ც' => 'Ც',
+ 'ძ' => 'Ძ',
+ 'წ' => 'Წ',
+ 'ჭ' => 'Ჭ',
+ 'ხ' => 'Ხ',
+ 'ჯ' => 'Ჯ',
+ 'ჰ' => 'Ჰ',
+ 'ჱ' => 'Ჱ',
+ 'ჲ' => 'Ჲ',
+ 'ჳ' => 'Ჳ',
+ 'ჴ' => 'Ჴ',
+ 'ჵ' => 'Ჵ',
+ 'ჶ' => 'Ჶ',
+ 'ჷ' => 'Ჷ',
+ 'ჸ' => 'Ჸ',
+ 'ჹ' => 'Ჹ',
+ 'ჺ' => 'Ჺ',
+ 'ჽ' => 'Ჽ',
+ 'ჾ' => 'Ჾ',
+ 'ჿ' => 'Ჿ',
+ 'ᏸ' => 'Ᏸ',
+ 'ᏹ' => 'Ᏹ',
+ 'ᏺ' => 'Ᏺ',
+ 'ᏻ' => 'Ᏻ',
+ 'ᏼ' => 'Ᏼ',
+ 'ᏽ' => 'Ᏽ',
+ 'ᲀ' => 'В',
+ 'ᲁ' => 'Д',
+ 'ᲂ' => 'О',
+ 'ᲃ' => 'С',
+ 'ᲄ' => 'Т',
+ 'ᲅ' => 'Т',
+ 'ᲆ' => 'Ъ',
+ 'ᲇ' => 'Ѣ',
+ 'ᲈ' => 'Ꙋ',
+ 'ᵹ' => 'Ᵹ',
+ 'ᵽ' => 'Ᵽ',
+ 'ᶎ' => 'Ᶎ',
+ 'ḁ' => 'Ḁ',
+ 'ḃ' => 'Ḃ',
+ 'ḅ' => 'Ḅ',
+ 'ḇ' => 'Ḇ',
+ 'ḉ' => 'Ḉ',
+ 'ḋ' => 'Ḋ',
+ 'ḍ' => 'Ḍ',
+ 'ḏ' => 'Ḏ',
+ 'ḑ' => 'Ḑ',
+ 'ḓ' => 'Ḓ',
+ 'ḕ' => 'Ḕ',
+ 'ḗ' => 'Ḗ',
+ 'ḙ' => 'Ḙ',
+ 'ḛ' => 'Ḛ',
+ 'ḝ' => 'Ḝ',
+ 'ḟ' => 'Ḟ',
+ 'ḡ' => 'Ḡ',
+ 'ḣ' => 'Ḣ',
+ 'ḥ' => 'Ḥ',
+ 'ḧ' => 'Ḧ',
+ 'ḩ' => 'Ḩ',
+ 'ḫ' => 'Ḫ',
+ 'ḭ' => 'Ḭ',
+ 'ḯ' => 'Ḯ',
+ 'ḱ' => 'Ḱ',
+ 'ḳ' => 'Ḳ',
+ 'ḵ' => 'Ḵ',
+ 'ḷ' => 'Ḷ',
+ 'ḹ' => 'Ḹ',
+ 'ḻ' => 'Ḻ',
+ 'ḽ' => 'Ḽ',
+ 'ḿ' => 'Ḿ',
+ 'ṁ' => 'Ṁ',
+ 'ṃ' => 'Ṃ',
+ 'ṅ' => 'Ṅ',
+ 'ṇ' => 'Ṇ',
+ 'ṉ' => 'Ṉ',
+ 'ṋ' => 'Ṋ',
+ 'ṍ' => 'Ṍ',
+ 'ṏ' => 'Ṏ',
+ 'ṑ' => 'Ṑ',
+ 'ṓ' => 'Ṓ',
+ 'ṕ' => 'Ṕ',
+ 'ṗ' => 'Ṗ',
+ 'ṙ' => 'Ṙ',
+ 'ṛ' => 'Ṛ',
+ 'ṝ' => 'Ṝ',
+ 'ṟ' => 'Ṟ',
+ 'ṡ' => 'Ṡ',
+ 'ṣ' => 'Ṣ',
+ 'ṥ' => 'Ṥ',
+ 'ṧ' => 'Ṧ',
+ 'ṩ' => 'Ṩ',
+ 'ṫ' => 'Ṫ',
+ 'ṭ' => 'Ṭ',
+ 'ṯ' => 'Ṯ',
+ 'ṱ' => 'Ṱ',
+ 'ṳ' => 'Ṳ',
+ 'ṵ' => 'Ṵ',
+ 'ṷ' => 'Ṷ',
+ 'ṹ' => 'Ṹ',
+ 'ṻ' => 'Ṻ',
+ 'ṽ' => 'Ṽ',
+ 'ṿ' => 'Ṿ',
+ 'ẁ' => 'Ẁ',
+ 'ẃ' => 'Ẃ',
+ 'ẅ' => 'Ẅ',
+ 'ẇ' => 'Ẇ',
+ 'ẉ' => 'Ẉ',
+ 'ẋ' => 'Ẋ',
+ 'ẍ' => 'Ẍ',
+ 'ẏ' => 'Ẏ',
+ 'ẑ' => 'Ẑ',
+ 'ẓ' => 'Ẓ',
+ 'ẕ' => 'Ẕ',
+ 'ẛ' => 'Ṡ',
+ 'ạ' => 'Ạ',
+ 'ả' => 'Ả',
+ 'ấ' => 'Ấ',
+ 'ầ' => 'Ầ',
+ 'ẩ' => 'Ẩ',
+ 'ẫ' => 'Ẫ',
+ 'ậ' => 'Ậ',
+ 'ắ' => 'Ắ',
+ 'ằ' => 'Ằ',
+ 'ẳ' => 'Ẳ',
+ 'ẵ' => 'Ẵ',
+ 'ặ' => 'Ặ',
+ 'ẹ' => 'Ẹ',
+ 'ẻ' => 'Ẻ',
+ 'ẽ' => 'Ẽ',
+ 'ế' => 'Ế',
+ 'ề' => 'Ề',
+ 'ể' => 'Ể',
+ 'ễ' => 'Ễ',
+ 'ệ' => 'Ệ',
+ 'ỉ' => 'Ỉ',
+ 'ị' => 'Ị',
+ 'ọ' => 'Ọ',
+ 'ỏ' => 'Ỏ',
+ 'ố' => 'Ố',
+ 'ồ' => 'Ồ',
+ 'ổ' => 'Ổ',
+ 'ỗ' => 'Ỗ',
+ 'ộ' => 'Ộ',
+ 'ớ' => 'Ớ',
+ 'ờ' => 'Ờ',
+ 'ở' => 'Ở',
+ 'ỡ' => 'Ỡ',
+ 'ợ' => 'Ợ',
+ 'ụ' => 'Ụ',
+ 'ủ' => 'Ủ',
+ 'ứ' => 'Ứ',
+ 'ừ' => 'Ừ',
+ 'ử' => 'Ử',
+ 'ữ' => 'Ữ',
+ 'ự' => 'Ự',
+ 'ỳ' => 'Ỳ',
+ 'ỵ' => 'Ỵ',
+ 'ỷ' => 'Ỷ',
+ 'ỹ' => 'Ỹ',
+ 'ỻ' => 'Ỻ',
+ 'ỽ' => 'Ỽ',
+ 'ỿ' => 'Ỿ',
+ 'ἀ' => 'Ἀ',
+ 'ἁ' => 'Ἁ',
+ 'ἂ' => 'Ἂ',
+ 'ἃ' => 'Ἃ',
+ 'ἄ' => 'Ἄ',
+ 'ἅ' => 'Ἅ',
+ 'ἆ' => 'Ἆ',
+ 'ἇ' => 'Ἇ',
+ 'ἐ' => 'Ἐ',
+ 'ἑ' => 'Ἑ',
+ 'ἒ' => 'Ἒ',
+ 'ἓ' => 'Ἓ',
+ 'ἔ' => 'Ἔ',
+ 'ἕ' => 'Ἕ',
+ 'ἠ' => 'Ἠ',
+ 'ἡ' => 'Ἡ',
+ 'ἢ' => 'Ἢ',
+ 'ἣ' => 'Ἣ',
+ 'ἤ' => 'Ἤ',
+ 'ἥ' => 'Ἥ',
+ 'ἦ' => 'Ἦ',
+ 'ἧ' => 'Ἧ',
+ 'ἰ' => 'Ἰ',
+ 'ἱ' => 'Ἱ',
+ 'ἲ' => 'Ἲ',
+ 'ἳ' => 'Ἳ',
+ 'ἴ' => 'Ἴ',
+ 'ἵ' => 'Ἵ',
+ 'ἶ' => 'Ἶ',
+ 'ἷ' => 'Ἷ',
+ 'ὀ' => 'Ὀ',
+ 'ὁ' => 'Ὁ',
+ 'ὂ' => 'Ὂ',
+ 'ὃ' => 'Ὃ',
+ 'ὄ' => 'Ὄ',
+ 'ὅ' => 'Ὅ',
+ 'ὑ' => 'Ὑ',
+ 'ὓ' => 'Ὓ',
+ 'ὕ' => 'Ὕ',
+ 'ὗ' => 'Ὗ',
+ 'ὠ' => 'Ὠ',
+ 'ὡ' => 'Ὡ',
+ 'ὢ' => 'Ὢ',
+ 'ὣ' => 'Ὣ',
+ 'ὤ' => 'Ὤ',
+ 'ὥ' => 'Ὥ',
+ 'ὦ' => 'Ὦ',
+ 'ὧ' => 'Ὧ',
+ 'ὰ' => 'Ὰ',
+ 'ά' => 'Ά',
+ 'ὲ' => 'Ὲ',
+ 'έ' => 'Έ',
+ 'ὴ' => 'Ὴ',
+ 'ή' => 'Ή',
+ 'ὶ' => 'Ὶ',
+ 'ί' => 'Ί',
+ 'ὸ' => 'Ὸ',
+ 'ό' => 'Ό',
+ 'ὺ' => 'Ὺ',
+ 'ύ' => 'Ύ',
+ 'ὼ' => 'Ὼ',
+ 'ώ' => 'Ώ',
+ 'ᾀ' => 'ἈΙ',
+ 'ᾁ' => 'ἉΙ',
+ 'ᾂ' => 'ἊΙ',
+ 'ᾃ' => 'ἋΙ',
+ 'ᾄ' => 'ἌΙ',
+ 'ᾅ' => 'ἍΙ',
+ 'ᾆ' => 'ἎΙ',
+ 'ᾇ' => 'ἏΙ',
+ 'ᾐ' => 'ἨΙ',
+ 'ᾑ' => 'ἩΙ',
+ 'ᾒ' => 'ἪΙ',
+ 'ᾓ' => 'ἫΙ',
+ 'ᾔ' => 'ἬΙ',
+ 'ᾕ' => 'ἭΙ',
+ 'ᾖ' => 'ἮΙ',
+ 'ᾗ' => 'ἯΙ',
+ 'ᾠ' => 'ὨΙ',
+ 'ᾡ' => 'ὩΙ',
+ 'ᾢ' => 'ὪΙ',
+ 'ᾣ' => 'ὫΙ',
+ 'ᾤ' => 'ὬΙ',
+ 'ᾥ' => 'ὭΙ',
+ 'ᾦ' => 'ὮΙ',
+ 'ᾧ' => 'ὯΙ',
+ 'ᾰ' => 'Ᾰ',
+ 'ᾱ' => 'Ᾱ',
+ 'ᾳ' => 'ΑΙ',
+ 'ι' => 'Ι',
+ 'ῃ' => 'ΗΙ',
+ 'ῐ' => 'Ῐ',
+ 'ῑ' => 'Ῑ',
+ 'ῠ' => 'Ῠ',
+ 'ῡ' => 'Ῡ',
+ 'ῥ' => 'Ῥ',
+ 'ῳ' => 'ΩΙ',
+ 'ⅎ' => 'Ⅎ',
+ 'ⅰ' => 'Ⅰ',
+ 'ⅱ' => 'Ⅱ',
+ 'ⅲ' => 'Ⅲ',
+ 'ⅳ' => 'Ⅳ',
+ 'ⅴ' => 'Ⅴ',
+ 'ⅵ' => 'Ⅵ',
+ 'ⅶ' => 'Ⅶ',
+ 'ⅷ' => 'Ⅷ',
+ 'ⅸ' => 'Ⅸ',
+ 'ⅹ' => 'Ⅹ',
+ 'ⅺ' => 'Ⅺ',
+ 'ⅻ' => 'Ⅻ',
+ 'ⅼ' => 'Ⅼ',
+ 'ⅽ' => 'Ⅽ',
+ 'ⅾ' => 'Ⅾ',
+ 'ⅿ' => 'Ⅿ',
+ 'ↄ' => 'Ↄ',
+ 'ⓐ' => 'Ⓐ',
+ 'ⓑ' => 'Ⓑ',
+ 'ⓒ' => 'Ⓒ',
+ 'ⓓ' => 'Ⓓ',
+ 'ⓔ' => 'Ⓔ',
+ 'ⓕ' => 'Ⓕ',
+ 'ⓖ' => 'Ⓖ',
+ 'ⓗ' => 'Ⓗ',
+ 'ⓘ' => 'Ⓘ',
+ 'ⓙ' => 'Ⓙ',
+ 'ⓚ' => 'Ⓚ',
+ 'ⓛ' => 'Ⓛ',
+ 'ⓜ' => 'Ⓜ',
+ 'ⓝ' => 'Ⓝ',
+ 'ⓞ' => 'Ⓞ',
+ 'ⓟ' => 'Ⓟ',
+ 'ⓠ' => 'Ⓠ',
+ 'ⓡ' => 'Ⓡ',
+ 'ⓢ' => 'Ⓢ',
+ 'ⓣ' => 'Ⓣ',
+ 'ⓤ' => 'Ⓤ',
+ 'ⓥ' => 'Ⓥ',
+ 'ⓦ' => 'Ⓦ',
+ 'ⓧ' => 'Ⓧ',
+ 'ⓨ' => 'Ⓨ',
+ 'ⓩ' => 'Ⓩ',
+ 'ⰰ' => 'Ⰰ',
+ 'ⰱ' => 'Ⰱ',
+ 'ⰲ' => 'Ⰲ',
+ 'ⰳ' => 'Ⰳ',
+ 'ⰴ' => 'Ⰴ',
+ 'ⰵ' => 'Ⰵ',
+ 'ⰶ' => 'Ⰶ',
+ 'ⰷ' => 'Ⰷ',
+ 'ⰸ' => 'Ⰸ',
+ 'ⰹ' => 'Ⰹ',
+ 'ⰺ' => 'Ⰺ',
+ 'ⰻ' => 'Ⰻ',
+ 'ⰼ' => 'Ⰼ',
+ 'ⰽ' => 'Ⰽ',
+ 'ⰾ' => 'Ⰾ',
+ 'ⰿ' => 'Ⰿ',
+ 'ⱀ' => 'Ⱀ',
+ 'ⱁ' => 'Ⱁ',
+ 'ⱂ' => 'Ⱂ',
+ 'ⱃ' => 'Ⱃ',
+ 'ⱄ' => 'Ⱄ',
+ 'ⱅ' => 'Ⱅ',
+ 'ⱆ' => 'Ⱆ',
+ 'ⱇ' => 'Ⱇ',
+ 'ⱈ' => 'Ⱈ',
+ 'ⱉ' => 'Ⱉ',
+ 'ⱊ' => 'Ⱊ',
+ 'ⱋ' => 'Ⱋ',
+ 'ⱌ' => 'Ⱌ',
+ 'ⱍ' => 'Ⱍ',
+ 'ⱎ' => 'Ⱎ',
+ 'ⱏ' => 'Ⱏ',
+ 'ⱐ' => 'Ⱐ',
+ 'ⱑ' => 'Ⱑ',
+ 'ⱒ' => 'Ⱒ',
+ 'ⱓ' => 'Ⱓ',
+ 'ⱔ' => 'Ⱔ',
+ 'ⱕ' => 'Ⱕ',
+ 'ⱖ' => 'Ⱖ',
+ 'ⱗ' => 'Ⱗ',
+ 'ⱘ' => 'Ⱘ',
+ 'ⱙ' => 'Ⱙ',
+ 'ⱚ' => 'Ⱚ',
+ 'ⱛ' => 'Ⱛ',
+ 'ⱜ' => 'Ⱜ',
+ 'ⱝ' => 'Ⱝ',
+ 'ⱞ' => 'Ⱞ',
+ 'ⱡ' => 'Ⱡ',
+ 'ⱥ' => 'Ⱥ',
+ 'ⱦ' => 'Ⱦ',
+ 'ⱨ' => 'Ⱨ',
+ 'ⱪ' => 'Ⱪ',
+ 'ⱬ' => 'Ⱬ',
+ 'ⱳ' => 'Ⱳ',
+ 'ⱶ' => 'Ⱶ',
+ 'ⲁ' => 'Ⲁ',
+ 'ⲃ' => 'Ⲃ',
+ 'ⲅ' => 'Ⲅ',
+ 'ⲇ' => 'Ⲇ',
+ 'ⲉ' => 'Ⲉ',
+ 'ⲋ' => 'Ⲋ',
+ 'ⲍ' => 'Ⲍ',
+ 'ⲏ' => 'Ⲏ',
+ 'ⲑ' => 'Ⲑ',
+ 'ⲓ' => 'Ⲓ',
+ 'ⲕ' => 'Ⲕ',
+ 'ⲗ' => 'Ⲗ',
+ 'ⲙ' => 'Ⲙ',
+ 'ⲛ' => 'Ⲛ',
+ 'ⲝ' => 'Ⲝ',
+ 'ⲟ' => 'Ⲟ',
+ 'ⲡ' => 'Ⲡ',
+ 'ⲣ' => 'Ⲣ',
+ 'ⲥ' => 'Ⲥ',
+ 'ⲧ' => 'Ⲧ',
+ 'ⲩ' => 'Ⲩ',
+ 'ⲫ' => 'Ⲫ',
+ 'ⲭ' => 'Ⲭ',
+ 'ⲯ' => 'Ⲯ',
+ 'ⲱ' => 'Ⲱ',
+ 'ⲳ' => 'Ⲳ',
+ 'ⲵ' => 'Ⲵ',
+ 'ⲷ' => 'Ⲷ',
+ 'ⲹ' => 'Ⲹ',
+ 'ⲻ' => 'Ⲻ',
+ 'ⲽ' => 'Ⲽ',
+ 'ⲿ' => 'Ⲿ',
+ 'ⳁ' => 'Ⳁ',
+ 'ⳃ' => 'Ⳃ',
+ 'ⳅ' => 'Ⳅ',
+ 'ⳇ' => 'Ⳇ',
+ 'ⳉ' => 'Ⳉ',
+ 'ⳋ' => 'Ⳋ',
+ 'ⳍ' => 'Ⳍ',
+ 'ⳏ' => 'Ⳏ',
+ 'ⳑ' => 'Ⳑ',
+ 'ⳓ' => 'Ⳓ',
+ 'ⳕ' => 'Ⳕ',
+ 'ⳗ' => 'Ⳗ',
+ 'ⳙ' => 'Ⳙ',
+ 'ⳛ' => 'Ⳛ',
+ 'ⳝ' => 'Ⳝ',
+ 'ⳟ' => 'Ⳟ',
+ 'ⳡ' => 'Ⳡ',
+ 'ⳣ' => 'Ⳣ',
+ 'ⳬ' => 'Ⳬ',
+ 'ⳮ' => 'Ⳮ',
+ 'ⳳ' => 'Ⳳ',
+ 'ⴀ' => 'Ⴀ',
+ 'ⴁ' => 'Ⴁ',
+ 'ⴂ' => 'Ⴂ',
+ 'ⴃ' => 'Ⴃ',
+ 'ⴄ' => 'Ⴄ',
+ 'ⴅ' => 'Ⴅ',
+ 'ⴆ' => 'Ⴆ',
+ 'ⴇ' => 'Ⴇ',
+ 'ⴈ' => 'Ⴈ',
+ 'ⴉ' => 'Ⴉ',
+ 'ⴊ' => 'Ⴊ',
+ 'ⴋ' => 'Ⴋ',
+ 'ⴌ' => 'Ⴌ',
+ 'ⴍ' => 'Ⴍ',
+ 'ⴎ' => 'Ⴎ',
+ 'ⴏ' => 'Ⴏ',
+ 'ⴐ' => 'Ⴐ',
+ 'ⴑ' => 'Ⴑ',
+ 'ⴒ' => 'Ⴒ',
+ 'ⴓ' => 'Ⴓ',
+ 'ⴔ' => 'Ⴔ',
+ 'ⴕ' => 'Ⴕ',
+ 'ⴖ' => 'Ⴖ',
+ 'ⴗ' => 'Ⴗ',
+ 'ⴘ' => 'Ⴘ',
+ 'ⴙ' => 'Ⴙ',
+ 'ⴚ' => 'Ⴚ',
+ 'ⴛ' => 'Ⴛ',
+ 'ⴜ' => 'Ⴜ',
+ 'ⴝ' => 'Ⴝ',
+ 'ⴞ' => 'Ⴞ',
+ 'ⴟ' => 'Ⴟ',
+ 'ⴠ' => 'Ⴠ',
+ 'ⴡ' => 'Ⴡ',
+ 'ⴢ' => 'Ⴢ',
+ 'ⴣ' => 'Ⴣ',
+ 'ⴤ' => 'Ⴤ',
+ 'ⴥ' => 'Ⴥ',
+ 'ⴧ' => 'Ⴧ',
+ 'ⴭ' => 'Ⴭ',
+ 'ꙁ' => 'Ꙁ',
+ 'ꙃ' => 'Ꙃ',
+ 'ꙅ' => 'Ꙅ',
+ 'ꙇ' => 'Ꙇ',
+ 'ꙉ' => 'Ꙉ',
+ 'ꙋ' => 'Ꙋ',
+ 'ꙍ' => 'Ꙍ',
+ 'ꙏ' => 'Ꙏ',
+ 'ꙑ' => 'Ꙑ',
+ 'ꙓ' => 'Ꙓ',
+ 'ꙕ' => 'Ꙕ',
+ 'ꙗ' => 'Ꙗ',
+ 'ꙙ' => 'Ꙙ',
+ 'ꙛ' => 'Ꙛ',
+ 'ꙝ' => 'Ꙝ',
+ 'ꙟ' => 'Ꙟ',
+ 'ꙡ' => 'Ꙡ',
+ 'ꙣ' => 'Ꙣ',
+ 'ꙥ' => 'Ꙥ',
+ 'ꙧ' => 'Ꙧ',
+ 'ꙩ' => 'Ꙩ',
+ 'ꙫ' => 'Ꙫ',
+ 'ꙭ' => 'Ꙭ',
+ 'ꚁ' => 'Ꚁ',
+ 'ꚃ' => 'Ꚃ',
+ 'ꚅ' => 'Ꚅ',
+ 'ꚇ' => 'Ꚇ',
+ 'ꚉ' => 'Ꚉ',
+ 'ꚋ' => 'Ꚋ',
+ 'ꚍ' => 'Ꚍ',
+ 'ꚏ' => 'Ꚏ',
+ 'ꚑ' => 'Ꚑ',
+ 'ꚓ' => 'Ꚓ',
+ 'ꚕ' => 'Ꚕ',
+ 'ꚗ' => 'Ꚗ',
+ 'ꚙ' => 'Ꚙ',
+ 'ꚛ' => 'Ꚛ',
+ 'ꜣ' => 'Ꜣ',
+ 'ꜥ' => 'Ꜥ',
+ 'ꜧ' => 'Ꜧ',
+ 'ꜩ' => 'Ꜩ',
+ 'ꜫ' => 'Ꜫ',
+ 'ꜭ' => 'Ꜭ',
+ 'ꜯ' => 'Ꜯ',
+ 'ꜳ' => 'Ꜳ',
+ 'ꜵ' => 'Ꜵ',
+ 'ꜷ' => 'Ꜷ',
+ 'ꜹ' => 'Ꜹ',
+ 'ꜻ' => 'Ꜻ',
+ 'ꜽ' => 'Ꜽ',
+ 'ꜿ' => 'Ꜿ',
+ 'ꝁ' => 'Ꝁ',
+ 'ꝃ' => 'Ꝃ',
+ 'ꝅ' => 'Ꝅ',
+ 'ꝇ' => 'Ꝇ',
+ 'ꝉ' => 'Ꝉ',
+ 'ꝋ' => 'Ꝋ',
+ 'ꝍ' => 'Ꝍ',
+ 'ꝏ' => 'Ꝏ',
+ 'ꝑ' => 'Ꝑ',
+ 'ꝓ' => 'Ꝓ',
+ 'ꝕ' => 'Ꝕ',
+ 'ꝗ' => 'Ꝗ',
+ 'ꝙ' => 'Ꝙ',
+ 'ꝛ' => 'Ꝛ',
+ 'ꝝ' => 'Ꝝ',
+ 'ꝟ' => 'Ꝟ',
+ 'ꝡ' => 'Ꝡ',
+ 'ꝣ' => 'Ꝣ',
+ 'ꝥ' => 'Ꝥ',
+ 'ꝧ' => 'Ꝧ',
+ 'ꝩ' => 'Ꝩ',
+ 'ꝫ' => 'Ꝫ',
+ 'ꝭ' => 'Ꝭ',
+ 'ꝯ' => 'Ꝯ',
+ 'ꝺ' => 'Ꝺ',
+ 'ꝼ' => 'Ꝼ',
+ 'ꝿ' => 'Ꝿ',
+ 'ꞁ' => 'Ꞁ',
+ 'ꞃ' => 'Ꞃ',
+ 'ꞅ' => 'Ꞅ',
+ 'ꞇ' => 'Ꞇ',
+ 'ꞌ' => 'Ꞌ',
+ 'ꞑ' => 'Ꞑ',
+ 'ꞓ' => 'Ꞓ',
+ 'ꞔ' => 'Ꞔ',
+ 'ꞗ' => 'Ꞗ',
+ 'ꞙ' => 'Ꞙ',
+ 'ꞛ' => 'Ꞛ',
+ 'ꞝ' => 'Ꞝ',
+ 'ꞟ' => 'Ꞟ',
+ 'ꞡ' => 'Ꞡ',
+ 'ꞣ' => 'Ꞣ',
+ 'ꞥ' => 'Ꞥ',
+ 'ꞧ' => 'Ꞧ',
+ 'ꞩ' => 'Ꞩ',
+ 'ꞵ' => 'Ꞵ',
+ 'ꞷ' => 'Ꞷ',
+ 'ꞹ' => 'Ꞹ',
+ 'ꞻ' => 'Ꞻ',
+ 'ꞽ' => 'Ꞽ',
+ 'ꞿ' => 'Ꞿ',
+ 'ꟃ' => 'Ꟃ',
+ 'ꟈ' => 'Ꟈ',
+ 'ꟊ' => 'Ꟊ',
+ 'ꟶ' => 'Ꟶ',
+ 'ꭓ' => 'Ꭓ',
+ 'ꭰ' => 'Ꭰ',
+ 'ꭱ' => 'Ꭱ',
+ 'ꭲ' => 'Ꭲ',
+ 'ꭳ' => 'Ꭳ',
+ 'ꭴ' => 'Ꭴ',
+ 'ꭵ' => 'Ꭵ',
+ 'ꭶ' => 'Ꭶ',
+ 'ꭷ' => 'Ꭷ',
+ 'ꭸ' => 'Ꭸ',
+ 'ꭹ' => 'Ꭹ',
+ 'ꭺ' => 'Ꭺ',
+ 'ꭻ' => 'Ꭻ',
+ 'ꭼ' => 'Ꭼ',
+ 'ꭽ' => 'Ꭽ',
+ 'ꭾ' => 'Ꭾ',
+ 'ꭿ' => 'Ꭿ',
+ 'ꮀ' => 'Ꮀ',
+ 'ꮁ' => 'Ꮁ',
+ 'ꮂ' => 'Ꮂ',
+ 'ꮃ' => 'Ꮃ',
+ 'ꮄ' => 'Ꮄ',
+ 'ꮅ' => 'Ꮅ',
+ 'ꮆ' => 'Ꮆ',
+ 'ꮇ' => 'Ꮇ',
+ 'ꮈ' => 'Ꮈ',
+ 'ꮉ' => 'Ꮉ',
+ 'ꮊ' => 'Ꮊ',
+ 'ꮋ' => 'Ꮋ',
+ 'ꮌ' => 'Ꮌ',
+ 'ꮍ' => 'Ꮍ',
+ 'ꮎ' => 'Ꮎ',
+ 'ꮏ' => 'Ꮏ',
+ 'ꮐ' => 'Ꮐ',
+ 'ꮑ' => 'Ꮑ',
+ 'ꮒ' => 'Ꮒ',
+ 'ꮓ' => 'Ꮓ',
+ 'ꮔ' => 'Ꮔ',
+ 'ꮕ' => 'Ꮕ',
+ 'ꮖ' => 'Ꮖ',
+ 'ꮗ' => 'Ꮗ',
+ 'ꮘ' => 'Ꮘ',
+ 'ꮙ' => 'Ꮙ',
+ 'ꮚ' => 'Ꮚ',
+ 'ꮛ' => 'Ꮛ',
+ 'ꮜ' => 'Ꮜ',
+ 'ꮝ' => 'Ꮝ',
+ 'ꮞ' => 'Ꮞ',
+ 'ꮟ' => 'Ꮟ',
+ 'ꮠ' => 'Ꮠ',
+ 'ꮡ' => 'Ꮡ',
+ 'ꮢ' => 'Ꮢ',
+ 'ꮣ' => 'Ꮣ',
+ 'ꮤ' => 'Ꮤ',
+ 'ꮥ' => 'Ꮥ',
+ 'ꮦ' => 'Ꮦ',
+ 'ꮧ' => 'Ꮧ',
+ 'ꮨ' => 'Ꮨ',
+ 'ꮩ' => 'Ꮩ',
+ 'ꮪ' => 'Ꮪ',
+ 'ꮫ' => 'Ꮫ',
+ 'ꮬ' => 'Ꮬ',
+ 'ꮭ' => 'Ꮭ',
+ 'ꮮ' => 'Ꮮ',
+ 'ꮯ' => 'Ꮯ',
+ 'ꮰ' => 'Ꮰ',
+ 'ꮱ' => 'Ꮱ',
+ 'ꮲ' => 'Ꮲ',
+ 'ꮳ' => 'Ꮳ',
+ 'ꮴ' => 'Ꮴ',
+ 'ꮵ' => 'Ꮵ',
+ 'ꮶ' => 'Ꮶ',
+ 'ꮷ' => 'Ꮷ',
+ 'ꮸ' => 'Ꮸ',
+ 'ꮹ' => 'Ꮹ',
+ 'ꮺ' => 'Ꮺ',
+ 'ꮻ' => 'Ꮻ',
+ 'ꮼ' => 'Ꮼ',
+ 'ꮽ' => 'Ꮽ',
+ 'ꮾ' => 'Ꮾ',
+ 'ꮿ' => 'Ꮿ',
+ 'a' => 'A',
+ 'b' => 'B',
+ 'c' => 'C',
+ 'd' => 'D',
+ 'e' => 'E',
+ 'f' => 'F',
+ 'g' => 'G',
+ 'h' => 'H',
+ 'i' => 'I',
+ 'j' => 'J',
+ 'k' => 'K',
+ 'l' => 'L',
+ 'm' => 'M',
+ 'n' => 'N',
+ 'o' => 'O',
+ 'p' => 'P',
+ 'q' => 'Q',
+ 'r' => 'R',
+ 's' => 'S',
+ 't' => 'T',
+ 'u' => 'U',
+ 'v' => 'V',
+ 'w' => 'W',
+ 'x' => 'X',
+ 'y' => 'Y',
+ 'z' => 'Z',
+ '𐐨' => '𐐀',
+ '𐐩' => '𐐁',
+ '𐐪' => '𐐂',
+ '𐐫' => '𐐃',
+ '𐐬' => '𐐄',
+ '𐐭' => '𐐅',
+ '𐐮' => '𐐆',
+ '𐐯' => '𐐇',
+ '𐐰' => '𐐈',
+ '𐐱' => '𐐉',
+ '𐐲' => '𐐊',
+ '𐐳' => '𐐋',
+ '𐐴' => '𐐌',
+ '𐐵' => '𐐍',
+ '𐐶' => '𐐎',
+ '𐐷' => '𐐏',
+ '𐐸' => '𐐐',
+ '𐐹' => '𐐑',
+ '𐐺' => '𐐒',
+ '𐐻' => '𐐓',
+ '𐐼' => '𐐔',
+ '𐐽' => '𐐕',
+ '𐐾' => '𐐖',
+ '𐐿' => '𐐗',
+ '𐑀' => '𐐘',
+ '𐑁' => '𐐙',
+ '𐑂' => '𐐚',
+ '𐑃' => '𐐛',
+ '𐑄' => '𐐜',
+ '𐑅' => '𐐝',
+ '𐑆' => '𐐞',
+ '𐑇' => '𐐟',
+ '𐑈' => '𐐠',
+ '𐑉' => '𐐡',
+ '𐑊' => '𐐢',
+ '𐑋' => '𐐣',
+ '𐑌' => '𐐤',
+ '𐑍' => '𐐥',
+ '𐑎' => '𐐦',
+ '𐑏' => '𐐧',
+ '𐓘' => '𐒰',
+ '𐓙' => '𐒱',
+ '𐓚' => '𐒲',
+ '𐓛' => '𐒳',
+ '𐓜' => '𐒴',
+ '𐓝' => '𐒵',
+ '𐓞' => '𐒶',
+ '𐓟' => '𐒷',
+ '𐓠' => '𐒸',
+ '𐓡' => '𐒹',
+ '𐓢' => '𐒺',
+ '𐓣' => '𐒻',
+ '𐓤' => '𐒼',
+ '𐓥' => '𐒽',
+ '𐓦' => '𐒾',
+ '𐓧' => '𐒿',
+ '𐓨' => '𐓀',
+ '𐓩' => '𐓁',
+ '𐓪' => '𐓂',
+ '𐓫' => '𐓃',
+ '𐓬' => '𐓄',
+ '𐓭' => '𐓅',
+ '𐓮' => '𐓆',
+ '𐓯' => '𐓇',
+ '𐓰' => '𐓈',
+ '𐓱' => '𐓉',
+ '𐓲' => '𐓊',
+ '𐓳' => '𐓋',
+ '𐓴' => '𐓌',
+ '𐓵' => '𐓍',
+ '𐓶' => '𐓎',
+ '𐓷' => '𐓏',
+ '𐓸' => '𐓐',
+ '𐓹' => '𐓑',
+ '𐓺' => '𐓒',
+ '𐓻' => '𐓓',
+ '𐳀' => '𐲀',
+ '𐳁' => '𐲁',
+ '𐳂' => '𐲂',
+ '𐳃' => '𐲃',
+ '𐳄' => '𐲄',
+ '𐳅' => '𐲅',
+ '𐳆' => '𐲆',
+ '𐳇' => '𐲇',
+ '𐳈' => '𐲈',
+ '𐳉' => '𐲉',
+ '𐳊' => '𐲊',
+ '𐳋' => '𐲋',
+ '𐳌' => '𐲌',
+ '𐳍' => '𐲍',
+ '𐳎' => '𐲎',
+ '𐳏' => '𐲏',
+ '𐳐' => '𐲐',
+ '𐳑' => '𐲑',
+ '𐳒' => '𐲒',
+ '𐳓' => '𐲓',
+ '𐳔' => '𐲔',
+ '𐳕' => '𐲕',
+ '𐳖' => '𐲖',
+ '𐳗' => '𐲗',
+ '𐳘' => '𐲘',
+ '𐳙' => '𐲙',
+ '𐳚' => '𐲚',
+ '𐳛' => '𐲛',
+ '𐳜' => '𐲜',
+ '𐳝' => '𐲝',
+ '𐳞' => '𐲞',
+ '𐳟' => '𐲟',
+ '𐳠' => '𐲠',
+ '𐳡' => '𐲡',
+ '𐳢' => '𐲢',
+ '𐳣' => '𐲣',
+ '𐳤' => '𐲤',
+ '𐳥' => '𐲥',
+ '𐳦' => '𐲦',
+ '𐳧' => '𐲧',
+ '𐳨' => '𐲨',
+ '𐳩' => '𐲩',
+ '𐳪' => '𐲪',
+ '𐳫' => '𐲫',
+ '𐳬' => '𐲬',
+ '𐳭' => '𐲭',
+ '𐳮' => '𐲮',
+ '𐳯' => '𐲯',
+ '𐳰' => '𐲰',
+ '𐳱' => '𐲱',
+ '𐳲' => '𐲲',
+ '𑣀' => '𑢠',
+ '𑣁' => '𑢡',
+ '𑣂' => '𑢢',
+ '𑣃' => '𑢣',
+ '𑣄' => '𑢤',
+ '𑣅' => '𑢥',
+ '𑣆' => '𑢦',
+ '𑣇' => '𑢧',
+ '𑣈' => '𑢨',
+ '𑣉' => '𑢩',
+ '𑣊' => '𑢪',
+ '𑣋' => '𑢫',
+ '𑣌' => '𑢬',
+ '𑣍' => '𑢭',
+ '𑣎' => '𑢮',
+ '𑣏' => '𑢯',
+ '𑣐' => '𑢰',
+ '𑣑' => '𑢱',
+ '𑣒' => '𑢲',
+ '𑣓' => '𑢳',
+ '𑣔' => '𑢴',
+ '𑣕' => '𑢵',
+ '𑣖' => '𑢶',
+ '𑣗' => '𑢷',
+ '𑣘' => '𑢸',
+ '𑣙' => '𑢹',
+ '𑣚' => '𑢺',
+ '𑣛' => '𑢻',
+ '𑣜' => '𑢼',
+ '𑣝' => '𑢽',
+ '𑣞' => '𑢾',
+ '𑣟' => '𑢿',
+ '𖹠' => '𖹀',
+ '𖹡' => '𖹁',
+ '𖹢' => '𖹂',
+ '𖹣' => '𖹃',
+ '𖹤' => '𖹄',
+ '𖹥' => '𖹅',
+ '𖹦' => '𖹆',
+ '𖹧' => '𖹇',
+ '𖹨' => '𖹈',
+ '𖹩' => '𖹉',
+ '𖹪' => '𖹊',
+ '𖹫' => '𖹋',
+ '𖹬' => '𖹌',
+ '𖹭' => '𖹍',
+ '𖹮' => '𖹎',
+ '𖹯' => '𖹏',
+ '𖹰' => '𖹐',
+ '𖹱' => '𖹑',
+ '𖹲' => '𖹒',
+ '𖹳' => '𖹓',
+ '𖹴' => '𖹔',
+ '𖹵' => '𖹕',
+ '𖹶' => '𖹖',
+ '𖹷' => '𖹗',
+ '𖹸' => '𖹘',
+ '𖹹' => '𖹙',
+ '𖹺' => '𖹚',
+ '𖹻' => '𖹛',
+ '𖹼' => '𖹜',
+ '𖹽' => '𖹝',
+ '𖹾' => '𖹞',
+ '𖹿' => '𖹟',
+ '𞤢' => '𞤀',
+ '𞤣' => '𞤁',
+ '𞤤' => '𞤂',
+ '𞤥' => '𞤃',
+ '𞤦' => '𞤄',
+ '𞤧' => '𞤅',
+ '𞤨' => '𞤆',
+ '𞤩' => '𞤇',
+ '𞤪' => '𞤈',
+ '𞤫' => '𞤉',
+ '𞤬' => '𞤊',
+ '𞤭' => '𞤋',
+ '𞤮' => '𞤌',
+ '𞤯' => '𞤍',
+ '𞤰' => '𞤎',
+ '𞤱' => '𞤏',
+ '𞤲' => '𞤐',
+ '𞤳' => '𞤑',
+ '𞤴' => '𞤒',
+ '𞤵' => '𞤓',
+ '𞤶' => '𞤔',
+ '𞤷' => '𞤕',
+ '𞤸' => '𞤖',
+ '𞤹' => '𞤗',
+ '𞤺' => '𞤘',
+ '𞤻' => '𞤙',
+ '𞤼' => '𞤚',
+ '𞤽' => '𞤛',
+ '𞤾' => '𞤜',
+ '𞤿' => '𞤝',
+ '𞥀' => '𞤞',
+ '𞥁' => '𞤟',
+ '𞥂' => '𞤠',
+ '𞥃' => '𞤡',
+ 'ß' => 'SS',
+ 'ff' => 'FF',
+ 'fi' => 'FI',
+ 'fl' => 'FL',
+ 'ffi' => 'FFI',
+ 'ffl' => 'FFL',
+ 'ſt' => 'ST',
+ 'st' => 'ST',
+ 'և' => 'ԵՒ',
+ 'ﬓ' => 'ՄՆ',
+ 'ﬔ' => 'ՄԵ',
+ 'ﬕ' => 'ՄԻ',
+ 'ﬖ' => 'ՎՆ',
+ 'ﬗ' => 'ՄԽ',
+ 'ʼn' => 'ʼN',
+ 'ΐ' => 'Ϊ́',
+ 'ΰ' => 'Ϋ́',
+ 'ǰ' => 'J̌',
+ 'ẖ' => 'H̱',
+ 'ẗ' => 'T̈',
+ 'ẘ' => 'W̊',
+ 'ẙ' => 'Y̊',
+ 'ẚ' => 'Aʾ',
+ 'ὐ' => 'Υ̓',
+ 'ὒ' => 'Υ̓̀',
+ 'ὔ' => 'Υ̓́',
+ 'ὖ' => 'Υ̓͂',
+ 'ᾶ' => 'Α͂',
+ 'ῆ' => 'Η͂',
+ 'ῒ' => 'Ϊ̀',
+ 'ΐ' => 'Ϊ́',
+ 'ῖ' => 'Ι͂',
+ 'ῗ' => 'Ϊ͂',
+ 'ῢ' => 'Ϋ̀',
+ 'ΰ' => 'Ϋ́',
+ 'ῤ' => 'Ρ̓',
+ 'ῦ' => 'Υ͂',
+ 'ῧ' => 'Ϋ͂',
+ 'ῶ' => 'Ω͂',
+ 'ᾈ' => 'ἈΙ',
+ 'ᾉ' => 'ἉΙ',
+ 'ᾊ' => 'ἊΙ',
+ 'ᾋ' => 'ἋΙ',
+ 'ᾌ' => 'ἌΙ',
+ 'ᾍ' => 'ἍΙ',
+ 'ᾎ' => 'ἎΙ',
+ 'ᾏ' => 'ἏΙ',
+ 'ᾘ' => 'ἨΙ',
+ 'ᾙ' => 'ἩΙ',
+ 'ᾚ' => 'ἪΙ',
+ 'ᾛ' => 'ἫΙ',
+ 'ᾜ' => 'ἬΙ',
+ 'ᾝ' => 'ἭΙ',
+ 'ᾞ' => 'ἮΙ',
+ 'ᾟ' => 'ἯΙ',
+ 'ᾨ' => 'ὨΙ',
+ 'ᾩ' => 'ὩΙ',
+ 'ᾪ' => 'ὪΙ',
+ 'ᾫ' => 'ὫΙ',
+ 'ᾬ' => 'ὬΙ',
+ 'ᾭ' => 'ὭΙ',
+ 'ᾮ' => 'ὮΙ',
+ 'ᾯ' => 'ὯΙ',
+ 'ᾼ' => 'ΑΙ',
+ 'ῌ' => 'ΗΙ',
+ 'ῼ' => 'ΩΙ',
+ 'ᾲ' => 'ᾺΙ',
+ 'ᾴ' => 'ΆΙ',
+ 'ῂ' => 'ῊΙ',
+ 'ῄ' => 'ΉΙ',
+ 'ῲ' => 'ῺΙ',
+ 'ῴ' => 'ΏΙ',
+ 'ᾷ' => 'Α͂Ι',
+ 'ῇ' => 'Η͂Ι',
+ 'ῷ' => 'Ω͂Ι',
+);
diff --git a/vendor/symfony/polyfill-mbstring/bootstrap.php b/vendor/symfony/polyfill-mbstring/bootstrap.php
new file mode 100644
index 0000000..ff51ae0
--- /dev/null
+++ b/vendor/symfony/polyfill-mbstring/bootstrap.php
@@ -0,0 +1,172 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+use Symfony\Polyfill\Mbstring as p;
+
+if (\PHP_VERSION_ID >= 80000) {
+ return require __DIR__.'/bootstrap80.php';
+}
+
+if (!function_exists('mb_convert_encoding')) {
+ function mb_convert_encoding($string, $to_encoding, $from_encoding = null) { return p\Mbstring::mb_convert_encoding($string, $to_encoding, $from_encoding); }
+}
+if (!function_exists('mb_decode_mimeheader')) {
+ function mb_decode_mimeheader($string) { return p\Mbstring::mb_decode_mimeheader($string); }
+}
+if (!function_exists('mb_encode_mimeheader')) {
+ function mb_encode_mimeheader($string, $charset = null, $transfer_encoding = null, $newline = "\r\n", $indent = 0) { return p\Mbstring::mb_encode_mimeheader($string, $charset, $transfer_encoding, $newline, $indent); }
+}
+if (!function_exists('mb_decode_numericentity')) {
+ function mb_decode_numericentity($string, $map, $encoding = null) { return p\Mbstring::mb_decode_numericentity($string, $map, $encoding); }
+}
+if (!function_exists('mb_encode_numericentity')) {
+ function mb_encode_numericentity($string, $map, $encoding = null, $hex = false) { return p\Mbstring::mb_encode_numericentity($string, $map, $encoding, $hex); }
+}
+if (!function_exists('mb_convert_case')) {
+ function mb_convert_case($string, $mode, $encoding = null) { return p\Mbstring::mb_convert_case($string, $mode, $encoding); }
+}
+if (!function_exists('mb_internal_encoding')) {
+ function mb_internal_encoding($encoding = null) { return p\Mbstring::mb_internal_encoding($encoding); }
+}
+if (!function_exists('mb_language')) {
+ function mb_language($language = null) { return p\Mbstring::mb_language($language); }
+}
+if (!function_exists('mb_list_encodings')) {
+ function mb_list_encodings() { return p\Mbstring::mb_list_encodings(); }
+}
+if (!function_exists('mb_encoding_aliases')) {
+ function mb_encoding_aliases($encoding) { return p\Mbstring::mb_encoding_aliases($encoding); }
+}
+if (!function_exists('mb_check_encoding')) {
+ function mb_check_encoding($value = null, $encoding = null) { return p\Mbstring::mb_check_encoding($value, $encoding); }
+}
+if (!function_exists('mb_detect_encoding')) {
+ function mb_detect_encoding($string, $encodings = null, $strict = false) { return p\Mbstring::mb_detect_encoding($string, $encodings, $strict); }
+}
+if (!function_exists('mb_detect_order')) {
+ function mb_detect_order($encoding = null) { return p\Mbstring::mb_detect_order($encoding); }
+}
+if (!function_exists('mb_parse_str')) {
+ function mb_parse_str($string, &$result = []) { parse_str($string, $result); return (bool) $result; }
+}
+if (!function_exists('mb_strlen')) {
+ function mb_strlen($string, $encoding = null) { return p\Mbstring::mb_strlen($string, $encoding); }
+}
+if (!function_exists('mb_strpos')) {
+ function mb_strpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strpos($haystack, $needle, $offset, $encoding); }
+}
+if (!function_exists('mb_strtolower')) {
+ function mb_strtolower($string, $encoding = null) { return p\Mbstring::mb_strtolower($string, $encoding); }
+}
+if (!function_exists('mb_strtoupper')) {
+ function mb_strtoupper($string, $encoding = null) { return p\Mbstring::mb_strtoupper($string, $encoding); }
+}
+if (!function_exists('mb_substitute_character')) {
+ function mb_substitute_character($substitute_character = null) { return p\Mbstring::mb_substitute_character($substitute_character); }
+}
+if (!function_exists('mb_substr')) {
+ function mb_substr($string, $start, $length = 2147483647, $encoding = null) { return p\Mbstring::mb_substr($string, $start, $length, $encoding); }
+}
+if (!function_exists('mb_stripos')) {
+ function mb_stripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_stripos($haystack, $needle, $offset, $encoding); }
+}
+if (!function_exists('mb_stristr')) {
+ function mb_stristr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_stristr($haystack, $needle, $before_needle, $encoding); }
+}
+if (!function_exists('mb_strrchr')) {
+ function mb_strrchr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrchr($haystack, $needle, $before_needle, $encoding); }
+}
+if (!function_exists('mb_strrichr')) {
+ function mb_strrichr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrichr($haystack, $needle, $before_needle, $encoding); }
+}
+if (!function_exists('mb_strripos')) {
+ function mb_strripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strripos($haystack, $needle, $offset, $encoding); }
+}
+if (!function_exists('mb_strrpos')) {
+ function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strrpos($haystack, $needle, $offset, $encoding); }
+}
+if (!function_exists('mb_strstr')) {
+ function mb_strstr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strstr($haystack, $needle, $before_needle, $encoding); }
+}
+if (!function_exists('mb_get_info')) {
+ function mb_get_info($type = 'all') { return p\Mbstring::mb_get_info($type); }
+}
+if (!function_exists('mb_http_output')) {
+ function mb_http_output($encoding = null) { return p\Mbstring::mb_http_output($encoding); }
+}
+if (!function_exists('mb_strwidth')) {
+ function mb_strwidth($string, $encoding = null) { return p\Mbstring::mb_strwidth($string, $encoding); }
+}
+if (!function_exists('mb_substr_count')) {
+ function mb_substr_count($haystack, $needle, $encoding = null) { return p\Mbstring::mb_substr_count($haystack, $needle, $encoding); }
+}
+if (!function_exists('mb_output_handler')) {
+ function mb_output_handler($string, $status) { return p\Mbstring::mb_output_handler($string, $status); }
+}
+if (!function_exists('mb_http_input')) {
+ function mb_http_input($type = null) { return p\Mbstring::mb_http_input($type); }
+}
+
+if (!function_exists('mb_convert_variables')) {
+ function mb_convert_variables($to_encoding, $from_encoding, &...$vars) { return p\Mbstring::mb_convert_variables($to_encoding, $from_encoding, ...$vars); }
+}
+
+if (!function_exists('mb_ord')) {
+ function mb_ord($string, $encoding = null) { return p\Mbstring::mb_ord($string, $encoding); }
+}
+if (!function_exists('mb_chr')) {
+ function mb_chr($codepoint, $encoding = null) { return p\Mbstring::mb_chr($codepoint, $encoding); }
+}
+if (!function_exists('mb_scrub')) {
+ function mb_scrub($string, $encoding = null) { $encoding = null === $encoding ? mb_internal_encoding() : $encoding; return mb_convert_encoding($string, $encoding, $encoding); }
+}
+if (!function_exists('mb_str_split')) {
+ function mb_str_split($string, $length = 1, $encoding = null) { return p\Mbstring::mb_str_split($string, $length, $encoding); }
+}
+
+if (!function_exists('mb_str_pad')) {
+ function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string { return p\Mbstring::mb_str_pad($string, $length, $pad_string, $pad_type, $encoding); }
+}
+
+if (!function_exists('mb_ucfirst')) {
+ function mb_ucfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_ucfirst($string, $encoding); }
+}
+
+if (!function_exists('mb_lcfirst')) {
+ function mb_lcfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_lcfirst($string, $encoding); }
+}
+
+if (!function_exists('mb_trim')) {
+ function mb_trim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_trim($string, $characters, $encoding); }
+}
+
+if (!function_exists('mb_ltrim')) {
+ function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_ltrim($string, $characters, $encoding); }
+}
+
+if (!function_exists('mb_rtrim')) {
+ function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_rtrim($string, $characters, $encoding); }
+}
+
+
+if (extension_loaded('mbstring')) {
+ return;
+}
+
+if (!defined('MB_CASE_UPPER')) {
+ define('MB_CASE_UPPER', 0);
+}
+if (!defined('MB_CASE_LOWER')) {
+ define('MB_CASE_LOWER', 1);
+}
+if (!defined('MB_CASE_TITLE')) {
+ define('MB_CASE_TITLE', 2);
+}
diff --git a/vendor/symfony/polyfill-mbstring/bootstrap80.php b/vendor/symfony/polyfill-mbstring/bootstrap80.php
new file mode 100644
index 0000000..5be7d20
--- /dev/null
+++ b/vendor/symfony/polyfill-mbstring/bootstrap80.php
@@ -0,0 +1,167 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+use Symfony\Polyfill\Mbstring as p;
+
+if (!function_exists('mb_convert_encoding')) {
+ function mb_convert_encoding(array|string|null $string, ?string $to_encoding, array|string|null $from_encoding = null): array|string|false { return p\Mbstring::mb_convert_encoding($string ?? '', (string) $to_encoding, $from_encoding); }
+}
+if (!function_exists('mb_decode_mimeheader')) {
+ function mb_decode_mimeheader(?string $string): string { return p\Mbstring::mb_decode_mimeheader((string) $string); }
+}
+if (!function_exists('mb_encode_mimeheader')) {
+ function mb_encode_mimeheader(?string $string, ?string $charset = null, ?string $transfer_encoding = null, ?string $newline = "\r\n", ?int $indent = 0): string { return p\Mbstring::mb_encode_mimeheader((string) $string, $charset, $transfer_encoding, (string) $newline, (int) $indent); }
+}
+if (!function_exists('mb_decode_numericentity')) {
+ function mb_decode_numericentity(?string $string, array $map, ?string $encoding = null): string { return p\Mbstring::mb_decode_numericentity((string) $string, $map, $encoding); }
+}
+if (!function_exists('mb_encode_numericentity')) {
+ function mb_encode_numericentity(?string $string, array $map, ?string $encoding = null, ?bool $hex = false): string { return p\Mbstring::mb_encode_numericentity((string) $string, $map, $encoding, (bool) $hex); }
+}
+if (!function_exists('mb_convert_case')) {
+ function mb_convert_case(?string $string, ?int $mode, ?string $encoding = null): string { return p\Mbstring::mb_convert_case((string) $string, (int) $mode, $encoding); }
+}
+if (!function_exists('mb_internal_encoding')) {
+ function mb_internal_encoding(?string $encoding = null): string|bool { return p\Mbstring::mb_internal_encoding($encoding); }
+}
+if (!function_exists('mb_language')) {
+ function mb_language(?string $language = null): string|bool { return p\Mbstring::mb_language($language); }
+}
+if (!function_exists('mb_list_encodings')) {
+ function mb_list_encodings(): array { return p\Mbstring::mb_list_encodings(); }
+}
+if (!function_exists('mb_encoding_aliases')) {
+ function mb_encoding_aliases(?string $encoding): array { return p\Mbstring::mb_encoding_aliases((string) $encoding); }
+}
+if (!function_exists('mb_check_encoding')) {
+ function mb_check_encoding(array|string|null $value = null, ?string $encoding = null): bool { return p\Mbstring::mb_check_encoding($value, $encoding); }
+}
+if (!function_exists('mb_detect_encoding')) {
+ function mb_detect_encoding(?string $string, array|string|null $encodings = null, ?bool $strict = false): string|false { return p\Mbstring::mb_detect_encoding((string) $string, $encodings, (bool) $strict); }
+}
+if (!function_exists('mb_detect_order')) {
+ function mb_detect_order(array|string|null $encoding = null): array|bool { return p\Mbstring::mb_detect_order($encoding); }
+}
+if (!function_exists('mb_parse_str')) {
+ function mb_parse_str(?string $string, &$result = []): bool { parse_str((string) $string, $result); return (bool) $result; }
+}
+if (!function_exists('mb_strlen')) {
+ function mb_strlen(?string $string, ?string $encoding = null): int { return p\Mbstring::mb_strlen((string) $string, $encoding); }
+}
+if (!function_exists('mb_strpos')) {
+ function mb_strpos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strpos((string) $haystack, (string) $needle, (int) $offset, $encoding); }
+}
+if (!function_exists('mb_strtolower')) {
+ function mb_strtolower(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_strtolower((string) $string, $encoding); }
+}
+if (!function_exists('mb_strtoupper')) {
+ function mb_strtoupper(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_strtoupper((string) $string, $encoding); }
+}
+if (!function_exists('mb_substitute_character')) {
+ function mb_substitute_character(string|int|null $substitute_character = null): string|int|bool { return p\Mbstring::mb_substitute_character($substitute_character); }
+}
+if (!function_exists('mb_substr')) {
+ function mb_substr(?string $string, ?int $start, ?int $length = null, ?string $encoding = null): string { return p\Mbstring::mb_substr((string) $string, (int) $start, $length, $encoding); }
+}
+if (!function_exists('mb_stripos')) {
+ function mb_stripos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_stripos((string) $haystack, (string) $needle, (int) $offset, $encoding); }
+}
+if (!function_exists('mb_stristr')) {
+ function mb_stristr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_stristr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); }
+}
+if (!function_exists('mb_strrchr')) {
+ function mb_strrchr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strrchr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); }
+}
+if (!function_exists('mb_strrichr')) {
+ function mb_strrichr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strrichr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); }
+}
+if (!function_exists('mb_strripos')) {
+ function mb_strripos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strripos((string) $haystack, (string) $needle, (int) $offset, $encoding); }
+}
+if (!function_exists('mb_strrpos')) {
+ function mb_strrpos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strrpos((string) $haystack, (string) $needle, (int) $offset, $encoding); }
+}
+if (!function_exists('mb_strstr')) {
+ function mb_strstr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strstr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); }
+}
+if (!function_exists('mb_get_info')) {
+ function mb_get_info(?string $type = 'all'): array|string|int|false|null { return p\Mbstring::mb_get_info((string) $type); }
+}
+if (!function_exists('mb_http_output')) {
+ function mb_http_output(?string $encoding = null): string|bool { return p\Mbstring::mb_http_output($encoding); }
+}
+if (!function_exists('mb_strwidth')) {
+ function mb_strwidth(?string $string, ?string $encoding = null): int { return p\Mbstring::mb_strwidth((string) $string, $encoding); }
+}
+if (!function_exists('mb_substr_count')) {
+ function mb_substr_count(?string $haystack, ?string $needle, ?string $encoding = null): int { return p\Mbstring::mb_substr_count((string) $haystack, (string) $needle, $encoding); }
+}
+if (!function_exists('mb_output_handler')) {
+ function mb_output_handler(?string $string, ?int $status): string { return p\Mbstring::mb_output_handler((string) $string, (int) $status); }
+}
+if (!function_exists('mb_http_input')) {
+ function mb_http_input(?string $type = null): array|string|false { return p\Mbstring::mb_http_input($type); }
+}
+
+if (!function_exists('mb_convert_variables')) {
+ function mb_convert_variables(?string $to_encoding, array|string|null $from_encoding, mixed &$var, mixed &...$vars): string|false { return p\Mbstring::mb_convert_variables((string) $to_encoding, $from_encoding ?? '', $var, ...$vars); }
+}
+
+if (!function_exists('mb_ord')) {
+ function mb_ord(?string $string, ?string $encoding = null): int|false { return p\Mbstring::mb_ord((string) $string, $encoding); }
+}
+if (!function_exists('mb_chr')) {
+ function mb_chr(?int $codepoint, ?string $encoding = null): string|false { return p\Mbstring::mb_chr((int) $codepoint, $encoding); }
+}
+if (!function_exists('mb_scrub')) {
+ function mb_scrub(?string $string, ?string $encoding = null): string { $encoding ??= mb_internal_encoding(); return mb_convert_encoding((string) $string, $encoding, $encoding); }
+}
+if (!function_exists('mb_str_split')) {
+ function mb_str_split(?string $string, ?int $length = 1, ?string $encoding = null): array { return p\Mbstring::mb_str_split((string) $string, (int) $length, $encoding); }
+}
+
+if (!function_exists('mb_str_pad')) {
+ function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string { return p\Mbstring::mb_str_pad($string, $length, $pad_string, $pad_type, $encoding); }
+}
+
+if (!function_exists('mb_ucfirst')) {
+ function mb_ucfirst($string, ?string $encoding = null): string { return p\Mbstring::mb_ucfirst($string, $encoding); }
+}
+
+if (!function_exists('mb_lcfirst')) {
+ function mb_lcfirst($string, ?string $encoding = null): string { return p\Mbstring::mb_lcfirst($string, $encoding); }
+}
+
+if (!function_exists('mb_trim')) {
+ function mb_trim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_trim($string, $characters, $encoding); }
+}
+
+if (!function_exists('mb_ltrim')) {
+ function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_ltrim($string, $characters, $encoding); }
+}
+
+if (!function_exists('mb_rtrim')) {
+ function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_rtrim($string, $characters, $encoding); }
+}
+
+if (extension_loaded('mbstring')) {
+ return;
+}
+
+if (!defined('MB_CASE_UPPER')) {
+ define('MB_CASE_UPPER', 0);
+}
+if (!defined('MB_CASE_LOWER')) {
+ define('MB_CASE_LOWER', 1);
+}
+if (!defined('MB_CASE_TITLE')) {
+ define('MB_CASE_TITLE', 2);
+}
diff --git a/vendor/symfony/polyfill-mbstring/composer.json b/vendor/symfony/polyfill-mbstring/composer.json
new file mode 100644
index 0000000..4ed241a
--- /dev/null
+++ b/vendor/symfony/polyfill-mbstring/composer.json
@@ -0,0 +1,38 @@
+{
+ "name": "symfony/polyfill-mbstring",
+ "type": "library",
+ "description": "Symfony polyfill for the Mbstring extension",
+ "keywords": ["polyfill", "shim", "compatibility", "portable", "mbstring"],
+ "homepage": "https://symfony.com",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "require": {
+ "php": ">=7.2"
+ },
+ "provide": {
+ "ext-mbstring": "*"
+ },
+ "autoload": {
+ "psr-4": { "Symfony\\Polyfill\\Mbstring\\": "" },
+ "files": [ "bootstrap.php" ]
+ },
+ "suggest": {
+ "ext-mbstring": "For best performance"
+ },
+ "minimum-stability": "dev",
+ "extra": {
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ }
+}