Viewing file: Cookie.php (18.86 KB) -rw-r--r-- Select action/file-type: (+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
<?php
/** * This file is part of CodeIgniter 4 framework. * * (c) CodeIgniter Foundation <admin@codeigniter.com> * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */
namespace CodeIgniter\Cookie;
use ArrayAccess; use CodeIgniter\Cookie\Exceptions\CookieException; use Config\Cookie as CookieConfig; use DateTimeInterface; use InvalidArgumentException; use LogicException; use ReturnTypeWillChange;
/** * A `Cookie` class represents an immutable HTTP cookie value object. * * Being immutable, modifying one or more of its attributes will return * a new `Cookie` instance, rather than modifying itself. Users should * reassign this new instance to a new variable to capture it. * * ```php * $cookie = new Cookie('test_cookie', 'test_value'); * $cookie->getName(); // test_cookie * * $cookie->withName('prod_cookie'); * $cookie->getName(); // test_cookie * * $cookie2 = $cookie->withName('prod_cookie'); * $cookie2->getName(); // prod_cookie * ``` */ class Cookie implements ArrayAccess, CloneableCookieInterface { /** * @var string */ protected $prefix = '';
/** * @var string */ protected $name;
/** * @var string */ protected $value;
/** * @var int */ protected $expires;
/** * @var string */ protected $path = '/';
/** * @var string */ protected $domain = '';
/** * @var bool */ protected $secure = false;
/** * @var bool */ protected $httponly = true;
/** * @var string */ protected $samesite = self::SAMESITE_LAX;
/** * @var bool */ protected $raw = false;
/** * Default attributes for a Cookie object. The keys here are the * lowercase attribute names. Do not camelCase! * * @var array<string, mixed> */ private static array $defaults = [ 'prefix' => '', 'expires' => 0, 'path' => '/', 'domain' => '', 'secure' => false, 'httponly' => true, 'samesite' => self::SAMESITE_LAX, 'raw' => false, ];
/** * A cookie name can be any US-ASCII characters, except control characters, * spaces, tabs, or separator characters. * * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes * @see https://tools.ietf.org/html/rfc2616#section-2.2 */ private static string $reservedCharsList = "=,; \t\r\n\v\f()<>@:\\\"/[]?{}";
/** * Set the default attributes to a Cookie instance by injecting * the values from the `CookieConfig` config or an array. * * @param array<string, mixed>|CookieConfig $config * * @return array<string, mixed> The old defaults array. Useful for resetting. */ public static function setDefaults($config = []) { $oldDefaults = self::$defaults; $newDefaults = [];
if ($config instanceof CookieConfig) { $newDefaults = [ 'prefix' => $config->prefix, 'expires' => $config->expires, 'path' => $config->path, 'domain' => $config->domain, 'secure' => $config->secure, 'httponly' => $config->httponly, 'samesite' => $config->samesite, 'raw' => $config->raw, ]; } elseif (is_array($config)) { $newDefaults = $config; }
// This array union ensures that even if passed `$config` is not // `CookieConfig` or `array`, no empty defaults will occur. self::$defaults = $newDefaults + $oldDefaults;
return $oldDefaults; }
//========================================================================= // CONSTRUCTORS //=========================================================================
/** * Create a new Cookie instance from a `Set-Cookie` header. * * @throws CookieException * * @return static */ public static function fromHeaderString(string $cookie, bool $raw = false) { $data = self::$defaults; $data['raw'] = $raw;
$parts = preg_split('/\;[\s]*/', $cookie); $part = explode('=', array_shift($parts), 2);
$name = $raw ? $part[0] : urldecode($part[0]); $value = isset($part[1]) ? ($raw ? $part[1] : urldecode($part[1])) : ''; unset($part);
foreach ($parts as $part) { if (strpos($part, '=') !== false) { [$attr, $val] = explode('=', $part); } else { $attr = $part; $val = true; }
$data[strtolower($attr)] = $val; }
return new static($name, $value, $data); }
/** * Construct a new Cookie instance. * * @param string $name The cookie's name * @param string $value The cookie's value * @param array<string, mixed> $options The cookie's options * * @throws CookieException */ final public function __construct(string $name, string $value = '', array $options = []) { $options += self::$defaults;
$options['expires'] = static::convertExpiresTimestamp($options['expires']);
// If both `Expires` and `Max-Age` are set, `Max-Age` has precedence. if (isset($options['max-age']) && is_numeric($options['max-age'])) { $options['expires'] = time() + (int) $options['max-age']; unset($options['max-age']); }
// to preserve backward compatibility with array-based cookies in previous CI versions $prefix = $options['prefix'] ?: self::$defaults['prefix']; $path = $options['path'] ?: self::$defaults['path']; $domain = $options['domain'] ?: self::$defaults['domain'];
// empty string SameSite should use the default for browsers $samesite = $options['samesite'] ?: self::$defaults['samesite'];
$raw = $options['raw']; $secure = $options['secure']; $httponly = $options['httponly'];
$this->validateName($name, $raw); $this->validatePrefix($prefix, $secure, $path, $domain); $this->validateSameSite($samesite, $secure);
$this->prefix = $prefix; $this->name = $name; $this->value = $value; $this->expires = static::convertExpiresTimestamp($options['expires']); $this->path = $path; $this->domain = $domain; $this->secure = $secure; $this->httponly = $httponly; $this->samesite = ucfirst(strtolower($samesite)); $this->raw = $raw; }
//========================================================================= // GETTERS //=========================================================================
/** * {@inheritDoc} */ public function getId(): string { return implode(';', [$this->getPrefixedName(), $this->getPath(), $this->getDomain()]); }
/** * {@inheritDoc} */ public function getPrefix(): string { return $this->prefix; }
/** * {@inheritDoc} */ public function getName(): string { return $this->name; }
/** * {@inheritDoc} */ public function getPrefixedName(): string { $name = $this->getPrefix();
if ($this->isRaw()) { $name .= $this->getName(); } else { $search = str_split(self::$reservedCharsList); $replace = array_map('rawurlencode', $search);
$name .= str_replace($search, $replace, $this->getName()); }
return $name; }
/** * {@inheritDoc} */ public function getValue(): string { return $this->value; }
/** * {@inheritDoc} */ public function getExpiresTimestamp(): int { return $this->expires; }
/** * {@inheritDoc} */ public function getExpiresString(): string { return gmdate(self::EXPIRES_FORMAT, $this->expires); }
/** * {@inheritDoc} */ public function isExpired(): bool { return $this->expires === 0 || $this->expires < time(); }
/** * {@inheritDoc} */ public function getMaxAge(): int { $maxAge = $this->expires - time();
return $maxAge >= 0 ? $maxAge : 0; }
/** * {@inheritDoc} */ public function getPath(): string { return $this->path; }
/** * {@inheritDoc} */ public function getDomain(): string { return $this->domain; }
/** * {@inheritDoc} */ public function isSecure(): bool { return $this->secure; }
/** * {@inheritDoc} */ public function isHTTPOnly(): bool { return $this->httponly; }
/** * {@inheritDoc} */ public function getSameSite(): string { return $this->samesite; }
/** * {@inheritDoc} */ public function isRaw(): bool { return $this->raw; }
/** * {@inheritDoc} */ public function getOptions(): array { // This is the order of options in `setcookie`. DO NOT CHANGE. return [ 'expires' => $this->expires, 'path' => $this->path, 'domain' => $this->domain, 'secure' => $this->secure, 'httponly' => $this->httponly, 'samesite' => $this->samesite ?: ucfirst(self::SAMESITE_LAX), ]; }
//========================================================================= // CLONING //=========================================================================
/** * {@inheritDoc} */ public function withPrefix(string $prefix = '') { $this->validatePrefix($prefix, $this->secure, $this->path, $this->domain);
$cookie = clone $this;
$cookie->prefix = $prefix;
return $cookie; }
/** * {@inheritDoc} */ public function withName(string $name) { $this->validateName($name, $this->raw);
$cookie = clone $this;
$cookie->name = $name;
return $cookie; }
/** * {@inheritDoc} */ public function withValue(string $value) { $cookie = clone $this;
$cookie->value = $value;
return $cookie; }
/** * {@inheritDoc} */ public function withExpires($expires) { $cookie = clone $this;
$cookie->expires = static::convertExpiresTimestamp($expires);
return $cookie; }
/** * {@inheritDoc} */ public function withExpired() { $cookie = clone $this;
$cookie->expires = 0;
return $cookie; }
/** * {@inheritDoc} */ public function withNeverExpiring() { $cookie = clone $this;
$cookie->expires = time() + 5 * YEAR;
return $cookie; }
/** * {@inheritDoc} */ public function withPath(?string $path) { $path = $path ?: self::$defaults['path']; $this->validatePrefix($this->prefix, $this->secure, $path, $this->domain);
$cookie = clone $this;
$cookie->path = $path;
return $cookie; }
/** * {@inheritDoc} */ public function withDomain(?string $domain) { $domain ??= self::$defaults['domain']; $this->validatePrefix($this->prefix, $this->secure, $this->path, $domain);
$cookie = clone $this;
$cookie->domain = $domain;
return $cookie; }
/** * {@inheritDoc} */ public function withSecure(bool $secure = true) { $this->validatePrefix($this->prefix, $secure, $this->path, $this->domain); $this->validateSameSite($this->samesite, $secure);
$cookie = clone $this;
$cookie->secure = $secure;
return $cookie; }
/** * {@inheritDoc} */ public function withHTTPOnly(bool $httponly = true) { $cookie = clone $this;
$cookie->httponly = $httponly;
return $cookie; }
/** * {@inheritDoc} */ public function withSameSite(string $samesite) { $this->validateSameSite($samesite, $this->secure);
$cookie = clone $this;
$cookie->samesite = ucfirst(strtolower($samesite));
return $cookie; }
/** * {@inheritDoc} */ public function withRaw(bool $raw = true) { $this->validateName($this->name, $raw);
$cookie = clone $this;
$cookie->raw = $raw;
return $cookie; }
//========================================================================= // ARRAY ACCESS FOR BC //=========================================================================
/** * Whether an offset exists. * * @param mixed $offset */ public function offsetExists($offset): bool { return $offset === 'expire' ? true : property_exists($this, $offset); }
/** * Offset to retrieve. * * @param mixed $offset * * @throws InvalidArgumentException * * @return mixed */ #[ReturnTypeWillChange] public function offsetGet($offset) { if (! $this->offsetExists($offset)) { throw new InvalidArgumentException(sprintf('Undefined offset "%s".', $offset)); }
return $offset === 'expire' ? $this->expires : $this->{$offset}; }
/** * Offset to set. * * @param mixed $offset * @param mixed $value * * @throws LogicException */ public function offsetSet($offset, $value): void { throw new LogicException(sprintf('Cannot set values of properties of %s as it is immutable.', static::class)); }
/** * Offset to unset. * * @param mixed $offset * * @throws LogicException */ public function offsetUnset($offset): void { throw new LogicException(sprintf('Cannot unset values of properties of %s as it is immutable.', static::class)); }
//========================================================================= // CONVERTERS //=========================================================================
/** * {@inheritDoc} */ public function toHeaderString(): string { return $this->__toString(); }
/** * {@inheritDoc} */ public function __toString() { $cookieHeader = [];
if ($this->getValue() === '') { $cookieHeader[] = $this->getPrefixedName() . '=deleted'; $cookieHeader[] = 'Expires=' . gmdate(self::EXPIRES_FORMAT, 0); $cookieHeader[] = 'Max-Age=0'; } else { $value = $this->isRaw() ? $this->getValue() : rawurlencode($this->getValue());
$cookieHeader[] = sprintf('%s=%s', $this->getPrefixedName(), $value);
if ($this->getExpiresTimestamp() !== 0) { $cookieHeader[] = 'Expires=' . $this->getExpiresString(); $cookieHeader[] = 'Max-Age=' . $this->getMaxAge(); } }
if ($this->getPath() !== '') { $cookieHeader[] = 'Path=' . $this->getPath(); }
if ($this->getDomain() !== '') { $cookieHeader[] = 'Domain=' . $this->getDomain(); }
if ($this->isSecure()) { $cookieHeader[] = 'Secure'; }
if ($this->isHTTPOnly()) { $cookieHeader[] = 'HttpOnly'; }
$samesite = $this->getSameSite();
if ($samesite === '') { // modern browsers warn in console logs that an empty SameSite attribute // will be given the `Lax` value $samesite = self::SAMESITE_LAX; }
$cookieHeader[] = 'SameSite=' . ucfirst(strtolower($samesite));
return implode('; ', $cookieHeader); }
/** * {@inheritDoc} */ public function toArray(): array { return [ 'name' => $this->name, 'value' => $this->value, 'prefix' => $this->prefix, 'raw' => $this->raw, ] + $this->getOptions(); }
/** * Converts expires time to Unix format. * * @param DateTimeInterface|int|string $expires */ protected static function convertExpiresTimestamp($expires = 0): int { if ($expires instanceof DateTimeInterface) { $expires = $expires->format('U'); }
if (! is_string($expires) && ! is_int($expires)) { throw CookieException::forInvalidExpiresTime(gettype($expires)); }
if (! is_numeric($expires)) { $expires = strtotime($expires);
if ($expires === false) { throw CookieException::forInvalidExpiresValue(); } }
return $expires > 0 ? (int) $expires : 0; }
//========================================================================= // VALIDATION //=========================================================================
/** * Validates the cookie name per RFC 2616. * * If `$raw` is true, names should not contain invalid characters * as `setrawcookie()` will reject this. * * @throws CookieException */ protected function validateName(string $name, bool $raw): void { if ($raw && strpbrk($name, self::$reservedCharsList) !== false) { throw CookieException::forInvalidCookieName($name); }
if ($name === '') { throw CookieException::forEmptyCookieName(); } }
/** * Validates the special prefixes if some attribute requirements are met. * * @throws CookieException */ protected function validatePrefix(string $prefix, bool $secure, string $path, string $domain): void { if (strpos($prefix, '__Secure-') === 0 && ! $secure) { throw CookieException::forInvalidSecurePrefix(); }
if (strpos($prefix, '__Host-') === 0 && (! $secure || $domain !== '' || $path !== '/')) { throw CookieException::forInvalidHostPrefix(); } }
/** * Validates the `SameSite` to be within the allowed types. * * @throws CookieException * * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite */ protected function validateSameSite(string $samesite, bool $secure): void { if ($samesite === '') { $samesite = self::$defaults['samesite']; }
if ($samesite === '') { $samesite = self::SAMESITE_LAX; }
if (! in_array(strtolower($samesite), self::ALLOWED_SAMESITE_VALUES, true)) { throw CookieException::forInvalidSameSite($samesite); }
if (strtolower($samesite) === self::SAMESITE_NONE && ! $secure) { throw CookieException::forInvalidSameSiteNone(); } } }
|