Initial Commit

This commit is contained in:
David Stone
2024-11-30 18:24:12 -07:00
commit e8f7955c1c
5432 changed files with 1397750 additions and 0 deletions

View File

@ -0,0 +1,322 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Http\Cookie;
/**
* Cookie attributes as defined in https://tools.ietf.org/html/rfc6265.
*
* @link https://tools.ietf.org/html/rfc6265
*/
final class CookieAttributes
{
public const SAMESITE_NONE = 'None';
public const SAMESITE_LAX = 'Lax';
public const SAMESITE_STRICT = 'Strict';
/**
* @return CookieAttributes No cookie attributes.
*
* @see self::default()
*/
public static function empty() : self
{
$new = new self();
$new->httpOnly = \false;
return $new;
}
/**
* @return CookieAttributes Default cookie attributes, which means httpOnly is enabled by default.
*
* @see self::empty()
*/
public static function default() : self
{
return new self();
}
/** @var string */
private $path = '';
/** @var string */
private $domain = '';
/** @var int|null */
private $maxAge;
/** @var \DateTimeImmutable */
private $expiry;
/** @var bool */
private $secure = \false;
/** @var bool */
private $httpOnly = \true;
/** @var string|null */
private $sameSite;
private function __construct()
{
// only allow creation via named constructors
}
/**
* @param string $path Cookie path.
*
* @return self Cloned instance with the specified operation applied. Cloned instance with the specified operation
* applied.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.4
*/
public function withPath(string $path) : self
{
$new = clone $this;
$new->path = $path;
return $new;
}
/**
* @param string $domain Cookie domain.
*
* @return self Cloned instance with the specified operation applied.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.3
*/
public function withDomain(string $domain) : self
{
$new = clone $this;
$new->domain = $domain;
return $new;
}
/**
* @param string $sameSite Cookie SameSite attribute value.
*
* @return self Cloned instance with the specified operation applied.
*
* @link https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-5.3.7
*/
public function withSameSite(string $sameSite) : self
{
$normalizedValue = \ucfirst(\strtolower($sameSite));
if (!\in_array($normalizedValue, [self::SAMESITE_NONE, self::SAMESITE_LAX, self::SAMESITE_STRICT], \true)) {
throw new \Error("Invalid SameSite attribute: " . $sameSite);
}
$new = clone $this;
$new->sameSite = $normalizedValue;
return $new;
}
/**
* @return self Cloned instance with the specified operation applied.
*
* @link https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-5.3.7
*/
public function withoutSameSite() : self
{
$new = clone $this;
$new->sameSite = null;
return $new;
}
/**
* Applies the given maximum age to the cookie.
*
* @param int $maxAge Cookie maximum age.
*
* @return self Cloned instance with the specified operation applied.
*
* @see self::withoutMaxAge()
* @see self::withExpiry()
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.2
*/
public function withMaxAge(int $maxAge) : self
{
$new = clone $this;
$new->maxAge = $maxAge;
return $new;
}
/**
* Removes any max-age information.
*
* @return self Cloned instance with the specified operation applied.
*
* @see self::withMaxAge()
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.2
*/
public function withoutMaxAge() : self
{
$new = clone $this;
$new->maxAge = null;
return $new;
}
/**
* Applies the given expiry to the cookie.
*
* @return self Cloned instance with the specified operation applied.
*
* @see self::withMaxAge()
* @see self::withoutExpiry()
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.1
*/
public function withExpiry(\DateTimeInterface $date) : self
{
$new = clone $this;
if ($date instanceof \DateTimeImmutable) {
$new->expiry = $date;
} elseif ($date instanceof \DateTime) {
$new->expiry = \DateTimeImmutable::createFromMutable($date);
} else {
$new->expiry = new \DateTimeImmutable("@" . $date->getTimestamp());
}
return $new;
}
/**
* Removes any expiry information.
*
* @return self Cloned instance with the specified operation applied.
*
* @see self::withExpiry()
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.1
*/
public function withoutExpiry() : self
{
$new = clone $this;
$new->expiry = null;
return $new;
}
/**
* @return self Cloned instance with the specified operation applied.
*
* @see self::withoutSecure()
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.5
*/
public function withSecure() : self
{
$new = clone $this;
$new->secure = \true;
return $new;
}
/**
* @return self Cloned instance with the specified operation applied.
*
* @see self::withSecure()
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.5
*/
public function withoutSecure() : self
{
$new = clone $this;
$new->secure = \false;
return $new;
}
/**
* @return self Cloned instance with the specified operation applied.
*
* @see self::withoutHttpOnly()
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.6
*/
public function withHttpOnly() : self
{
$new = clone $this;
$new->httpOnly = \true;
return $new;
}
/**
* @return self Cloned instance with the specified operation applied.
*
* @see self::withHttpOnly()
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.6
*/
public function withoutHttpOnly() : self
{
$new = clone $this;
$new->httpOnly = \false;
return $new;
}
/**
* @return string Cookie path.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.4
*/
public function getPath() : string
{
return $this->path;
}
/**
* @return string Cookie domain.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.3
*/
public function getDomain() : string
{
return $this->domain;
}
/**
* @return string Cookie domain.
*
* @link https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-5.3.7
*/
public function getSameSite() : ?string
{
return $this->sameSite;
}
/**
* @return int|null Cookie maximum age in seconds or `null` if no value is set.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.2
*/
public function getMaxAge() : ?int
{
return $this->maxAge;
}
/**
* @return \DateTimeImmutable|null Cookie expiry or `null` if no value is set.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.2
*/
public function getExpiry() : ?\DateTimeImmutable
{
return $this->expiry;
}
/**
* @return bool Whether the secure flag is enabled or not.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.5
*/
public function isSecure() : bool
{
return $this->secure;
}
/**
* @return bool Whether the httpOnly flag is enabled or not.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.6
*/
public function isHttpOnly() : bool
{
return $this->httpOnly;
}
/**
* @return string Representation of the cookie attributes appended to key=value in a 'set-cookie' header.
*/
public function __toString() : string
{
$string = '';
if ($this->expiry) {
$string .= '; Expires=' . \gmdate('D, j M Y G:i:s T', $this->expiry->getTimestamp());
}
if ($this->maxAge) {
$string .= '; Max-Age=' . $this->maxAge;
}
if ('' !== $this->path) {
$string .= '; Path=' . $this->path;
}
if ('' !== $this->domain) {
$string .= '; Domain=' . $this->domain;
}
if ($this->secure) {
$string .= '; Secure';
}
if ($this->httpOnly) {
$string .= '; HttpOnly';
}
if ($this->sameSite !== null) {
$string .= '; SameSite=' . $this->sameSite;
}
return $string;
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Http\Cookie;
final class InvalidCookieException extends \Exception
{
public function __construct(string $message)
{
parent::__construct($message);
}
}

View File

@ -0,0 +1,107 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Http\Cookie;
/**
* A cookie as sent in a request's 'cookie' header, so without any attributes.
*
* This class does not deal with encoding of arbitrary names and values. If you want to use arbitrary values, please use
* an encoding mechanism like Base64 or URL encoding.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.4
*/
final class RequestCookie
{
/** @var string */
private $name;
/** @var string */
private $value;
/**
* Parses the cookies from a 'cookie' header.
*
* Note: Parsing is aborted if there's an invalid value and no cookies are returned.
*
* @param string $string Valid 'cookie' header line.
*
* @return RequestCookie[]
*/
public static function fromHeader(string $string) : array
{
$cookies = \explode(";", $string);
$result = [];
try {
foreach ($cookies as $cookie) {
// Ignore zero-length cookie.
if (\trim($cookie) === '') {
continue;
}
$parts = \explode('=', $cookie, 2);
if (2 !== \count($parts)) {
return [];
}
list($name, $value) = $parts;
// We can safely trim quotes, as they're not allowed within cookie values
$result[] = new self(\trim($name), \trim($value, " \t\""));
}
} catch (InvalidCookieException $e) {
return [];
}
return $result;
}
/**
* @param string $name Cookie name in its decoded form.
* @param string $value Cookie value in its decoded form.
*
* @throws InvalidCookieException If name or value is invalid.
*/
public function __construct(string $name, string $value = '')
{
if (!\preg_match('(^[^()<>@,;:\\\\"/[\\]?={}\\x01-\\x20\\x7F]*+$)', $name)) {
throw new InvalidCookieException("Invalid cookie name: '{$name}'");
}
if (!\preg_match('(^[\\x21\\x23-\\x2B\\x2D-\\x3A\\x3C-\\x5B\\x5D-\\x7E]*+$)', $value)) {
throw new InvalidCookieException("Invalid cookie value: '{$value}'");
}
$this->name = $name;
$this->value = $value;
}
/**
* @return string Name of the cookie.
*/
public function getName() : string
{
return $this->name;
}
public function withName(string $name) : self
{
if (!\preg_match('(^[^()<>@,;:\\\\"/[\\]?={}\\x01-\\x20\\x7F]++$)', $name)) {
throw new InvalidCookieException("Invalid cookie name: '{$name}'");
}
$clone = clone $this;
$clone->name = $name;
return $clone;
}
/**
* @return string Value of the cookie.
*/
public function getValue() : string
{
return $this->value;
}
public function withValue(string $value) : self
{
if (!\preg_match('(^[\\x21\\x23-\\x2B\\x2D-\\x3A\\x3C-\\x5B\\x5D-\\x7E]*+$)', $value)) {
throw new InvalidCookieException("Invalid cookie value: '{$value}'");
}
$clone = clone $this;
$clone->value = $value;
return $clone;
}
/**
* @return string Representation of the cookie as in a 'cookie' header.
*/
public function __toString() : string
{
return $this->name . '=' . $this->value;
}
}

View File

@ -0,0 +1,306 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Http\Cookie;
/**
* A cookie as sent in a response's 'set-cookie' header, so with attributes.
*
* This class does not deal with encoding of arbitrary names and values. If you want to use arbitrary values, please use
* an encoding mechanism like Base64 or URL encoding.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2
*/
final class ResponseCookie
{
private static $dateFormats = ['D, d M Y H:i:s T', 'D, d-M-y H:i:s T', 'D, d-M-Y H:i:s T', 'D, d-m-y H:i:s T', 'D, d-m-Y H:i:s T', 'D M j G:i:s Y', 'D M d H:i:s Y T'];
/**
* Parses a cookie from a 'set-cookie' header.
*
* @param string $string Valid 'set-cookie' header line.
*
* @return self|null Returns a `ResponseCookie` instance on success and `null` on failure.
*/
public static function fromHeader(string $string) : ?self
{
$parts = \array_map("trim", \explode(";", $string));
$nameValue = \explode("=", \array_shift($parts), 2);
if (\count($nameValue) !== 2) {
return null;
}
list($name, $value) = $nameValue;
$name = \trim($name);
$value = \trim($value, " \t\n\r\x00\v\"");
if ($name === "") {
return null;
}
// httpOnly must default to false for parsing
$meta = CookieAttributes::empty();
$unknownAttributes = [];
foreach ($parts as $part) {
$pieces = \array_map('trim', \explode('=', $part, 2));
$key = \strtolower($pieces[0]);
if (1 === \count($pieces)) {
switch ($key) {
case 'secure':
$meta = $meta->withSecure();
break;
case 'httponly':
$meta = $meta->withHttpOnly();
break;
default:
$unknownAttributes[] = $part;
break;
}
} else {
switch ($key) {
case 'expires':
$time = self::parseDate($pieces[1]);
if ($time === null) {
break;
// break is correct, see https://tools.ietf.org/html/rfc6265#section-5.2.1
}
$meta = $meta->withExpiry($time);
break;
case 'max-age':
$maxAge = \trim($pieces[1]);
// This also allows +1.42, but avoids a more complicated manual check
if (!\is_numeric($maxAge)) {
break;
// break is correct, see https://tools.ietf.org/html/rfc6265#section-5.2.2
}
$meta = $meta->withMaxAge($maxAge);
break;
case 'path':
$meta = $meta->withPath($pieces[1]);
break;
case 'domain':
$meta = $meta->withDomain($pieces[1]);
break;
case 'samesite':
$normalizedValue = \ucfirst(\strtolower($pieces[1]));
if (!\in_array($normalizedValue, [CookieAttributes::SAMESITE_NONE, CookieAttributes::SAMESITE_LAX, CookieAttributes::SAMESITE_STRICT], \true)) {
$unknownAttributes[] = $part;
} else {
$meta = $meta->withSameSite($normalizedValue);
}
break;
default:
$unknownAttributes[] = $part;
break;
}
}
}
try {
$cookie = new self($name, $value, $meta);
$cookie->unknownAttributes = $unknownAttributes;
return $cookie;
} catch (InvalidCookieException $e) {
return null;
}
}
/**
* @param string $date Formatted cookie date
*
* @return \DateTimeImmutable|null Parsed date.
*/
private static function parseDate(string $date) : ?\DateTimeImmutable
{
foreach (self::$dateFormats as $dateFormat) {
if ($parsedDate = \DateTimeImmutable::createFromFormat($dateFormat, $date, new \DateTimeZone('GMT'))) {
return $parsedDate;
}
}
return null;
}
/** @var string[] */
private $unknownAttributes = [];
/** @var string */
private $name;
/** @var string */
private $value;
/** @var CookieAttributes */
private $attributes;
/**
* @param string $name Name of the cookie.
* @param string $value Value of the cookie.
* @param CookieAttributes $attributes Attributes of the cookie.
*
* @throws InvalidCookieException If name or value is invalid.
*/
public function __construct(string $name, string $value = '', CookieAttributes $attributes = null)
{
if (!\preg_match('(^[^()<>@,;:\\\\"/[\\]?={}\\x01-\\x20\\x7F]++$)', $name)) {
throw new InvalidCookieException("Invalid cookie name: '{$name}'");
}
if (!\preg_match('(^[\\x21\\x23-\\x2B\\x2D-\\x3A\\x3C-\\x5B\\x5D-\\x7E]*+$)', $value)) {
throw new InvalidCookieException("Invalid cookie value: '{$value}'");
}
$this->name = $name;
$this->value = $value;
$this->attributes = $attributes ?? CookieAttributes::default();
}
/**
* @return string Name of the cookie.
*/
public function getName() : string
{
return $this->name;
}
public function withName(string $name) : self
{
if (!\preg_match('(^[^()<>@,;:\\\\"/[\\]?={}\\x01-\\x20\\x7F]++$)', $name)) {
throw new InvalidCookieException("Invalid cookie name: '{$name}'");
}
$clone = clone $this;
$clone->name = $name;
return $clone;
}
/**
* @return string Value of the cookie.
*/
public function getValue() : string
{
return $this->value;
}
public function withValue(string $value) : self
{
if (!\preg_match('(^[\\x21\\x23-\\x2B\\x2D-\\x3A\\x3C-\\x5B\\x5D-\\x7E]*+$)', $value)) {
throw new InvalidCookieException("Invalid cookie value: '{$value}'");
}
$clone = clone $this;
$clone->value = $value;
return $clone;
}
/**
* @return \DateTimeImmutable|null Expiry if set, otherwise `null`.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.1
*/
public function getExpiry() : ?\DateTimeImmutable
{
return $this->attributes->getExpiry();
}
public function withExpiry(\DateTimeInterface $expiry) : self
{
return $this->withAttributes($this->attributes->withExpiry($expiry));
}
public function withoutExpiry() : self
{
return $this->withAttributes($this->attributes->withoutExpiry());
}
/**
* @return int|null Max-Age if set, otherwise `null`.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.2
*/
public function getMaxAge() : ?int
{
return $this->attributes->getMaxAge();
}
public function withMaxAge(int $maxAge) : self
{
return $this->withAttributes($this->attributes->withMaxAge($maxAge));
}
public function withoutMaxAge() : self
{
return $this->withAttributes($this->attributes->withoutMaxAge());
}
/**
* @return string Cookie path.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.4
*/
public function getPath() : string
{
return $this->attributes->getPath();
}
public function withPath(string $path) : self
{
return $this->withAttributes($this->attributes->withPath($path));
}
/**
* @return string Cookie domain.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.3
*/
public function getDomain() : string
{
return $this->attributes->getDomain();
}
public function withDomain(string $domain) : self
{
return $this->withAttributes($this->attributes->withDomain($domain));
}
/**
* @return bool Whether the secure flag is enabled or not.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.5
*/
public function isSecure() : bool
{
return $this->attributes->isSecure();
}
public function withSecure() : self
{
return $this->withAttributes($this->attributes->withSecure());
}
public function withoutSecure() : self
{
return $this->withAttributes($this->attributes->withoutSecure());
}
/**
* @return bool Whether the httpOnly flag is enabled or not.
*
* @link https://tools.ietf.org/html/rfc6265#section-5.2.6
*/
public function isHttpOnly() : bool
{
return $this->attributes->isHttpOnly();
}
public function withHttpOnly() : self
{
return $this->withAttributes($this->attributes->withHttpOnly());
}
public function withoutHttpOnly() : self
{
return $this->withAttributes($this->attributes->withoutHttpOnly());
}
public function withSameSite(string $sameSite) : self
{
return $this->withAttributes($this->attributes->withSameSite($sameSite));
}
public function withoutSameSite() : self
{
return $this->withAttributes($this->attributes->withoutSameSite());
}
public function getSameSite() : ?string
{
return $this->attributes->getSameSite();
}
/**
* @return CookieAttributes All cookie attributes.
*/
public function getAttributes() : CookieAttributes
{
return $this->attributes;
}
public function withAttributes(CookieAttributes $attributes) : self
{
$clone = clone $this;
$clone->attributes = $attributes;
return $clone;
}
/**
* @return string Representation of the cookie as in a 'set-cookie' header.
*/
public function __toString() : string
{
$line = $this->name . '=' . $this->value;
$line .= $this->attributes;
$unknownAttributes = \implode('; ', $this->unknownAttributes);
if ($unknownAttributes !== '') {
$line .= '; ' . $unknownAttributes;
}
return $line;
}
}