Reintroduce Webklex IMAP for ticket processing as PHP-IMAP is no longer being developed. This is optional for now and considered beta can be found in cron/ticket_email_parser.php

This commit is contained in:
johnnyq
2025-09-10 14:27:46 -04:00
parent 981fb9585d
commit ce7d84aa2f
2035 changed files with 174115 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
CHANGELOG
=========
7.1
---
* Add `DatePoint::getMicrosecond()` and `DatePoint::setMicrosecond()`
6.4
---
* Add `DatePoint`: an immutable DateTime implementation with stricter error handling and return types
* Throw `DateMalformedStringException`/`DateInvalidTimeZoneException` when appropriate
* Add `$modifier` argument to the `now()` helper
6.3
---
* Add `ClockAwareTrait` to help write time-sensitive classes
* Add `Clock` class and `now()` function
6.2
---
* Add the component

89
plugins/vendor/symfony/clock/Clock.php vendored Normal file
View File

@@ -0,0 +1,89 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Clock;
use Psr\Clock\ClockInterface as PsrClockInterface;
/**
* A global clock.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class Clock implements ClockInterface
{
private static ClockInterface $globalClock;
public function __construct(
private readonly ?PsrClockInterface $clock = null,
private ?\DateTimeZone $timezone = null,
) {
}
/**
* Returns the current global clock.
*
* Note that you should prefer injecting a ClockInterface or using
* ClockAwareTrait when possible instead of using this method.
*/
public static function get(): ClockInterface
{
return self::$globalClock ??= new NativeClock();
}
public static function set(PsrClockInterface $clock): void
{
self::$globalClock = $clock instanceof ClockInterface ? $clock : new self($clock);
}
public function now(): DatePoint
{
$now = ($this->clock ?? self::get())->now();
if (!$now instanceof DatePoint) {
$now = DatePoint::createFromInterface($now);
}
return isset($this->timezone) ? $now->setTimezone($this->timezone) : $now;
}
public function sleep(float|int $seconds): void
{
$clock = $this->clock ?? self::get();
if ($clock instanceof ClockInterface) {
$clock->sleep($seconds);
} else {
(new NativeClock())->sleep($seconds);
}
}
/**
* @throws \DateInvalidTimeZoneException When $timezone is invalid
*/
public function withTimeZone(\DateTimeZone|string $timezone): static
{
if (\PHP_VERSION_ID >= 80300 && \is_string($timezone)) {
$timezone = new \DateTimeZone($timezone);
} elseif (\is_string($timezone)) {
try {
$timezone = new \DateTimeZone($timezone);
} catch (\Exception $e) {
throw new \DateInvalidTimeZoneException($e->getMessage(), $e->getCode(), $e);
}
}
$clone = clone $this;
$clone->timezone = $timezone;
return $clone;
}
}

View File

@@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Clock;
use Psr\Clock\ClockInterface;
use Symfony\Contracts\Service\Attribute\Required;
/**
* A trait to help write time-sensitive classes.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
trait ClockAwareTrait
{
private readonly ClockInterface $clock;
#[Required]
public function setClock(ClockInterface $clock): void
{
$this->clock = $clock;
}
protected function now(): DatePoint
{
$now = ($this->clock ??= new Clock())->now();
return $now instanceof DatePoint ? $now : DatePoint::createFromInterface($now);
}
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Clock;
use Psr\Clock\ClockInterface as PsrClockInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
interface ClockInterface extends PsrClockInterface
{
public function sleep(float|int $seconds): void;
public function withTimeZone(\DateTimeZone|string $timezone): static;
}

View File

@@ -0,0 +1,169 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Clock;
/**
* An immmutable DateTime with stricter error handling and return types than the native one.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class DatePoint extends \DateTimeImmutable
{
/**
* @throws \DateMalformedStringException When $datetime is invalid
*/
public function __construct(string $datetime = 'now', ?\DateTimeZone $timezone = null, ?parent $reference = null)
{
$now = $reference ?? Clock::get()->now();
if ('now' !== $datetime) {
if (!$now instanceof static) {
$now = static::createFromInterface($now);
}
if (\PHP_VERSION_ID < 80300) {
try {
$builtInDate = new parent($datetime, $timezone ?? $now->getTimezone());
$timezone = $builtInDate->getTimezone();
} catch (\Exception $e) {
throw new \DateMalformedStringException($e->getMessage(), $e->getCode(), $e);
}
} else {
$builtInDate = new parent($datetime, $timezone ?? $now->getTimezone());
$timezone = $builtInDate->getTimezone();
}
$now = $now->setTimezone($timezone)->modify($datetime);
if ('00:00:00.000000' === $builtInDate->format('H:i:s.u')) {
$now = $now->setTime(0, 0);
}
} elseif (null !== $timezone) {
$now = $now->setTimezone($timezone);
}
$this->__unserialize((array) $now);
}
/**
* @throws \DateMalformedStringException When $format or $datetime are invalid
*/
public static function createFromFormat(string $format, string $datetime, ?\DateTimeZone $timezone = null): static
{
return parent::createFromFormat($format, $datetime, $timezone) ?: throw new \DateMalformedStringException(static::getLastErrors()['errors'][0] ?? 'Invalid date string or format.');
}
public static function createFromInterface(\DateTimeInterface $object): static
{
return parent::createFromInterface($object);
}
public static function createFromMutable(\DateTime $object): static
{
return parent::createFromMutable($object);
}
public static function createFromTimestamp(int|float $timestamp): static
{
if (\PHP_VERSION_ID >= 80400) {
return parent::createFromTimestamp($timestamp);
}
if (\is_int($timestamp) || !$ms = (int) $timestamp - $timestamp) {
return static::createFromFormat('U', (string) $timestamp);
}
if (!is_finite($timestamp) || \PHP_INT_MAX + 1.0 <= $timestamp || \PHP_INT_MIN > $timestamp) {
throw new \DateRangeError(\sprintf('DateTimeImmutable::createFromTimestamp(): Argument #1 ($timestamp) must be a finite number between %s and %s.999999, %s given', \PHP_INT_MIN, \PHP_INT_MAX, $timestamp));
}
if ($timestamp < 0) {
$timestamp = (int) $timestamp - 2.0 + $ms;
}
return static::createFromFormat('U.u', \sprintf('%.6F', $timestamp));
}
public function add(\DateInterval $interval): static
{
return parent::add($interval);
}
public function sub(\DateInterval $interval): static
{
return parent::sub($interval);
}
/**
* @throws \DateMalformedStringException When $modifier is invalid
*/
public function modify(string $modifier): static
{
if (\PHP_VERSION_ID < 80300) {
return @parent::modify($modifier) ?: throw new \DateMalformedStringException(error_get_last()['message'] ?? \sprintf('Invalid modifier: "%s".', $modifier));
}
return parent::modify($modifier);
}
public function setTimestamp(int $value): static
{
return parent::setTimestamp($value);
}
public function setDate(int $year, int $month, int $day): static
{
return parent::setDate($year, $month, $day);
}
public function setISODate(int $year, int $week, int $day = 1): static
{
return parent::setISODate($year, $week, $day);
}
public function setTime(int $hour, int $minute, int $second = 0, int $microsecond = 0): static
{
return parent::setTime($hour, $minute, $second, $microsecond);
}
public function setTimezone(\DateTimeZone $timezone): static
{
return parent::setTimezone($timezone);
}
public function getTimezone(): \DateTimeZone
{
return parent::getTimezone() ?: throw new \DateInvalidTimeZoneException('The DatePoint object has no timezone.');
}
public function setMicrosecond(int $microsecond): static
{
if ($microsecond < 0 || $microsecond > 999999) {
throw new \DateRangeError('DatePoint::setMicrosecond(): Argument #1 ($microsecond) must be between 0 and 999999, '.$microsecond.' given');
}
if (\PHP_VERSION_ID < 80400) {
return $this->setTime(...explode('.', $this->format('H.i.s.'.$microsecond)));
}
return parent::setMicrosecond($microsecond);
}
public function getMicrosecond(): int
{
if (\PHP_VERSION_ID >= 80400) {
return parent::getMicrosecond();
}
return $this->format('u');
}
}

19
plugins/vendor/symfony/clock/LICENSE vendored Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2022-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.

View File

@@ -0,0 +1,98 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Clock;
/**
* A clock that always returns the same date, suitable for testing time-sensitive logic.
*
* Consider using ClockSensitiveTrait in your test cases instead of using this class directly.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class MockClock implements ClockInterface
{
private DatePoint $now;
/**
* @throws \DateMalformedStringException When $now is invalid
* @throws \DateInvalidTimeZoneException When $timezone is invalid
*/
public function __construct(\DateTimeImmutable|string $now = 'now', \DateTimeZone|string|null $timezone = null)
{
if (\PHP_VERSION_ID >= 80300 && \is_string($timezone)) {
$timezone = new \DateTimeZone($timezone);
} elseif (\is_string($timezone)) {
try {
$timezone = new \DateTimeZone($timezone);
} catch (\Exception $e) {
throw new \DateInvalidTimeZoneException($e->getMessage(), $e->getCode(), $e);
}
}
if (\is_string($now)) {
$now = new DatePoint($now, $timezone ?? new \DateTimeZone('UTC'));
} elseif (!$now instanceof DatePoint) {
$now = DatePoint::createFromInterface($now);
}
$this->now = null !== $timezone ? $now->setTimezone($timezone) : $now;
}
public function now(): DatePoint
{
return clone $this->now;
}
public function sleep(float|int $seconds): void
{
$now = (float) $this->now->format('Uu') + $seconds * 1e6;
$now = substr_replace(\sprintf('@%07.0F', $now), '.', -6, 0);
$timezone = $this->now->getTimezone();
$this->now = DatePoint::createFromInterface(new \DateTimeImmutable($now, $timezone))->setTimezone($timezone);
}
/**
* @throws \DateMalformedStringException When $modifier is invalid
*/
public function modify(string $modifier): void
{
if (\PHP_VERSION_ID < 80300) {
$this->now = @$this->now->modify($modifier) ?: throw new \DateMalformedStringException(error_get_last()['message'] ?? \sprintf('Invalid modifier: "%s". Could not modify MockClock.', $modifier));
return;
}
$this->now = $this->now->modify($modifier);
}
/**
* @throws \DateInvalidTimeZoneException When the timezone name is invalid
*/
public function withTimeZone(\DateTimeZone|string $timezone): static
{
if (\PHP_VERSION_ID >= 80300 && \is_string($timezone)) {
$timezone = new \DateTimeZone($timezone);
} elseif (\is_string($timezone)) {
try {
$timezone = new \DateTimeZone($timezone);
} catch (\Exception $e) {
throw new \DateInvalidTimeZoneException($e->getMessage(), $e->getCode(), $e);
}
}
$clone = clone $this;
$clone->now = $clone->now->setTimezone($timezone);
return $clone;
}
}

View File

@@ -0,0 +1,93 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Clock;
/**
* A monotonic clock suitable for performance profiling.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class MonotonicClock implements ClockInterface
{
private int $sOffset;
private int $usOffset;
private \DateTimeZone $timezone;
/**
* @throws \DateInvalidTimeZoneException When $timezone is invalid
*/
public function __construct(\DateTimeZone|string|null $timezone = null)
{
if (false === $offset = hrtime()) {
throw new \RuntimeException('hrtime() returned false: the runtime environment does not provide access to a monotonic timer.');
}
$time = explode(' ', microtime(), 2);
$this->sOffset = $time[1] - $offset[0];
$this->usOffset = (int) ($time[0] * 1000000) - (int) ($offset[1] / 1000);
$this->timezone = \is_string($timezone ??= date_default_timezone_get()) ? $this->withTimeZone($timezone)->timezone : $timezone;
}
public function now(): DatePoint
{
[$s, $us] = hrtime();
if (1000000 <= $us = (int) ($us / 1000) + $this->usOffset) {
++$s;
$us -= 1000000;
} elseif (0 > $us) {
--$s;
$us += 1000000;
}
if (6 !== \strlen($now = (string) $us)) {
$now = str_pad($now, 6, '0', \STR_PAD_LEFT);
}
$now = '@'.($s + $this->sOffset).'.'.$now;
return DatePoint::createFromInterface(new \DateTimeImmutable($now, $this->timezone))->setTimezone($this->timezone);
}
public function sleep(float|int $seconds): void
{
if (0 < $s = (int) $seconds) {
sleep($s);
}
if (0 < $us = $seconds - $s) {
usleep((int) ($us * 1E6));
}
}
/**
* @throws \DateInvalidTimeZoneException When $timezone is invalid
*/
public function withTimeZone(\DateTimeZone|string $timezone): static
{
if (\PHP_VERSION_ID >= 80300 && \is_string($timezone)) {
$timezone = new \DateTimeZone($timezone);
} elseif (\is_string($timezone)) {
try {
$timezone = new \DateTimeZone($timezone);
} catch (\Exception $e) {
throw new \DateInvalidTimeZoneException($e->getMessage(), $e->getCode(), $e);
}
}
$clone = clone $this;
$clone->timezone = $timezone;
return $clone;
}
}

View File

@@ -0,0 +1,67 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Clock;
/**
* A clock that relies the system time.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class NativeClock implements ClockInterface
{
private \DateTimeZone $timezone;
/**
* @throws \DateInvalidTimeZoneException When $timezone is invalid
*/
public function __construct(\DateTimeZone|string|null $timezone = null)
{
$this->timezone = \is_string($timezone ??= date_default_timezone_get()) ? $this->withTimeZone($timezone)->timezone : $timezone;
}
public function now(): DatePoint
{
return DatePoint::createFromInterface(new \DateTimeImmutable('now', $this->timezone));
}
public function sleep(float|int $seconds): void
{
if (0 < $s = (int) $seconds) {
sleep($s);
}
if (0 < $us = $seconds - $s) {
usleep((int) ($us * 1E6));
}
}
/**
* @throws \DateInvalidTimeZoneException When $timezone is invalid
*/
public function withTimeZone(\DateTimeZone|string $timezone): static
{
if (\PHP_VERSION_ID >= 80300 && \is_string($timezone)) {
$timezone = new \DateTimeZone($timezone);
} elseif (\is_string($timezone)) {
try {
$timezone = new \DateTimeZone($timezone);
} catch (\Exception $e) {
throw new \DateInvalidTimeZoneException($e->getMessage(), $e->getCode(), $e);
}
}
$clone = clone $this;
$clone->timezone = $timezone;
return $clone;
}
}

47
plugins/vendor/symfony/clock/README.md vendored Normal file
View File

@@ -0,0 +1,47 @@
Clock Component
===============
Symfony Clock decouples applications from the system clock.
Getting Started
---------------
```bash
composer require symfony/clock
```
```php
use Symfony\Component\Clock\NativeClock;
use Symfony\Component\Clock\ClockInterface;
class MyClockSensitiveClass
{
public function __construct(
private ClockInterface $clock,
) {
// Only if you need to force a timezone:
//$this->clock = $clock->withTimeZone('UTC');
}
public function doSomething()
{
$now = $this->clock->now();
// [...] do something with $now, which is a \DateTimeImmutable object
$this->clock->sleep(2.5); // Pause execution for 2.5 seconds
}
}
$clock = new NativeClock();
$service = new MyClockSensitiveClass($clock);
$service->doSomething();
```
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/clock.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)

View File

@@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Clock;
if (!\function_exists(now::class)) {
/**
* @throws \DateMalformedStringException When the modifier is invalid
*/
function now(string $modifier = 'now'): DatePoint
{
if ('now' !== $modifier) {
return new DatePoint($modifier);
}
$now = Clock::get()->now();
return $now instanceof DatePoint ? $now : DatePoint::createFromInterface($now);
}
}

View File

@@ -0,0 +1,77 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Clock\Test;
use PHPUnit\Framework\Attributes\After;
use PHPUnit\Framework\Attributes\Before;
use PHPUnit\Framework\Attributes\BeforeClass;
use Symfony\Component\Clock\Clock;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\Clock\MockClock;
use function Symfony\Component\Clock\now;
/**
* Helps with mocking the time in your test cases.
*
* This trait provides one self::mockTime() method that freezes the time.
* It restores the global clock after each test case.
* self::mockTime() accepts either a string (eg '+1 days' or '2022-12-22'),
* a DateTimeImmutable, or a boolean (to freeze/restore the global clock).
*
* @author Nicolas Grekas <p@tchwork.com>
*/
trait ClockSensitiveTrait
{
public static function mockTime(string|\DateTimeImmutable|bool $when = true): ClockInterface
{
Clock::set(match (true) {
false === $when => self::saveClockBeforeTest(false),
true === $when => new MockClock(),
$when instanceof \DateTimeImmutable => new MockClock($when),
default => new MockClock(now($when)),
});
return Clock::get();
}
/**
* @beforeClass
*
* @before
*
* @internal
*/
#[Before]
#[BeforeClass]
public static function saveClockBeforeTest(bool $save = true): ClockInterface
{
static $originalClock;
if ($save && $originalClock) {
self::restoreClockAfterTest();
}
return $save ? $originalClock = Clock::get() : $originalClock;
}
/**
* @after
*
* @internal
*/
#[After]
protected static function restoreClockAfterTest(): void
{
Clock::set(self::saveClockBeforeTest(false));
}
}

View File

@@ -0,0 +1,34 @@
{
"name": "symfony/clock",
"type": "library",
"description": "Decouples applications from the system clock",
"keywords": ["clock", "time", "psr20"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"provide": {
"psr/clock-implementation": "1.0"
},
"require": {
"php": ">=8.2",
"psr/clock": "^1.0",
"symfony/polyfill-php83": "^1.28"
},
"autoload": {
"files": [ "Resources/now.php" ],
"psr-4": { "Symfony\\Component\\Clock\\": "" },
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev"
}

View File

@@ -0,0 +1,5 @@
CHANGELOG
=========
The changelog is maintained for all Symfony contracts at the following URL:
https://github.com/symfony/contracts/blob/main/CHANGELOG.md

View File

@@ -0,0 +1,19 @@
Copyright (c) 2020-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.

View File

@@ -0,0 +1,26 @@
Symfony Deprecation Contracts
=============================
A generic function and convention to trigger deprecation notices.
This package provides a single global function named `trigger_deprecation()` that triggers silenced deprecation notices.
By using a custom PHP error handler such as the one provided by the Symfony ErrorHandler component,
the triggered deprecations can be caught and logged for later discovery, both on dev and prod environments.
The function requires at least 3 arguments:
- the name of the Composer package that is triggering the deprecation
- the version of the package that introduced the deprecation
- the message of the deprecation
- more arguments can be provided: they will be inserted in the message using `printf()` formatting
Example:
```php
trigger_deprecation('symfony/blockchain', '8.9', 'Using "%s" is deprecated, use "%s" instead.', 'bitcoin', 'fabcoin');
```
This will generate the following message:
`Since symfony/blockchain 8.9: Using "bitcoin" is deprecated, use "fabcoin" instead.`
While not recommended, the deprecation notices can be completely ignored by declaring an empty
`function trigger_deprecation() {}` in your application.

View File

@@ -0,0 +1,35 @@
{
"name": "symfony/deprecation-contracts",
"type": "library",
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=8.1"
},
"autoload": {
"files": [
"function.php"
]
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-main": "3.6-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
}
}

View File

@@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (!function_exists('trigger_deprecation')) {
/**
* Triggers a silenced deprecation notice.
*
* @param string $package The name of the Composer package that is triggering the deprecation
* @param string $version The version of the package that introduced the deprecation
* @param string $message The message of the deprecation
* @param mixed ...$args Values to insert in the message using printf() formatting
*
* @author Nicolas Grekas <p@tchwork.com>
*/
function trigger_deprecation(string $package, string $version, string $message, mixed ...$args): void
{
@trigger_error(($package || $version ? "Since $package $version: " : '').($args ? vsprintf($message, $args) : $message), \E_USER_DEPRECATED);
}
}

View File

@@ -0,0 +1,150 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation;
// Help opcache.preload discover always-needed symbols
class_exists(AcceptHeaderItem::class);
/**
* Represents an Accept-* header.
*
* An accept header is compound with a list of items,
* sorted by descending quality.
*
* @author Jean-François Simon <contact@jfsimon.fr>
*/
class AcceptHeader
{
/**
* @var AcceptHeaderItem[]
*/
private array $items = [];
private bool $sorted = true;
/**
* @param AcceptHeaderItem[] $items
*/
public function __construct(array $items)
{
foreach ($items as $item) {
$this->add($item);
}
}
/**
* Builds an AcceptHeader instance from a string.
*/
public static function fromString(?string $headerValue): self
{
$parts = HeaderUtils::split($headerValue ?? '', ',;=');
return new self(array_map(function ($subParts) {
static $index = 0;
$part = array_shift($subParts);
$attributes = HeaderUtils::combine($subParts);
$item = new AcceptHeaderItem($part[0], $attributes);
$item->setIndex($index++);
return $item;
}, $parts));
}
/**
* Returns header value's string representation.
*/
public function __toString(): string
{
return implode(',', $this->items);
}
/**
* Tests if header has given value.
*/
public function has(string $value): bool
{
return isset($this->items[$value]);
}
/**
* Returns given value's item, if exists.
*/
public function get(string $value): ?AcceptHeaderItem
{
return $this->items[$value] ?? $this->items[explode('/', $value)[0].'/*'] ?? $this->items['*/*'] ?? $this->items['*'] ?? null;
}
/**
* Adds an item.
*
* @return $this
*/
public function add(AcceptHeaderItem $item): static
{
$this->items[$item->getValue()] = $item;
$this->sorted = false;
return $this;
}
/**
* Returns all items.
*
* @return AcceptHeaderItem[]
*/
public function all(): array
{
$this->sort();
return $this->items;
}
/**
* Filters items on their value using given regex.
*/
public function filter(string $pattern): self
{
return new self(array_filter($this->items, fn (AcceptHeaderItem $item) => preg_match($pattern, $item->getValue())));
}
/**
* Returns first item.
*/
public function first(): ?AcceptHeaderItem
{
$this->sort();
return $this->items ? reset($this->items) : null;
}
/**
* Sorts items by descending quality.
*/
private function sort(): void
{
if (!$this->sorted) {
uasort($this->items, function (AcceptHeaderItem $a, AcceptHeaderItem $b) {
$qA = $a->getQuality();
$qB = $b->getQuality();
if ($qA === $qB) {
return $a->getIndex() > $b->getIndex() ? 1 : -1;
}
return $qA > $qB ? -1 : 1;
});
$this->sorted = true;
}
}
}

View File

@@ -0,0 +1,159 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation;
/**
* Represents an Accept-* header item.
*
* @author Jean-François Simon <contact@jfsimon.fr>
*/
class AcceptHeaderItem
{
private float $quality = 1.0;
private int $index = 0;
private array $attributes = [];
public function __construct(
private string $value,
array $attributes = [],
) {
foreach ($attributes as $name => $value) {
$this->setAttribute($name, $value);
}
}
/**
* Builds an AcceptHeaderInstance instance from a string.
*/
public static function fromString(?string $itemValue): self
{
$parts = HeaderUtils::split($itemValue ?? '', ';=');
$part = array_shift($parts);
$attributes = HeaderUtils::combine($parts);
return new self($part[0], $attributes);
}
/**
* Returns header value's string representation.
*/
public function __toString(): string
{
$string = $this->value.($this->quality < 1 ? ';q='.$this->quality : '');
if (\count($this->attributes) > 0) {
$string .= '; '.HeaderUtils::toString($this->attributes, ';');
}
return $string;
}
/**
* Set the item value.
*
* @return $this
*/
public function setValue(string $value): static
{
$this->value = $value;
return $this;
}
/**
* Returns the item value.
*/
public function getValue(): string
{
return $this->value;
}
/**
* Set the item quality.
*
* @return $this
*/
public function setQuality(float $quality): static
{
$this->quality = $quality;
return $this;
}
/**
* Returns the item quality.
*/
public function getQuality(): float
{
return $this->quality;
}
/**
* Set the item index.
*
* @return $this
*/
public function setIndex(int $index): static
{
$this->index = $index;
return $this;
}
/**
* Returns the item index.
*/
public function getIndex(): int
{
return $this->index;
}
/**
* Tests if an attribute exists.
*/
public function hasAttribute(string $name): bool
{
return isset($this->attributes[$name]);
}
/**
* Returns an attribute by its name.
*/
public function getAttribute(string $name, mixed $default = null): mixed
{
return $this->attributes[$name] ?? $default;
}
/**
* Returns all attributes.
*/
public function getAttributes(): array
{
return $this->attributes;
}
/**
* Set an attribute.
*
* @return $this
*/
public function setAttribute(string $name, string $value): static
{
if ('q' === $name) {
$this->quality = (float) $value;
} else {
$this->attributes[$name] = $value;
}
return $this;
}
}

View File

@@ -0,0 +1,396 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\File;
/**
* BinaryFileResponse represents an HTTP response delivering a file.
*
* @author Niklas Fiekas <niklas.fiekas@tu-clausthal.de>
* @author stealth35 <stealth35-php@live.fr>
* @author Igor Wiedler <igor@wiedler.ch>
* @author Jordan Alliot <jordan.alliot@gmail.com>
* @author Sergey Linnik <linniksa@gmail.com>
*/
class BinaryFileResponse extends Response
{
protected static bool $trustXSendfileTypeHeader = false;
protected File $file;
protected ?\SplTempFileObject $tempFileObject = null;
protected int $offset = 0;
protected int $maxlen = -1;
protected bool $deleteFileAfterSend = false;
protected int $chunkSize = 16 * 1024;
/**
* @param \SplFileInfo|string $file The file to stream
* @param int $status The response status code (200 "OK" by default)
* @param array $headers An array of response headers
* @param bool $public Files are public by default
* @param string|null $contentDisposition The type of Content-Disposition to set automatically with the filename
* @param bool $autoEtag Whether the ETag header should be automatically set
* @param bool $autoLastModified Whether the Last-Modified header should be automatically set
*/
public function __construct(\SplFileInfo|string $file, int $status = 200, array $headers = [], bool $public = true, ?string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true)
{
parent::__construct(null, $status, $headers);
$this->setFile($file, $contentDisposition, $autoEtag, $autoLastModified);
if ($public) {
$this->setPublic();
}
}
/**
* Sets the file to stream.
*
* @return $this
*
* @throws FileException
*/
public function setFile(\SplFileInfo|string $file, ?string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true): static
{
$isTemporaryFile = $file instanceof \SplTempFileObject;
$this->tempFileObject = $isTemporaryFile ? $file : null;
if (!$file instanceof File) {
if ($file instanceof \SplFileInfo) {
$file = new File($file->getPathname(), !$isTemporaryFile);
} else {
$file = new File($file);
}
}
if (!$file->isReadable() && !$isTemporaryFile) {
throw new FileException('File must be readable.');
}
$this->file = $file;
if ($autoEtag) {
$this->setAutoEtag();
}
if ($autoLastModified && !$isTemporaryFile) {
$this->setAutoLastModified();
}
if ($contentDisposition) {
$this->setContentDisposition($contentDisposition);
}
return $this;
}
/**
* Gets the file.
*/
public function getFile(): File
{
return $this->file;
}
/**
* Sets the response stream chunk size.
*
* @return $this
*/
public function setChunkSize(int $chunkSize): static
{
if ($chunkSize < 1) {
throw new \InvalidArgumentException('The chunk size of a BinaryFileResponse cannot be less than 1.');
}
$this->chunkSize = $chunkSize;
return $this;
}
/**
* Automatically sets the Last-Modified header according the file modification date.
*
* @return $this
*/
public function setAutoLastModified(): static
{
$this->setLastModified(\DateTimeImmutable::createFromFormat('U', $this->tempFileObject ? time() : $this->file->getMTime()));
return $this;
}
/**
* Automatically sets the ETag header according to the checksum of the file.
*
* @return $this
*/
public function setAutoEtag(): static
{
$this->setEtag(base64_encode(hash_file('xxh128', $this->file->getPathname(), true)));
return $this;
}
/**
* Sets the Content-Disposition header with the given filename.
*
* @param string $disposition ResponseHeaderBag::DISPOSITION_INLINE or ResponseHeaderBag::DISPOSITION_ATTACHMENT
* @param string $filename Optionally use this UTF-8 encoded filename instead of the real name of the file
* @param string $filenameFallback A fallback filename, containing only ASCII characters. Defaults to an automatically encoded filename
*
* @return $this
*/
public function setContentDisposition(string $disposition, string $filename = '', string $filenameFallback = ''): static
{
if ('' === $filename) {
$filename = $this->file->getFilename();
}
if ('' === $filenameFallback && (!preg_match('/^[\x20-\x7e]*$/', $filename) || str_contains($filename, '%'))) {
$encoding = mb_detect_encoding($filename, null, true) ?: '8bit';
for ($i = 0, $filenameLength = mb_strlen($filename, $encoding); $i < $filenameLength; ++$i) {
$char = mb_substr($filename, $i, 1, $encoding);
if ('%' === $char || \ord($char) < 32 || \ord($char) > 126) {
$filenameFallback .= '_';
} else {
$filenameFallback .= $char;
}
}
}
$dispositionHeader = $this->headers->makeDisposition($disposition, $filename, $filenameFallback);
$this->headers->set('Content-Disposition', $dispositionHeader);
return $this;
}
public function prepare(Request $request): static
{
if ($this->isInformational() || $this->isEmpty()) {
parent::prepare($request);
$this->maxlen = 0;
return $this;
}
if (!$this->headers->has('Content-Type')) {
$mimeType = null;
if (!$this->tempFileObject) {
$mimeType = $this->file->getMimeType();
}
$this->headers->set('Content-Type', $mimeType ?: 'application/octet-stream');
}
parent::prepare($request);
$this->offset = 0;
$this->maxlen = -1;
if ($this->tempFileObject) {
$fileSize = $this->tempFileObject->fstat()['size'];
} elseif (false === $fileSize = $this->file->getSize()) {
return $this;
}
$this->headers->remove('Transfer-Encoding');
$this->headers->set('Content-Length', $fileSize);
if (!$this->headers->has('Accept-Ranges')) {
// Only accept ranges on safe HTTP methods
$this->headers->set('Accept-Ranges', $request->isMethodSafe() ? 'bytes' : 'none');
}
if (self::$trustXSendfileTypeHeader && $request->headers->has('X-Sendfile-Type')) {
// Use X-Sendfile, do not send any content.
$type = $request->headers->get('X-Sendfile-Type');
$path = $this->file->getRealPath();
// Fall back to scheme://path for stream wrapped locations.
if (false === $path) {
$path = $this->file->getPathname();
}
if ('x-accel-redirect' === strtolower($type)) {
// Do X-Accel-Mapping substitutions.
// @link https://github.com/rack/rack/blob/main/lib/rack/sendfile.rb
// @link https://mattbrictson.com/blog/accelerated-rails-downloads
if (!$request->headers->has('X-Accel-Mapping')) {
throw new \LogicException('The "X-Accel-Mapping" header must be set when "X-Sendfile-Type" is set to "X-Accel-Redirect".');
}
$parts = HeaderUtils::split($request->headers->get('X-Accel-Mapping'), ',=');
foreach ($parts as $part) {
[$pathPrefix, $location] = $part;
if (str_starts_with($path, $pathPrefix)) {
$path = $location.substr($path, \strlen($pathPrefix));
// Only set X-Accel-Redirect header if a valid URI can be produced
// as nginx does not serve arbitrary file paths.
$this->headers->set($type, rawurlencode($path));
$this->maxlen = 0;
break;
}
}
} else {
$this->headers->set($type, $path);
$this->maxlen = 0;
}
} elseif ($request->headers->has('Range') && $request->isMethod('GET')) {
// Process the range headers.
if (!$request->headers->has('If-Range') || $this->hasValidIfRangeHeader($request->headers->get('If-Range'))) {
$range = $request->headers->get('Range');
if (str_starts_with($range, 'bytes=')) {
[$start, $end] = explode('-', substr($range, 6), 2) + [1 => 0];
$end = ('' === $end) ? $fileSize - 1 : (int) $end;
if ('' === $start) {
$start = $fileSize - $end;
$end = $fileSize - 1;
} else {
$start = (int) $start;
}
if ($start <= $end) {
$end = min($end, $fileSize - 1);
if ($start < 0 || $start > $end) {
$this->setStatusCode(416);
$this->headers->set('Content-Range', \sprintf('bytes */%s', $fileSize));
} elseif ($end - $start < $fileSize - 1) {
$this->maxlen = $end < $fileSize ? $end - $start + 1 : -1;
$this->offset = $start;
$this->setStatusCode(206);
$this->headers->set('Content-Range', \sprintf('bytes %s-%s/%s', $start, $end, $fileSize));
$this->headers->set('Content-Length', $end - $start + 1);
}
}
}
}
}
if ($request->isMethod('HEAD')) {
$this->maxlen = 0;
}
return $this;
}
private function hasValidIfRangeHeader(?string $header): bool
{
if ($this->getEtag() === $header) {
return true;
}
if (null === $lastModified = $this->getLastModified()) {
return false;
}
return $lastModified->format('D, d M Y H:i:s').' GMT' === $header;
}
public function sendContent(): static
{
try {
if (!$this->isSuccessful()) {
return $this;
}
if (0 === $this->maxlen) {
return $this;
}
$out = fopen('php://output', 'w');
if ($this->tempFileObject) {
$file = $this->tempFileObject;
$file->rewind();
} else {
$file = new \SplFileObject($this->file->getPathname(), 'r');
}
ignore_user_abort(true);
if (0 !== $this->offset) {
$file->fseek($this->offset);
}
$length = $this->maxlen;
while ($length && !$file->eof()) {
$read = $length > $this->chunkSize || 0 > $length ? $this->chunkSize : $length;
if (false === $data = $file->fread($read)) {
break;
}
while ('' !== $data) {
$read = fwrite($out, $data);
if (false === $read || connection_aborted()) {
break 2;
}
if (0 < $length) {
$length -= $read;
}
$data = substr($data, $read);
}
}
fclose($out);
} finally {
if (null === $this->tempFileObject && $this->deleteFileAfterSend && is_file($this->file->getPathname())) {
unlink($this->file->getPathname());
}
}
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 BinaryFileResponse instance.');
}
return $this;
}
public function getContent(): string|false
{
return false;
}
/**
* Trust X-Sendfile-Type header.
*/
public static function trustXSendfileTypeHeader(): void
{
self::$trustXSendfileTypeHeader = true;
}
/**
* If this is set to true, the file will be unlinked after the request is sent
* Note: If the X-Sendfile header is used, the deleteFileAfterSend setting will not be used.
*
* @return $this
*/
public function deleteFileAfterSend(bool $shouldDelete = true): static
{
$this->deleteFileAfterSend = $shouldDelete;
return $this;
}
}

View File

@@ -0,0 +1,396 @@
CHANGELOG
=========
7.3
---
* Add support for iterable of string in `StreamedResponse`
* Add `EventStreamResponse` and `ServerEvent` classes to streamline server event streaming
* Add support for `valkey:` / `valkeys:` schemes for sessions
* `Request::getPreferredLanguage()` now favors a more preferred language above exactly matching a locale
* Allow `UriSigner` to use a `ClockInterface`
* Add `UriSigner::verify()`
7.2
---
* Add optional `$requests` parameter to `RequestStack::__construct()`
* Add optional `$v4Bytes` and `$v6Bytes` parameters to `IpUtils::anonymize()`
* Add `PRIVATE_SUBNETS` as a shortcut for private IP address ranges to `Request::setTrustedProxies()`
* Deprecate passing `referer_check`, `use_only_cookies`, `use_trans_sid`, `trans_sid_hosts`, `trans_sid_tags`, `sid_bits_per_character` and `sid_length` options to `NativeSessionStorage`
7.1
---
* Add optional `$expirationParameter` argument to `UriSigner::__construct()`
* Add optional `$expiration` argument to `UriSigner::sign()`
* Rename `$parameter` argument of `UriSigner::__construct()` to `$hashParameter`
* Add `UploadedFile::getClientOriginalPath()`
* Add `QueryParameterRequestMatcher`
* Add `HeaderRequestMatcher`
* Add support for `\SplTempFileObject` in `BinaryFileResponse`
* Add `verbose` argument to response test constraints
7.0
---
* Calling `ParameterBag::filter()` throws an `UnexpectedValueException` on invalid value, unless flag `FILTER_NULL_ON_FAILURE` is set
* Calling `ParameterBag::getInt()` and `ParameterBag::getBool()` throws an `UnexpectedValueException` on invalid value
* Remove classes `RequestMatcher` and `ExpressionRequestMatcher`
* Remove `Request::getContentType()`, use `Request::getContentTypeFormat()` instead
* Throw an `InvalidArgumentException` when calling `Request::create()` with a malformed URI
* Require explicit argument when calling `JsonResponse::setCallback()`, `Response::setExpires/setLastModified/setEtag()`, `MockArraySessionStorage/NativeSessionStorage::setMetadataBag()`, `NativeSessionStorage::setSaveHandler()`
* Add argument `$statusCode` to `Response::sendHeaders()` and `StreamedResponse::sendHeaders()`
6.4
---
* Make `HeaderBag::getDate()`, `Response::getDate()`, `getExpires()` and `getLastModified()` return a `DateTimeImmutable`
* Support root-level `Generator` in `StreamedJsonResponse`
* Add `UriSigner` from the HttpKernel component
* Add `partitioned` flag to `Cookie` (CHIPS Cookie)
* Add argument `bool $flush = true` to `Response::send()`
* Make `MongoDbSessionHandler` instantiable with the mongodb extension directly
6.3
---
* Calling `ParameterBag::getDigit()`, `getAlnum()`, `getAlpha()` on an `array` throws a `UnexpectedValueException` instead of a `TypeError`
* Add `ParameterBag::getString()` to convert a parameter into string and throw an exception if the value is invalid
* Add `ParameterBag::getEnum()`
* Create migration for session table when pdo handler is used
* Add support for Relay PHP extension for Redis
* The `Response::sendHeaders()` method now takes an optional HTTP status code as parameter, allowing to send informational responses such as Early Hints responses (103 status code)
* Add `IpUtils::isPrivateIp()`
* Add `Request::getPayload(): InputBag`
* Deprecate conversion of invalid values in `ParameterBag::getInt()` and `ParameterBag::getBoolean()`,
* Deprecate ignoring invalid values when using `ParameterBag::filter()`, unless flag `FILTER_NULL_ON_FAILURE` is set
6.2
---
* Add `StreamedJsonResponse` class for efficient JSON streaming
* The HTTP cache store uses the `xxh128` algorithm
* Deprecate calling `JsonResponse::setCallback()`, `Response::setExpires/setLastModified/setEtag()`, `MockArraySessionStorage/NativeSessionStorage::setMetadataBag()`, `NativeSessionStorage::setSaveHandler()` without arguments
* Add request matchers under the `Symfony\Component\HttpFoundation\RequestMatcher` namespace
* Deprecate `RequestMatcher` in favor of `ChainRequestMatcher`
* Deprecate `Symfony\Component\HttpFoundation\ExpressionRequestMatcher` in favor of `Symfony\Component\HttpFoundation\RequestMatcher\ExpressionRequestMatcher`
6.1
---
* Add stale while revalidate and stale if error cache header
* Allow dynamic session "ttl" when using a remote storage
* Deprecate `Request::getContentType()`, use `Request::getContentTypeFormat()` instead
6.0
---
* Remove the `NamespacedAttributeBag` class
* Removed `Response::create()`, `JsonResponse::create()`,
`RedirectResponse::create()`, `StreamedResponse::create()` and
`BinaryFileResponse::create()` methods (use `__construct()` instead)
* Not passing a `Closure` together with `FILTER_CALLBACK` to `ParameterBag::filter()` throws an `\InvalidArgumentException`; wrap your filter in a closure instead
* Not passing a `Closure` together with `FILTER_CALLBACK` to `InputBag::filter()` throws an `\InvalidArgumentException`; wrap your filter in a closure instead
* Removed the `Request::HEADER_X_FORWARDED_ALL` constant, use either `Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO` or `Request::HEADER_X_FORWARDED_AWS_ELB` or `Request::HEADER_X_FORWARDED_TRAEFIK`constants instead
* Rename `RequestStack::getMasterRequest()` to `getMainRequest()`
* Not passing `FILTER_REQUIRE_ARRAY` or `FILTER_FORCE_ARRAY` flags to `InputBag::filter()` when filtering an array will throw `BadRequestException`
* Removed the `Request::HEADER_X_FORWARDED_ALL` constant
* Retrieving non-scalar values using `InputBag::get()` will throw `BadRequestException` (use `InputBad::all()` instead to retrieve an array)
* Passing non-scalar default value as the second argument `InputBag::get()` will throw `\InvalidArgumentException`
* Passing non-scalar, non-array value as the second argument `InputBag::set()` will throw `\InvalidArgumentException`
* Passing `null` as `$requestIp` to `IpUtils::__checkIp()`, `IpUtils::__checkIp4()` or `IpUtils::__checkIp6()` is not supported anymore.
5.4
---
* Deprecate passing `null` as `$requestIp` to `IpUtils::__checkIp()`, `IpUtils::__checkIp4()` or `IpUtils::__checkIp6()`, pass an empty string instead.
* Add the `litespeed_finish_request` method to work with Litespeed
* Deprecate `upload_progress.*` and `url_rewriter.tags` session options
* Allow setting session options via DSN
5.3
---
* Add the `SessionFactory`, `NativeSessionStorageFactory`, `PhpBridgeSessionStorageFactory` and `MockFileSessionStorageFactory` classes
* Calling `Request::getSession()` when there is no available session throws a `SessionNotFoundException`
* Add the `RequestStack::getSession` method
* Deprecate the `NamespacedAttributeBag` class
* Add `ResponseFormatSame` PHPUnit constraint
* Deprecate the `RequestStack::getMasterRequest()` method and add `getMainRequest()` as replacement
5.2.0
-----
* added support for `X-Forwarded-Prefix` header
* added `HeaderUtils::parseQuery()`: it does the same as `parse_str()` but preserves dots in variable names
* added `File::getContent()`
* added ability to use comma separated ip addresses for `RequestMatcher::matchIps()`
* added `Request::toArray()` to parse a JSON request body to an array
* added `RateLimiter\RequestRateLimiterInterface` and `RateLimiter\AbstractRequestRateLimiter`
* deprecated not passing a `Closure` together with `FILTER_CALLBACK` to `ParameterBag::filter()`; wrap your filter in a closure instead.
* Deprecated the `Request::HEADER_X_FORWARDED_ALL` constant, use either `HEADER_X_FORWARDED_FOR | HEADER_X_FORWARDED_HOST | HEADER_X_FORWARDED_PORT | HEADER_X_FORWARDED_PROTO` or `HEADER_X_FORWARDED_AWS_ELB` or `HEADER_X_FORWARDED_TRAEFIK` constants instead.
* Deprecated `BinaryFileResponse::create()`, use `__construct()` instead
5.1.0
-----
* added `Cookie::withValue`, `Cookie::withDomain`, `Cookie::withExpires`,
`Cookie::withPath`, `Cookie::withSecure`, `Cookie::withHttpOnly`,
`Cookie::withRaw`, `Cookie::withSameSite`
* Deprecate `Response::create()`, `JsonResponse::create()`,
`RedirectResponse::create()`, and `StreamedResponse::create()` methods (use
`__construct()` instead)
* added `Request::preferSafeContent()` and `Response::setContentSafe()` to handle "safe" HTTP preference
according to [RFC 8674](https://tools.ietf.org/html/rfc8674)
* made the Mime component an optional dependency
* added `MarshallingSessionHandler`, `IdentityMarshaller`
* made `Session` accept a callback to report when the session is being used
* Add support for all core cache control directives
* Added `Symfony\Component\HttpFoundation\InputBag`
* Deprecated retrieving non-string values using `InputBag::get()`, use `InputBag::all()` if you need access to the collection of values
5.0.0
-----
* made `Cookie` auto-secure and lax by default
* removed classes in the `MimeType` namespace, use the Symfony Mime component instead
* removed method `UploadedFile::getClientSize()` and the related constructor argument
* made `Request::getSession()` throw if the session has not been set before
* removed `Response::HTTP_RESERVED_FOR_WEBDAV_ADVANCED_COLLECTIONS_EXPIRED_PROPOSAL`
* passing a null url when instantiating a `RedirectResponse` is not allowed
4.4.0
-----
* passing arguments to `Request::isMethodSafe()` is deprecated.
* `ApacheRequest` is deprecated, use the `Request` class instead.
* passing a third argument to `HeaderBag::get()` is deprecated, use method `all()` instead
* [BC BREAK] `PdoSessionHandler` with MySQL changed the type of the lifetime column,
make sure to run `ALTER TABLE sessions MODIFY sess_lifetime INTEGER UNSIGNED NOT NULL` to
update your database.
* `PdoSessionHandler` now precalculates the expiry timestamp in the lifetime column,
make sure to run `CREATE INDEX expiry ON sessions (sess_lifetime)` to update your database
to speed up garbage collection of expired sessions.
* added `SessionHandlerFactory` to create session handlers with a DSN
* added `IpUtils::anonymize()` to help with GDPR compliance.
4.3.0
-----
* added PHPUnit constraints: `RequestAttributeValueSame`, `ResponseCookieValueSame`, `ResponseHasCookie`,
`ResponseHasHeader`, `ResponseHeaderSame`, `ResponseIsRedirected`, `ResponseIsSuccessful`, and `ResponseStatusCodeSame`
* deprecated `MimeTypeGuesserInterface` and `ExtensionGuesserInterface` in favor of `Symfony\Component\Mime\MimeTypesInterface`.
* deprecated `MimeType` and `MimeTypeExtensionGuesser` in favor of `Symfony\Component\Mime\MimeTypes`.
* deprecated `FileBinaryMimeTypeGuesser` in favor of `Symfony\Component\Mime\FileBinaryMimeTypeGuesser`.
* deprecated `FileinfoMimeTypeGuesser` in favor of `Symfony\Component\Mime\FileinfoMimeTypeGuesser`.
* added `UrlHelper` that allows to get an absolute URL and a relative path for a given path
4.2.0
-----
* the default value of the "$secure" and "$samesite" arguments of Cookie's constructor
will respectively change from "false" to "null" and from "null" to "lax" in Symfony
5.0, you should define their values explicitly or use "Cookie::create()" instead.
* added `matchPort()` in RequestMatcher
4.1.3
-----
* [BC BREAK] Support for the IIS-only `X_ORIGINAL_URL` and `X_REWRITE_URL`
HTTP headers has been dropped for security reasons.
4.1.0
-----
* Query string normalization uses `parse_str()` instead of custom parsing logic.
* Passing the file size to the constructor of the `UploadedFile` class is deprecated.
* The `getClientSize()` method of the `UploadedFile` class is deprecated. Use `getSize()` instead.
* added `RedisSessionHandler` to use Redis as a session storage
* The `get()` method of the `AcceptHeader` class now takes into account the
`*` and `*/*` default values (if they are present in the Accept HTTP header)
when looking for items.
* deprecated `Request::getSession()` when no session has been set. Use `Request::hasSession()` instead.
* added `CannotWriteFileException`, `ExtensionFileException`, `FormSizeFileException`,
`IniSizeFileException`, `NoFileException`, `NoTmpDirFileException`, `PartialFileException` to
handle failed `UploadedFile`.
* added `MigratingSessionHandler` for migrating between two session handlers without losing sessions
* added `HeaderUtils`.
4.0.0
-----
* the `Request::setTrustedHeaderName()` and `Request::getTrustedHeaderName()`
methods have been removed
* the `Request::HEADER_CLIENT_IP` constant has been removed, use
`Request::HEADER_X_FORWARDED_FOR` instead
* the `Request::HEADER_CLIENT_HOST` constant has been removed, use
`Request::HEADER_X_FORWARDED_HOST` instead
* the `Request::HEADER_CLIENT_PROTO` constant has been removed, use
`Request::HEADER_X_FORWARDED_PROTO` instead
* the `Request::HEADER_CLIENT_PORT` constant has been removed, use
`Request::HEADER_X_FORWARDED_PORT` instead
* checking for cacheable HTTP methods using the `Request::isMethodSafe()`
method (by not passing `false` as its argument) is not supported anymore and
throws a `\BadMethodCallException`
* the `WriteCheckSessionHandler`, `NativeSessionHandler` and `NativeProxy` classes have been removed
* setting session save handlers that do not implement `\SessionHandlerInterface` in
`NativeSessionStorage::setSaveHandler()` is not supported anymore and throws a
`\TypeError`
3.4.0
-----
* implemented PHP 7.0's `SessionUpdateTimestampHandlerInterface` with a new
`AbstractSessionHandler` base class and a new `StrictSessionHandler` wrapper
* deprecated the `WriteCheckSessionHandler`, `NativeSessionHandler` and `NativeProxy` classes
* deprecated setting session save handlers that do not implement `\SessionHandlerInterface` in `NativeSessionStorage::setSaveHandler()`
* deprecated using `MongoDbSessionHandler` with the legacy mongo extension; use it with the mongodb/mongodb package and ext-mongodb instead
* deprecated `MemcacheSessionHandler`; use `MemcachedSessionHandler` instead
3.3.0
-----
* the `Request::setTrustedProxies()` method takes a new `$trustedHeaderSet` argument,
see https://symfony.com/doc/current/deployment/proxies.html for more info,
* deprecated the `Request::setTrustedHeaderName()` and `Request::getTrustedHeaderName()` methods,
* added `File\Stream`, to be passed to `BinaryFileResponse` when the size of the served file is unknown,
disabling `Range` and `Content-Length` handling, switching to chunked encoding instead
* added the `Cookie::fromString()` method that allows to create a cookie from a
raw header string
3.1.0
-----
* Added support for creating `JsonResponse` with a string of JSON data
3.0.0
-----
* The precedence of parameters returned from `Request::get()` changed from "GET, PATH, BODY" to "PATH, GET, BODY"
2.8.0
-----
* Finding deep items in `ParameterBag::get()` is deprecated since version 2.8 and
will be removed in 3.0.
2.6.0
-----
* PdoSessionHandler changes
- implemented different session locking strategies to prevent loss of data by concurrent access to the same session
- [BC BREAK] save session data in a binary column without base64_encode
- [BC BREAK] added lifetime column to the session table which allows to have different lifetimes for each session
- implemented lazy connections that are only opened when a session is used by either passing a dsn string
explicitly or falling back to session.save_path ini setting
- added a createTable method that initializes a correctly defined table depending on the database vendor
2.5.0
-----
* added `JsonResponse::setEncodingOptions()` & `JsonResponse::getEncodingOptions()` for easier manipulation
of the options used while encoding data to JSON format.
2.4.0
-----
* added RequestStack
* added Request::getEncodings()
* added accessors methods to session handlers
2.3.0
-----
* added support for ranges of IPs in trusted proxies
* `UploadedFile::isValid` now returns false if the file was not uploaded via HTTP (in a non-test mode)
* Improved error-handling of `\Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler`
to ensure the supplied PDO handler throws Exceptions on error (as the class expects). Added related test cases
to verify that Exceptions are properly thrown when the PDO queries fail.
2.2.0
-----
* fixed the Request::create() precedence (URI information always take precedence now)
* added Request::getTrustedProxies()
* deprecated Request::isProxyTrusted()
* [BC BREAK] JsonResponse does not turn a top level empty array to an object anymore, use an ArrayObject to enforce objects
* added a IpUtils class to check if an IP belongs to a CIDR
* added Request::getRealMethod() to get the "real" HTTP method (getMethod() returns the "intended" HTTP method)
* disabled _method request parameter support by default (call Request::enableHttpMethodParameterOverride() to
enable it, and Request::getHttpMethodParameterOverride() to check if it is supported)
* Request::splitHttpAcceptHeader() method is deprecated and will be removed in 2.3
* Deprecated Flashbag::count() and \Countable interface, will be removed in 2.3
2.1.0
-----
* added Request::getSchemeAndHttpHost() and Request::getUserInfo()
* added a fluent interface to the Response class
* added Request::isProxyTrusted()
* added JsonResponse
* added a getTargetUrl method to RedirectResponse
* added support for streamed responses
* made Response::prepare() method the place to enforce HTTP specification
* [BC BREAK] moved management of the locale from the Session class to the Request class
* added a generic access to the PHP built-in filter mechanism: ParameterBag::filter()
* made FileBinaryMimeTypeGuesser command configurable
* added Request::getUser() and Request::getPassword()
* added support for the PATCH method in Request
* removed the ContentTypeMimeTypeGuesser class as it is deprecated and never used on PHP 5.3
* added ResponseHeaderBag::makeDisposition() (implements RFC 6266)
* made mimetype to extension conversion configurable
* [BC BREAK] Moved all session related classes and interfaces into own namespace, as
`Symfony\Component\HttpFoundation\Session` and renamed classes accordingly.
Session handlers are located in the subnamespace `Symfony\Component\HttpFoundation\Session\Handler`.
* SessionHandlers must implement `\SessionHandlerInterface` or extend from the
`Symfony\Component\HttpFoundation\Storage\Handler\NativeSessionHandler` base class.
* Added internal storage driver proxy mechanism for forward compatibility with
PHP 5.4 `\SessionHandler` class.
* Added session handlers for custom Memcache, Memcached and Null session save handlers.
* [BC BREAK] Removed `NativeSessionStorage` and replaced with `NativeFileSessionHandler`.
* [BC BREAK] `SessionStorageInterface` methods removed: `write()`, `read()` and
`remove()`. Added `getBag()`, `registerBag()`. The `NativeSessionStorage` class
is a mediator for the session storage internals including the session handlers
which do the real work of participating in the internal PHP session workflow.
* [BC BREAK] Introduced mock implementations of `SessionStorage` to enable unit
and functional testing without starting real PHP sessions. Removed
`ArraySessionStorage`, and replaced with `MockArraySessionStorage` for unit
tests; removed `FilesystemSessionStorage`, and replaced with`MockFileSessionStorage`
for functional tests. These do not interact with global session ini
configuration values, session functions or `$_SESSION` superglobal. This means
they can be configured directly allowing multiple instances to work without
conflicting in the same PHP process.
* [BC BREAK] Removed the `close()` method from the `Session` class, as this is
now redundant.
* Deprecated the following methods from the Session class: `setFlash()`, `setFlashes()`
`getFlash()`, `hasFlash()`, and `removeFlash()`. Use `getFlashBag()` instead
which returns a `FlashBagInterface`.
* `Session->clear()` now only clears session attributes as before it cleared
flash messages and attributes. `Session->getFlashBag()->all()` clears flashes now.
* Session data is now managed by `SessionBagInterface` to better encapsulate
session data.
* Refactored session attribute and flash messages system to their own
`SessionBagInterface` implementations.
* Added `FlashBag`. Flashes expire when retrieved by `get()` or `all()`. This
implementation is ESI compatible.
* Added `AutoExpireFlashBag` (default) to replicate Symfony 2.0.x auto expire
behavior of messages auto expiring after one page page load. Messages must
be retrieved by `get()` or `all()`.
* Added `Symfony\Component\HttpFoundation\Attribute\AttributeBag` to replicate
attributes storage behavior from 2.0.x (default).
* Added `Symfony\Component\HttpFoundation\Attribute\NamespacedAttributeBag` for
namespace session attributes.
* Flash API can stores messages in an array so there may be multiple messages
per flash type. The old `Session` class API remains without BC break as it
will allow single messages as before.
* Added basic session meta-data to the session to record session create time,
last updated time, and the lifetime of the session cookie that was provided
to the client.
* Request::getClientIp() method doesn't take a parameter anymore but bases
itself on the trustProxy parameter.
* Added isMethod() to Request object.
* [BC BREAK] The methods `getPathInfo()`, `getBaseUrl()` and `getBasePath()` of
a `Request` now all return a raw value (vs a urldecoded value before). Any call
to one of these methods must be checked and wrapped in a `rawurldecode()` if
needed.

View File

@@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation;
/**
* ChainRequestMatcher verifies that all checks match against a Request instance.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class ChainRequestMatcher implements RequestMatcherInterface
{
/**
* @param iterable<RequestMatcherInterface> $matchers
*/
public function __construct(private iterable $matchers)
{
}
public function matches(Request $request): bool
{
foreach ($this->matchers as $matcher) {
if (!$matcher->matches($request)) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,405 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation;
/**
* Represents a cookie.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class Cookie
{
public const SAMESITE_NONE = 'none';
public const SAMESITE_LAX = 'lax';
public const SAMESITE_STRICT = 'strict';
protected int $expire;
protected string $path;
private ?string $sameSite = null;
private bool $secureDefault = false;
private const RESERVED_CHARS_LIST = "=,; \t\r\n\v\f";
private const RESERVED_CHARS_FROM = ['=', ',', ';', ' ', "\t", "\r", "\n", "\v", "\f"];
private const RESERVED_CHARS_TO = ['%3D', '%2C', '%3B', '%20', '%09', '%0D', '%0A', '%0B', '%0C'];
/**
* Creates cookie from raw header string.
*/
public static function fromString(string $cookie, bool $decode = false): static
{
$data = [
'expires' => 0,
'path' => '/',
'domain' => null,
'secure' => false,
'httponly' => false,
'raw' => !$decode,
'samesite' => null,
'partitioned' => false,
];
$parts = HeaderUtils::split($cookie, ';=');
$part = array_shift($parts);
$name = $decode ? urldecode($part[0]) : $part[0];
$value = isset($part[1]) ? ($decode ? urldecode($part[1]) : $part[1]) : null;
$data = HeaderUtils::combine($parts) + $data;
$data['expires'] = self::expiresTimestamp($data['expires']);
if (isset($data['max-age']) && ($data['max-age'] > 0 || $data['expires'] > time())) {
$data['expires'] = time() + (int) $data['max-age'];
}
return new static($name, $value, $data['expires'], $data['path'], $data['domain'], $data['secure'], $data['httponly'], $data['raw'], $data['samesite'], $data['partitioned']);
}
/**
* @see self::__construct
*
* @param self::SAMESITE_*|''|null $sameSite
*/
public static function create(string $name, ?string $value = null, int|string|\DateTimeInterface $expire = 0, ?string $path = '/', ?string $domain = null, ?bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = self::SAMESITE_LAX, bool $partitioned = false): self
{
return new self($name, $value, $expire, $path, $domain, $secure, $httpOnly, $raw, $sameSite, $partitioned);
}
/**
* @param string $name The name of the cookie
* @param string|null $value The value of the cookie
* @param int|string|\DateTimeInterface $expire The time the cookie expires
* @param string|null $path The path on the server in which the cookie will be available on
* @param string|null $domain The domain that the cookie is available to
* @param bool|null $secure Whether the client should send back the cookie only over HTTPS or null to auto-enable this when the request is already using HTTPS
* @param bool $httpOnly Whether the cookie will be made accessible only through the HTTP protocol
* @param bool $raw Whether the cookie value should be sent with no url encoding
* @param self::SAMESITE_*|''|null $sameSite Whether the cookie will be available for cross-site requests
*
* @throws \InvalidArgumentException
*/
public function __construct(
protected string $name,
protected ?string $value = null,
int|string|\DateTimeInterface $expire = 0,
?string $path = '/',
protected ?string $domain = null,
protected ?bool $secure = null,
protected bool $httpOnly = true,
private bool $raw = false,
?string $sameSite = self::SAMESITE_LAX,
private bool $partitioned = false,
) {
// from PHP source code
if ($raw && false !== strpbrk($name, self::RESERVED_CHARS_LIST)) {
throw new \InvalidArgumentException(\sprintf('The cookie name "%s" contains invalid characters.', $name));
}
if (!$name) {
throw new \InvalidArgumentException('The cookie name cannot be empty.');
}
$this->expire = self::expiresTimestamp($expire);
$this->path = $path ?: '/';
$this->sameSite = $this->withSameSite($sameSite)->sameSite;
}
/**
* Creates a cookie copy with a new value.
*/
public function withValue(?string $value): static
{
$cookie = clone $this;
$cookie->value = $value;
return $cookie;
}
/**
* Creates a cookie copy with a new domain that the cookie is available to.
*/
public function withDomain(?string $domain): static
{
$cookie = clone $this;
$cookie->domain = $domain;
return $cookie;
}
/**
* Creates a cookie copy with a new time the cookie expires.
*/
public function withExpires(int|string|\DateTimeInterface $expire = 0): static
{
$cookie = clone $this;
$cookie->expire = self::expiresTimestamp($expire);
return $cookie;
}
/**
* Converts expires formats to a unix timestamp.
*/
private static function expiresTimestamp(int|string|\DateTimeInterface $expire = 0): int
{
// convert expiration time to a Unix timestamp
if ($expire instanceof \DateTimeInterface) {
$expire = $expire->format('U');
} elseif (!is_numeric($expire)) {
$expire = strtotime($expire);
if (false === $expire) {
throw new \InvalidArgumentException('The cookie expiration time is not valid.');
}
}
return 0 < $expire ? (int) $expire : 0;
}
/**
* Creates a cookie copy with a new path on the server in which the cookie will be available on.
*/
public function withPath(string $path): static
{
$cookie = clone $this;
$cookie->path = '' === $path ? '/' : $path;
return $cookie;
}
/**
* Creates a cookie copy that only be transmitted over a secure HTTPS connection from the client.
*/
public function withSecure(bool $secure = true): static
{
$cookie = clone $this;
$cookie->secure = $secure;
return $cookie;
}
/**
* Creates a cookie copy that be accessible only through the HTTP protocol.
*/
public function withHttpOnly(bool $httpOnly = true): static
{
$cookie = clone $this;
$cookie->httpOnly = $httpOnly;
return $cookie;
}
/**
* Creates a cookie copy that uses no url encoding.
*/
public function withRaw(bool $raw = true): static
{
if ($raw && false !== strpbrk($this->name, self::RESERVED_CHARS_LIST)) {
throw new \InvalidArgumentException(\sprintf('The cookie name "%s" contains invalid characters.', $this->name));
}
$cookie = clone $this;
$cookie->raw = $raw;
return $cookie;
}
/**
* Creates a cookie copy with SameSite attribute.
*
* @param self::SAMESITE_*|''|null $sameSite
*/
public function withSameSite(?string $sameSite): static
{
if ('' === $sameSite) {
$sameSite = null;
} elseif (null !== $sameSite) {
$sameSite = strtolower($sameSite);
}
if (!\in_array($sameSite, [self::SAMESITE_LAX, self::SAMESITE_STRICT, self::SAMESITE_NONE, null], true)) {
throw new \InvalidArgumentException('The "sameSite" parameter value is not valid.');
}
$cookie = clone $this;
$cookie->sameSite = $sameSite;
return $cookie;
}
/**
* Creates a cookie copy that is tied to the top-level site in cross-site context.
*/
public function withPartitioned(bool $partitioned = true): static
{
$cookie = clone $this;
$cookie->partitioned = $partitioned;
return $cookie;
}
/**
* Returns the cookie as a string.
*/
public function __toString(): string
{
if ($this->isRaw()) {
$str = $this->getName();
} else {
$str = str_replace(self::RESERVED_CHARS_FROM, self::RESERVED_CHARS_TO, $this->getName());
}
$str .= '=';
if ('' === (string) $this->getValue()) {
$str .= 'deleted; expires='.gmdate('D, d M Y H:i:s T', time() - 31536001).'; Max-Age=0';
} else {
$str .= $this->isRaw() ? $this->getValue() : rawurlencode($this->getValue());
if (0 !== $this->getExpiresTime()) {
$str .= '; expires='.gmdate('D, d M Y H:i:s T', $this->getExpiresTime()).'; Max-Age='.$this->getMaxAge();
}
}
if ($this->getPath()) {
$str .= '; path='.$this->getPath();
}
if ($this->getDomain()) {
$str .= '; domain='.$this->getDomain();
}
if ($this->isSecure()) {
$str .= '; secure';
}
if ($this->isHttpOnly()) {
$str .= '; httponly';
}
if (null !== $this->getSameSite()) {
$str .= '; samesite='.$this->getSameSite();
}
if ($this->isPartitioned()) {
$str .= '; partitioned';
}
return $str;
}
/**
* Gets the name of the cookie.
*/
public function getName(): string
{
return $this->name;
}
/**
* Gets the value of the cookie.
*/
public function getValue(): ?string
{
return $this->value;
}
/**
* Gets the domain that the cookie is available to.
*/
public function getDomain(): ?string
{
return $this->domain;
}
/**
* Gets the time the cookie expires.
*/
public function getExpiresTime(): int
{
return $this->expire;
}
/**
* Gets the max-age attribute.
*/
public function getMaxAge(): int
{
$maxAge = $this->expire - time();
return max(0, $maxAge);
}
/**
* Gets the path on the server in which the cookie will be available on.
*/
public function getPath(): string
{
return $this->path;
}
/**
* Checks whether the cookie should only be transmitted over a secure HTTPS connection from the client.
*/
public function isSecure(): bool
{
return $this->secure ?? $this->secureDefault;
}
/**
* Checks whether the cookie will be made accessible only through the HTTP protocol.
*/
public function isHttpOnly(): bool
{
return $this->httpOnly;
}
/**
* Whether this cookie is about to be cleared.
*/
public function isCleared(): bool
{
return 0 !== $this->expire && $this->expire < time();
}
/**
* Checks if the cookie value should be sent with no url encoding.
*/
public function isRaw(): bool
{
return $this->raw;
}
/**
* Checks whether the cookie should be tied to the top-level site in cross-site context.
*/
public function isPartitioned(): bool
{
return $this->partitioned;
}
/**
* @return self::SAMESITE_*|null
*/
public function getSameSite(): ?string
{
return $this->sameSite;
}
/**
* @param bool $default The default value of the "secure" flag when it is set to null
*/
public function setSecureDefault(bool $default): void
{
$this->secureDefault = $default;
}
}

View File

@@ -0,0 +1,110 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation;
/**
* Represents a streaming HTTP response for sending server events
* as part of the Server-Sent Events (SSE) streaming technique.
*
* To broadcast events to multiple users at once, for long-running
* connections and for high-traffic websites, prefer using the Mercure
* Symfony Component, which relies on Software designed for these use
* cases: https://symfony.com/doc/current/mercure.html
*
* @see ServerEvent
*
* @author Yonel Ceruto <open@yceruto.dev>
*
* Example usage:
*
* return new EventStreamResponse(function () {
* yield new ServerEvent(time());
*
* sleep(1);
*
* yield new ServerEvent(time());
* });
*/
class EventStreamResponse extends StreamedResponse
{
/**
* @param int|null $retry The number of milliseconds the client should wait
* before reconnecting in case of network failure
*/
public function __construct(?callable $callback = null, int $status = 200, array $headers = [], private ?int $retry = null)
{
$headers += [
'Connection' => 'keep-alive',
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'private, no-cache, no-store, must-revalidate, max-age=0',
'X-Accel-Buffering' => 'no',
'Pragma' => 'no-cache',
'Expire' => '0',
];
parent::__construct($callback, $status, $headers);
}
public function setCallback(callable $callback): static
{
if ($this->callback) {
return parent::setCallback($callback);
}
$this->callback = function () use ($callback) {
if (is_iterable($events = $callback($this))) {
foreach ($events as $event) {
$this->sendEvent($event);
if (connection_aborted()) {
break;
}
}
}
};
return $this;
}
/**
* Sends a server event to the client.
*
* @return $this
*/
public function sendEvent(ServerEvent $event): static
{
if ($this->retry > 0 && !$event->getRetry()) {
$event->setRetry($this->retry);
}
foreach ($event as $part) {
echo $part;
if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
static::closeOutputBuffers(0, true);
flush();
}
}
return $this;
}
public function getRetry(): ?int
{
return $this->retry;
}
public function setRetry(int $retry): void
{
$this->retry = $retry;
}
}

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Exception;
/**
* Raised when a user sends a malformed request.
*/
class BadRequestException extends UnexpectedValueException implements RequestExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Exception;
/**
* The HTTP request contains headers with conflicting information.
*
* @author Magnus Nordlander <magnus@fervo.se>
*/
class ConflictingHeadersException extends UnexpectedValueException implements RequestExceptionInterface
{
}

View File

@@ -0,0 +1,16 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Exception;
interface ExceptionInterface extends \Throwable
{
}

View File

@@ -0,0 +1,26 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Exception;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
final class ExpiredSignedUriException extends SignedUriException
{
/**
* @internal
*/
public function __construct()
{
parent::__construct('The URI has expired.');
}
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Exception;
/**
* Thrown by Request::toArray() when the content cannot be JSON-decoded.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class JsonException extends UnexpectedValueException implements RequestExceptionInterface
{
}

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Exception;
/**
* Base LogicException for Http Foundation component.
*/
class LogicException extends \LogicException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Exception;
/**
* Interface for Request exceptions.
*
* Exceptions implementing this interface should trigger an HTTP 400 response in the application code.
*/
interface RequestExceptionInterface
{
}

View File

@@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Exception;
/**
* Raised when a session does not exist. This happens in the following cases:
* - the session is not enabled
* - attempt to read a session outside a request context (ie. cli script).
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class SessionNotFoundException extends \LogicException implements RequestExceptionInterface
{
public function __construct(string $message = 'There is currently no session available.', int $code = 0, ?\Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Exception;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
abstract class SignedUriException extends \RuntimeException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Exception;
/**
* Raised when a user has performed an operation that should be considered
* suspicious from a security perspective.
*/
class SuspiciousOperationException extends UnexpectedValueException implements RequestExceptionInterface
{
}

View File

@@ -0,0 +1,16 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Exception;
class UnexpectedValueException extends \UnexpectedValueException
{
}

View File

@@ -0,0 +1,26 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Exception;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
final class UnsignedUriException extends SignedUriException
{
/**
* @internal
*/
public function __construct()
{
parent::__construct('The URI is not signed.');
}
}

View File

@@ -0,0 +1,26 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Exception;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
final class UnverifiedSignedUriException extends SignedUriException
{
/**
* @internal
*/
public function __construct()
{
parent::__construct('The URI signature is invalid.');
}
}

View File

@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\File\Exception;
/**
* Thrown when the access on a file was denied.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class AccessDeniedException extends FileException
{
public function __construct(string $path)
{
parent::__construct(\sprintf('The file %s could not be accessed', $path));
}
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\File\Exception;
/**
* Thrown when an UPLOAD_ERR_CANT_WRITE error occurred with UploadedFile.
*
* @author Florent Mata <florentmata@gmail.com>
*/
class CannotWriteFileException extends FileException
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\File\Exception;
/**
* Thrown when an UPLOAD_ERR_EXTENSION error occurred with UploadedFile.
*
* @author Florent Mata <florentmata@gmail.com>
*/
class ExtensionFileException extends FileException
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\File\Exception;
/**
* Thrown when an error occurred in the component File.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FileException extends \RuntimeException
{
}

View File

@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\File\Exception;
/**
* Thrown when a file was not found.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FileNotFoundException extends FileException
{
public function __construct(string $path)
{
parent::__construct(\sprintf('The file "%s" does not exist', $path));
}
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\File\Exception;
/**
* Thrown when an UPLOAD_ERR_FORM_SIZE error occurred with UploadedFile.
*
* @author Florent Mata <florentmata@gmail.com>
*/
class FormSizeFileException extends FileException
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\File\Exception;
/**
* Thrown when an UPLOAD_ERR_INI_SIZE error occurred with UploadedFile.
*
* @author Florent Mata <florentmata@gmail.com>
*/
class IniSizeFileException extends FileException
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\File\Exception;
/**
* Thrown when an UPLOAD_ERR_NO_FILE error occurred with UploadedFile.
*
* @author Florent Mata <florentmata@gmail.com>
*/
class NoFileException extends FileException
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\File\Exception;
/**
* Thrown when an UPLOAD_ERR_NO_TMP_DIR error occurred with UploadedFile.
*
* @author Florent Mata <florentmata@gmail.com>
*/
class NoTmpDirFileException extends FileException
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\File\Exception;
/**
* Thrown when an UPLOAD_ERR_PARTIAL error occurred with UploadedFile.
*
* @author Florent Mata <florentmata@gmail.com>
*/
class PartialFileException extends FileException
{
}

View File

@@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\File\Exception;
class UnexpectedTypeException extends FileException
{
public function __construct(mixed $value, string $expectedType)
{
parent::__construct(\sprintf('Expected argument of type %s, %s given', $expectedType, get_debug_type($value)));
}
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\File\Exception;
/**
* Thrown when an error occurred during file upload.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class UploadException extends FileException
{
}

View File

@@ -0,0 +1,140 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\File;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
use Symfony\Component\Mime\MimeTypes;
/**
* A file in the file system.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class File extends \SplFileInfo
{
/**
* Constructs a new file from the given path.
*
* @param string $path The path to the file
* @param bool $checkPath Whether to check the path or not
*
* @throws FileNotFoundException If the given path is not a file
*/
public function __construct(string $path, bool $checkPath = true)
{
if ($checkPath && !is_file($path)) {
throw new FileNotFoundException($path);
}
parent::__construct($path);
}
/**
* Returns the extension based on the mime type.
*
* If the mime type is unknown, returns null.
*
* This method uses the mime type as guessed by getMimeType()
* to guess the file extension.
*
* @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, 0666 & ~umask());
return $target;
}
public function getContent(): string
{
$content = file_get_contents($this->getPathname());
if (false === $content) {
throw new FileException(\sprintf('Could not get the content of the file "%s".', $this->getPathname()));
}
return $content;
}
protected function getTargetFile(string $directory, ?string $name = null): self
{
if (!is_dir($directory)) {
if (false === @mkdir($directory, 0777, true) && !is_dir($directory)) {
throw new FileException(\sprintf('Unable to create the "%s" directory.', $directory));
}
} elseif (!is_writable($directory)) {
throw new FileException(\sprintf('Unable to write in the "%s" directory.', $directory));
}
$target = rtrim($directory, '/\\').\DIRECTORY_SEPARATOR.(null === $name ? $this->getBasename() : $this->getName($name));
return new self($target, false);
}
/**
* Returns locale independent base name of the given path.
*/
protected function getName(string $name): string
{
$originalName = str_replace('\\', '/', $name);
$pos = strrpos($originalName, '/');
return false === $pos ? $originalName : substr($originalName, $pos + 1);
}
}

View File

@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\File;
/**
* A PHP stream of unknown size.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class Stream extends File
{
public function getSize(): int|false
{
return false;
}
}

View File

@@ -0,0 +1,289 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\File;
use Symfony\Component\HttpFoundation\File\Exception\CannotWriteFileException;
use Symfony\Component\HttpFoundation\File\Exception\ExtensionFileException;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
use Symfony\Component\HttpFoundation\File\Exception\FormSizeFileException;
use Symfony\Component\HttpFoundation\File\Exception\IniSizeFileException;
use Symfony\Component\HttpFoundation\File\Exception\NoFileException;
use Symfony\Component\HttpFoundation\File\Exception\NoTmpDirFileException;
use Symfony\Component\HttpFoundation\File\Exception\PartialFileException;
use Symfony\Component\Mime\MimeTypes;
/**
* A file uploaded through a form.
*
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Florian Eckerstorfer <florian@eckerstorfer.org>
* @author Fabien Potencier <fabien@symfony.com>
*/
class UploadedFile extends File
{
private 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, 0666 & ~umask());
return $target;
}
switch ($this->error) {
case \UPLOAD_ERR_INI_SIZE:
throw new IniSizeFileException($this->getErrorMessage());
case \UPLOAD_ERR_FORM_SIZE:
throw new FormSizeFileException($this->getErrorMessage());
case \UPLOAD_ERR_PARTIAL:
throw new PartialFileException($this->getErrorMessage());
case \UPLOAD_ERR_NO_FILE:
throw new NoFileException($this->getErrorMessage());
case \UPLOAD_ERR_CANT_WRITE:
throw new CannotWriteFileException($this->getErrorMessage());
case \UPLOAD_ERR_NO_TMP_DIR:
throw new NoTmpDirFileException($this->getErrorMessage());
case \UPLOAD_ERR_EXTENSION:
throw new ExtensionFileException($this->getErrorMessage());
}
throw new FileException($this->getErrorMessage());
}
/**
* Returns the maximum size of an uploaded file as configured in php.ini.
*
* @return int|float The maximum size of an uploaded file in bytes (returns float if size > PHP_INT_MAX)
*/
public static function getMaxFilesize(): 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);
}
}

View File

@@ -0,0 +1,127 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation;
use Symfony\Component\HttpFoundation\File\UploadedFile;
/**
* FileBag is a container for uploaded files.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Bulat Shakirzyanov <mallluhuct@gmail.com>
*/
class FileBag extends ParameterBag
{
private const FILE_KEYS = ['error', '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;
}
}

View File

@@ -0,0 +1,273 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation;
/**
* HeaderBag is a container for HTTP headers.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @implements \IteratorAggregate<string, list<string|null>>
*/
class HeaderBag implements \IteratorAggregate, \Countable, \Stringable
{
protected const UPPER = '_ABCDEFGHIJKLMNOPQRSTUVWXYZ';
protected const LOWER = '-abcdefghijklmnopqrstuvwxyz';
/**
* @var array<string, list<string|null>>
*/
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<string, list<string|null>> : list<string|null>)
*/
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<string, list<string|null>>
*/
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);
}
}

View File

@@ -0,0 +1,298 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation;
/**
* HTTP header utility functions.
*
* @author Christian Schmidt <github@chsc.dk>
*/
class HeaderUtils
{
public const DISPOSITION_ATTACHMENT = 'attachment';
public const DISPOSITION_INLINE = 'inline';
/**
* This class should not be instantiated.
*/
private function __construct()
{
}
/**
* Splits an HTTP header by one or more separators.
*
* Example:
*
* HeaderUtils::split('da, en-gb;q=0.8', ',;')
* # 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.']+
)+
(?<!\s)
|
# separator
\s*
(?<separator>['.$quotedSeparators.'])
\s*
/x', trim($header), $matches, \PREG_SET_ORDER);
return self::groupParts($matches, $separators);
}
/**
* Combines an array of arrays into one associative array.
*
* Each of the nested arrays should have one or two elements. The first
* value will be used as the keys in the associative array, and the second
* will be used as the values, or true if the nested array only contains one
* element. Array keys are lowercased.
*
* Example:
*
* HeaderUtils::combine([['foo', 'abc'], ['bar']])
* // => ['foo' => 'abc', 'bar' => true]
*/
public static function combine(array $parts): array
{
$assoc = [];
foreach ($parts as $part) {
$name = strtolower($part[0]);
$value = $part[1] ?? true;
$assoc[$name] = $value;
}
return $assoc;
}
/**
* Joins an associative array into a string for use in an HTTP header.
*
* The key and value of each entry are joined with '=', and all entries
* are joined with the specified separator and an additional space (for
* readability). Values are quoted if necessary.
*
* Example:
*
* HeaderUtils::toString(['foo' => 'abc', 'bar' => true, 'baz' => 'a b c'], ',')
* // => 'foo=abc, bar, baz="a b c"'
*/
public static function toString(array $assoc, string $separator): string
{
$parts = [];
foreach ($assoc as $name => $value) {
if (true === $value) {
$parts[] = $name;
} else {
$parts[] = $name.'='.self::quote($value);
}
}
return implode($separator.' ', $parts);
}
/**
* Encodes a string as a quoted string, if necessary.
*
* If a string contains characters not allowed by the "token" construct in
* the HTTP specification, it is backslash-escaped and enclosed in quotes
* to match the "quoted-string" construct.
*/
public static function quote(string $s): string
{
if (preg_match('/^[a-z0-9!#$%&\'*.^_`|~-]+$/i', $s)) {
return $s;
}
return '"'.addcslashes($s, '"\\"').'"';
}
/**
* Decodes a quoted string.
*
* If passed an unquoted string that matches the "token" construct (as
* defined in the HTTP specification), it is passed through 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])) {
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;
}
}

View File

@@ -0,0 +1,146 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\HttpFoundation\Exception\UnexpectedValueException;
/**
* InputBag is a container for user input values such as $_GET, $_POST, $_REQUEST, and $_COOKIE.
*
* @author Saif Eddin Gmati <azjezz@protonmail.com>
*/
final class InputBag extends ParameterBag
{
/**
* Returns a scalar input value by name.
*
* @param string|int|float|bool|null $default The default value if the input key does not exist
*
* @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<T> $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));
}
}

View File

@@ -0,0 +1,273 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation;
/**
* Http utility functions.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class IpUtils
{
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 <dsp at php dot net>
*
* @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
{
$v4Bytes = 1 < \func_num_args() ? func_get_arg(1) : 1;
$v6Bytes = 2 < \func_num_args() ? func_get_arg(2) : 8;
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;
}
}

View File

@@ -0,0 +1,187 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation;
/**
* Response represents an HTTP response in JSON format.
*
* Note that this class does not force the returned JSON content to be an
* object. It is however recommended that you do return an object as it
* protects yourself against XSSI and JSON-JavaScript Hijacking.
*
* @see https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/AJAX_Security_Cheat_Sheet.md#always-return-json-with-an-object-on-the-outside
*
* @author Igor Wiedler <igor@wiedler.ch>
*/
class JsonResponse extends Response
{
protected 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 <william.durand1@gmail.com>
$pattern = '/^[$_\p{L}][$_\p{L}\p{Mn}\p{Mc}\p{Nd}\p{Pc}\x{200C}\x{200D}]*(?:\[(?:"(?:\\\.|[^"\\\])*"|\'(?:\\\.|[^\'\\\])*\'|\d+)\])*?$/u';
$reserved = [
'break', 'do', 'instanceof', 'typeof', 'case', 'else', 'new', 'var', 'catch', 'finally', 'return', 'void', 'continue', 'for', 'switch', 'while',
'debugger', 'function', 'this', 'with', 'default', 'if', 'throw', 'delete', 'in', 'try', 'class', 'enum', 'extends', 'super', 'const', 'export',
'import', 'implements', 'let', 'private', 'public', 'yield', 'interface', 'package', 'protected', 'static', 'null', 'true', 'false',
];
$parts = explode('.', $callback);
foreach ($parts as $part) {
if (!preg_match($pattern, $part) || \in_array($part, $reserved, true)) {
throw new \InvalidArgumentException('The callback name is not valid.');
}
}
}
$this->callback = $callback;
return $this->update();
}
/**
* Sets a raw string containing a JSON document to be sent.
*
* @return $this
*/
public function setJson(string $json): 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);
}
}

View File

@@ -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.

View File

@@ -0,0 +1,258 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\HttpFoundation\Exception\UnexpectedValueException;
/**
* ParameterBag is a container for key/value pairs.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @implements \IteratorAggregate<string, mixed>
*/
class ParameterBag implements \IteratorAggregate, \Countable
{
public function __construct(
protected array $parameters = [],
) {
}
/**
* Returns the parameters.
*
* @param string|null $key The name of the parameter to return or null to get them all
*
* @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.
*/
public function keys(): array
{
return array_keys($this->parameters);
}
/**
* Replaces the current parameters by a new set.
*/
public function replace(array $parameters = []): void
{
$this->parameters = $parameters;
}
/**
* Adds 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<T> $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: %s.', $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<string, mixed>
*/
public function getIterator(): \ArrayIterator
{
return new \ArrayIterator($this->parameters);
}
/**
* Returns the number of parameters.
*/
public function count(): int
{
return \count($this->parameters);
}
}

View File

@@ -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)

View File

@@ -0,0 +1,81 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\RateLimiter;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\RateLimiter\LimiterInterface;
use Symfony\Component\RateLimiter\Policy\NoLimiter;
use Symfony\Component\RateLimiter\RateLimit;
/**
* An implementation of PeekableRequestRateLimiterInterface that
* fits most use-cases.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
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;
}
}

View File

@@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\RateLimiter;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\RateLimiter\RateLimit;
/**
* A 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 <j.boggiano@seld.be>
*/
interface PeekableRequestRateLimiterInterface extends RequestRateLimiterInterface
{
public function peek(Request $request): RateLimit;
}

View File

@@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\RateLimiter;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\RateLimiter\RateLimit;
/**
* A special type of limiter that deals with requests.
*
* This allows to limit on different types of information
* from the requests.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
interface RequestRateLimiterInterface
{
public function consume(Request $request): RateLimit;
public function reset(Request $request): void;
}

View File

@@ -0,0 +1,92 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation;
/**
* RedirectResponse represents an HTTP response doing a redirect.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class RedirectResponse extends Response
{
protected 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('<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="refresh" content="0;url=\'%1$s\'" />
<title>Redirecting to %1$s</title>
</head>
<body>
Redirecting to <a href="%1$s">%1$s</a>.
</body>
</html>', htmlspecialchars($url, \ENT_QUOTES, 'UTF-8')));
$this->headers->set('Location', $url);
$this->headers->set('Content-Type', 'text/html; charset=utf-8');
return $this;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,45 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\RequestMatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
/**
* Checks the Request attributes matches all regular expressions.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class AttributesRequestMatcher implements RequestMatcherInterface
{
/**
* @param array<string, string> $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;
}
}

View File

@@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\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 <fabien@symfony.com>
*/
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(),
]);
}
}

View File

@@ -0,0 +1,52 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\RequestMatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
/**
* Checks the presence of HTTP headers in a Request.
*
* @author Alexandre Daubois <alex.daubois@gmail.com>
*/
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;
}
}

View File

@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\RequestMatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
/**
* Checks the Request URL host name matches a regular expression.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class HostRequestMatcher implements RequestMatcherInterface
{
public function __construct(private string $regexp)
{
}
public function matches(Request $request): bool
{
return preg_match('{'.$this->regexp.'}i', $request->getHost());
}
}

View File

@@ -0,0 +1,44 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\RequestMatcher;
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 <fabien@symfony.com>
*/
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);
}
}

View File

@@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\RequestMatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
/**
* Checks the Request content is valid JSON.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class IsJsonRequestMatcher implements RequestMatcherInterface
{
public function matches(Request $request): bool
{
return json_validate($request->getContent());
}
}

View File

@@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\RequestMatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
/**
* Checks the HTTP method of a Request.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
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);
}
}

View File

@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\RequestMatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
/**
* Checks the Request URL path info matches a regular expression.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class PathRequestMatcher implements RequestMatcherInterface
{
public function __construct(private string $regexp)
{
}
public function matches(Request $request): bool
{
return preg_match('{'.$this->regexp.'}', rawurldecode($request->getPathInfo()));
}
}

View File

@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\RequestMatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
/**
* Checks the HTTP port of a Request.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class PortRequestMatcher implements RequestMatcherInterface
{
public function __construct(private int $port)
{
}
public function matches(Request $request): bool
{
return $request->getPort() === $this->port;
}
}

View File

@@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\RequestMatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
/**
* Checks the presence of HTTP query parameters of a Request.
*
* @author Alexandre Daubois <alex.daubois@gmail.com>
*/
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()));
}
}

View File

@@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\RequestMatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
/**
* Checks the HTTP scheme of a Request.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
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);
}
}

View File

@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation;
/**
* RequestMatcherInterface is an interface for strategies to match a Request.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface RequestMatcherInterface
{
/**
* Decides whether the rule(s) implemented by the strategy matches the supplied request.
*/
public function matches(Request $request): bool;
}

View File

@@ -0,0 +1,124 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation;
use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
/**
* Request stack that controls the lifecycle of requests.
*
* @author Benjamin Eberlei <kontakt@beberlei.de>
*/
class RequestStack
{
/**
* @var Request[]
*/
private 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();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,271 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation;
/**
* ResponseHeaderBag is a container for Response HTTP headers.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class ResponseHeaderBag extends HeaderBag
{
public const COOKIES_FLAT = 'flat';
public const COOKIES_ARRAY = 'array';
public const DISPOSITION_ATTACHMENT = 'attachment';
public const DISPOSITION_INLINE = 'inline';
protected 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])) {
throw new \InvalidArgumentException(\sprintf('Format "%s" invalid (%s).', $format, implode(', ', [self::COOKIES_FLAT, self::COOKIES_ARRAY])));
}
if (self::COOKIES_ARRAY === $format) {
return $this->cookies;
}
$flattenedCookies = [];
foreach ($this->cookies as $path) {
foreach ($path as $cookies) {
foreach ($cookies as $cookie) {
$flattenedCookies[] = $cookie;
}
}
}
return $flattenedCookies;
}
/**
* Clears a cookie in the browser.
*
* @param bool $partitioned
*/
public function clearCookie(string $name, ?string $path = '/', ?string $domain = null, bool $secure = false, bool $httpOnly = true, ?string $sameSite = null /* , bool $partitioned = false */): void
{
$partitioned = 6 < \func_num_args() ? func_get_arg(6) : false;
$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');
}
}

View File

@@ -0,0 +1,97 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation;
/**
* ServerBag is a container for HTTP headers from the $_SERVER variable.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Bulat Shakirzyanov <mallluhuct@gmail.com>
* @author Robert Kiss <kepten@gmail.com>
*/
class ServerBag extends ParameterBag
{
/**
* Gets the HTTP headers.
*/
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;
}
}

View File

@@ -0,0 +1,147 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation;
/**
* An event generated on the server intended for streaming to the client
* as part of the SSE streaming technique.
*
* @implements \IteratorAggregate<string>
*
* @author Yonel Ceruto <open@yceruto.dev>
*/
class ServerEvent implements \IteratorAggregate
{
/**
* @param string|iterable<string> $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<string>
*/
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 ($this->data) {
if (is_iterable($this->data)) {
foreach ($this->data as $data) {
yield \sprintf('data: %s', $data)."\n";
}
} else {
yield \sprintf('data: %s', $this->data)."\n";
}
}
yield "\n";
}
}

View File

@@ -0,0 +1,117 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session\Attribute;
/**
* This class relates to session attribute storage.
*
* @implements \IteratorAggregate<string, mixed>
*/
class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Countable
{
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<string, mixed>
*/
public function getIterator(): \ArrayIterator
{
return new \ArrayIterator($this->attributes);
}
/**
* Returns the number of attributes.
*/
public function count(): int
{
return \count($this->attributes);
}
}

View File

@@ -0,0 +1,53 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session\Attribute;
use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
/**
* Attributes store.
*
* @author Drak <drak@zikula.org>
*/
interface AttributeBagInterface extends SessionBagInterface
{
/**
* Checks if an attribute is defined.
*/
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<string, mixed>
*/
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;
}

View File

@@ -0,0 +1,121 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session\Flash;
/**
* AutoExpireFlashBag flash message container.
*
* @author Drak <drak@zikula.org>
*/
class AutoExpireFlashBag implements FlashBagInterface
{
private 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();
}
}

View File

@@ -0,0 +1,112 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session\Flash;
/**
* FlashBag flash message container.
*
* @author Drak <drak@zikula.org>
*/
class FlashBag implements FlashBagInterface
{
private 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();
}
}

View File

@@ -0,0 +1,72 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session\Flash;
use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
/**
* FlashBagInterface.
*
* @author Drak <drak@zikula.org>
*/
interface FlashBagInterface extends SessionBagInterface
{
/**
* Adds a flash message for the given type.
*/
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;
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
/**
* Interface for session with a flashbag.
*/
interface FlashBagAwareSessionInterface extends SessionInterface
{
public function getFlashBag(): FlashBagInterface;
}

View File

@@ -0,0 +1,223 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session;
use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag;
use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBag;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
use Symfony\Component\HttpFoundation\Session\Storage\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 <fabien@symfony.com>
* @author Drak <drak@zikula.org>
*
* @implements \IteratorAggregate<string, mixed>
*/
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<string, mixed>
*/
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);
}
}

View File

@@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session;
/**
* Session Bag store.
*
* @author Drak <drak@zikula.org>
*/
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;
}

View File

@@ -0,0 +1,86 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
final class SessionBagProxy implements SessionBagInterface
{
private 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();
}
}

View File

@@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageFactoryInterface;
// Help opcache.preload discover always-needed symbols
class_exists(Session::class);
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class SessionFactory implements SessionFactoryInterface
{
private ?\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);
}
}

View File

@@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
interface SessionFactoryInterface
{
public function createSession(): SessionInterface;
}

View File

@@ -0,0 +1,140 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag;
/**
* Interface for the session.
*
* @author Drak <drak@zikula.org>
*/
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;
}

View File

@@ -0,0 +1,59 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session;
/**
* Session utility functions.
*
* @author Nicolas Grekas <p@tchwork.com>
* @author Rémon van de Kamp <rpkamp@gmail.com>
*
* @internal
*/
final class SessionUtils
{
/**
* Finds the session header amongst the headers that are to be sent, removes it, and returns
* it so the caller can process it further.
*/
public static function popSessionCookie(string $sessionName, #[\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;
}
}

View File

@@ -0,0 +1,111 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
use Symfony\Component\HttpFoundation\Session\SessionUtils;
/**
* This abstract session handler provides a generic implementation
* of the PHP 7.0 SessionUpdateTimestampHandlerInterface,
* enabling strict and lazy session handling.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
abstract class AbstractSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface
{
private 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);
}
}

View File

@@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
/**
* @author Ahmed TAILOULOUTE <ahmed.tailouloute@gmail.com>
*/
class IdentityMarshaller implements MarshallerInterface
{
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;
}
}

View File

@@ -0,0 +1,73 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
/**
* @author Ahmed TAILOULOUTE <ahmed.tailouloute@gmail.com>
*/
class MarshallingSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface
{
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);
}
}

Some files were not shown because too many files have changed in this diff Show More