Initial
This commit is contained in:
107
system/Cookie/CloneableCookieInterface.php
Normal file
107
system/Cookie/CloneableCookieInterface.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?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 DateTimeInterface;
|
||||
|
||||
/**
|
||||
* Interface for a fresh Cookie instance with selected attribute(s)
|
||||
* only changed from the original instance.
|
||||
*/
|
||||
interface CloneableCookieInterface extends CookieInterface
|
||||
{
|
||||
/**
|
||||
* Creates a new Cookie with a new cookie prefix.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function withPrefix(string $prefix = '');
|
||||
|
||||
/**
|
||||
* Creates a new Cookie with a new name.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function withName(string $name);
|
||||
|
||||
/**
|
||||
* Creates a new Cookie with new value.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function withValue(string $value);
|
||||
|
||||
/**
|
||||
* Creates a new Cookie with a new cookie expires time.
|
||||
*
|
||||
* @param DateTimeInterface|int|string $expires
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function withExpires($expires);
|
||||
|
||||
/**
|
||||
* Creates a new Cookie that will expire the cookie from the browser.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function withExpired();
|
||||
|
||||
/**
|
||||
* Creates a new Cookie that will virtually never expire from the browser.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function withNeverExpiring();
|
||||
|
||||
/**
|
||||
* Creates a new Cookie with a new path on the server the cookie is available.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function withPath(?string $path);
|
||||
|
||||
/**
|
||||
* Creates a new Cookie with a new domain the cookie is available.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function withDomain(?string $domain);
|
||||
|
||||
/**
|
||||
* Creates a new Cookie with a new "Secure" attribute.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function withSecure(bool $secure = true);
|
||||
|
||||
/**
|
||||
* Creates a new Cookie with a new "HttpOnly" attribute
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function withHTTPOnly(bool $httponly = true);
|
||||
|
||||
/**
|
||||
* Creates a new Cookie with a new "SameSite" attribute.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function withSameSite(string $samesite);
|
||||
|
||||
/**
|
||||
* Creates a new Cookie with URL encoding option updated.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function withRaw(bool $raw = true);
|
||||
}
|
||||
783
system/Cookie/Cookie.php
Normal file
783
system/Cookie/Cookie.php
Normal file
@@ -0,0 +1,783 @@
|
||||
<?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 $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.
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @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 $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 = $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();
|
||||
}
|
||||
}
|
||||
}
|
||||
168
system/Cookie/CookieInterface.php
Normal file
168
system/Cookie/CookieInterface.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Interface for a value object representation of an HTTP cookie.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
|
||||
*/
|
||||
interface CookieInterface
|
||||
{
|
||||
/**
|
||||
* Cookies will be sent in all contexts, i.e in responses to both
|
||||
* first-party and cross-origin requests. If `SameSite=None` is set,
|
||||
* the cookie `Secure` attribute must also be set (or the cookie will be blocked).
|
||||
*/
|
||||
public const SAMESITE_NONE = 'none';
|
||||
|
||||
/**
|
||||
* Cookies are not sent on normal cross-site subrequests (for example to
|
||||
* load images or frames into a third party site), but are sent when a
|
||||
* user is navigating to the origin site (i.e. when following a link).
|
||||
*/
|
||||
public const SAMESITE_LAX = 'lax';
|
||||
|
||||
/**
|
||||
* Cookies will only be sent in a first-party context and not be sent
|
||||
* along with requests initiated by third party websites.
|
||||
*/
|
||||
public const SAMESITE_STRICT = 'strict';
|
||||
|
||||
/**
|
||||
* RFC 6265 allowed values for the "SameSite" attribute.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
|
||||
*/
|
||||
public const ALLOWED_SAMESITE_VALUES = [
|
||||
self::SAMESITE_NONE,
|
||||
self::SAMESITE_LAX,
|
||||
self::SAMESITE_STRICT,
|
||||
];
|
||||
|
||||
/**
|
||||
* Expires date format.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Date
|
||||
* @see https://tools.ietf.org/html/rfc7231#section-7.1.1.2
|
||||
*/
|
||||
public const EXPIRES_FORMAT = 'D, d-M-Y H:i:s T';
|
||||
|
||||
/**
|
||||
* Returns a unique identifier for the cookie consisting
|
||||
* of its prefixed name, path, and domain.
|
||||
*/
|
||||
public function getId(): string;
|
||||
|
||||
/**
|
||||
* Gets the cookie prefix.
|
||||
*/
|
||||
public function getPrefix(): string;
|
||||
|
||||
/**
|
||||
* Gets the cookie name.
|
||||
*/
|
||||
public function getName(): string;
|
||||
|
||||
/**
|
||||
* Gets the cookie name prepended with the prefix, if any.
|
||||
*/
|
||||
public function getPrefixedName(): string;
|
||||
|
||||
/**
|
||||
* Gets the cookie value.
|
||||
*/
|
||||
public function getValue(): string;
|
||||
|
||||
/**
|
||||
* Gets the time in Unix timestamp the cookie expires.
|
||||
*/
|
||||
public function getExpiresTimestamp(): int;
|
||||
|
||||
/**
|
||||
* Gets the formatted expires time.
|
||||
*/
|
||||
public function getExpiresString(): string;
|
||||
|
||||
/**
|
||||
* Checks if the cookie is expired.
|
||||
*/
|
||||
public function isExpired(): bool;
|
||||
|
||||
/**
|
||||
* Gets the "Max-Age" cookie attribute.
|
||||
*/
|
||||
public function getMaxAge(): int;
|
||||
|
||||
/**
|
||||
* Gets the "Path" cookie attribute.
|
||||
*/
|
||||
public function getPath(): string;
|
||||
|
||||
/**
|
||||
* Gets the "Domain" cookie attribute.
|
||||
*/
|
||||
public function getDomain(): string;
|
||||
|
||||
/**
|
||||
* Gets the "Secure" cookie attribute.
|
||||
*
|
||||
* Checks if the cookie is only sent to the server when a request is made
|
||||
* with the `https:` scheme (except on `localhost`), and therefore is more
|
||||
* resistent to man-in-the-middle attacks.
|
||||
*/
|
||||
public function isSecure(): bool;
|
||||
|
||||
/**
|
||||
* Gets the "HttpOnly" cookie attribute.
|
||||
*
|
||||
* Checks if JavaScript is forbidden from accessing the cookie.
|
||||
*/
|
||||
public function isHTTPOnly(): bool;
|
||||
|
||||
/**
|
||||
* Gets the "SameSite" cookie attribute.
|
||||
*/
|
||||
public function getSameSite(): string;
|
||||
|
||||
/**
|
||||
* Checks if the cookie should be sent with no URL encoding.
|
||||
*/
|
||||
public function isRaw(): bool;
|
||||
|
||||
/**
|
||||
* Gets the options that are passable to the `setcookie` variant
|
||||
* available on PHP 7.3+
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getOptions(): array;
|
||||
|
||||
/**
|
||||
* Returns the Cookie as a header value.
|
||||
*/
|
||||
public function toHeaderString(): string;
|
||||
|
||||
/**
|
||||
* Returns the string representation of the Cookie object.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString();
|
||||
|
||||
/**
|
||||
* Returns the array representation of the Cookie object.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array;
|
||||
}
|
||||
256
system/Cookie/CookieStore.php
Normal file
256
system/Cookie/CookieStore.php
Normal file
@@ -0,0 +1,256 @@
|
||||
<?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 ArrayIterator;
|
||||
use CodeIgniter\Cookie\Exceptions\CookieException;
|
||||
use Countable;
|
||||
use IteratorAggregate;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* The CookieStore object represents an immutable collection of `Cookie` value objects.
|
||||
*
|
||||
* @implements IteratorAggregate<string, Cookie>
|
||||
*/
|
||||
class CookieStore implements Countable, IteratorAggregate
|
||||
{
|
||||
/**
|
||||
* The cookie collection.
|
||||
*
|
||||
* @var array<string, Cookie>
|
||||
*/
|
||||
protected $cookies = [];
|
||||
|
||||
/**
|
||||
* Creates a CookieStore from an array of `Set-Cookie` headers.
|
||||
*
|
||||
* @param string[] $headers
|
||||
*
|
||||
* @throws CookieException
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function fromCookieHeaders(array $headers, bool $raw = false)
|
||||
{
|
||||
/**
|
||||
* @var Cookie[] $cookies
|
||||
*/
|
||||
$cookies = array_filter(array_map(static function (string $header) use ($raw) {
|
||||
try {
|
||||
return Cookie::fromHeaderString($header, $raw);
|
||||
} catch (CookieException $e) {
|
||||
log_message('error', $e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
}, $headers));
|
||||
|
||||
return new static($cookies);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Cookie[] $cookies
|
||||
*
|
||||
* @throws CookieException
|
||||
*/
|
||||
final public function __construct(array $cookies)
|
||||
{
|
||||
$this->validateCookies($cookies);
|
||||
|
||||
foreach ($cookies as $cookie) {
|
||||
$this->cookies[$cookie->getId()] = $cookie;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a `Cookie` object identified by name and
|
||||
* prefix is present in the collection.
|
||||
*/
|
||||
public function has(string $name, string $prefix = '', ?string $value = null): bool
|
||||
{
|
||||
$name = $prefix . $name;
|
||||
|
||||
foreach ($this->cookies as $cookie) {
|
||||
if ($cookie->getPrefixedName() !== $name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
return true; // for BC
|
||||
}
|
||||
|
||||
return $cookie->getValue() === $value;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an instance of `Cookie` identified by a name and prefix.
|
||||
* This throws an exception if not found.
|
||||
*
|
||||
* @throws CookieException
|
||||
*/
|
||||
public function get(string $name, string $prefix = ''): Cookie
|
||||
{
|
||||
$name = $prefix . $name;
|
||||
|
||||
foreach ($this->cookies as $cookie) {
|
||||
if ($cookie->getPrefixedName() === $name) {
|
||||
return $cookie;
|
||||
}
|
||||
}
|
||||
|
||||
throw CookieException::forUnknownCookieInstance([$name, $prefix]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a new cookie and return a new collection. The original collection
|
||||
* is left unchanged.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function put(Cookie $cookie)
|
||||
{
|
||||
$store = clone $this;
|
||||
|
||||
$store->cookies[$cookie->getId()] = $cookie;
|
||||
|
||||
return $store;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a cookie from a collection and returns an updated collection.
|
||||
* The original collection is left unchanged.
|
||||
*
|
||||
* Removing a cookie from the store **DOES NOT** delete it from the browser.
|
||||
* If you intend to delete a cookie *from the browser*, you must put an empty
|
||||
* value cookie with the same name to the store.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function remove(string $name, string $prefix = '')
|
||||
{
|
||||
$default = Cookie::setDefaults();
|
||||
|
||||
$id = implode(';', [$prefix . $name, $default['path'], $default['domain']]);
|
||||
|
||||
$store = clone $this;
|
||||
|
||||
foreach (array_keys($store->cookies) as $index) {
|
||||
if ($index === $id) {
|
||||
unset($store->cookies[$index]);
|
||||
}
|
||||
}
|
||||
|
||||
return $store;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches all cookies in store.
|
||||
*
|
||||
* @deprecated Response should dispatch cookies.
|
||||
*/
|
||||
public function dispatch(): void
|
||||
{
|
||||
foreach ($this->cookies as $cookie) {
|
||||
$name = $cookie->getPrefixedName();
|
||||
$value = $cookie->getValue();
|
||||
$options = $cookie->getOptions();
|
||||
|
||||
if ($cookie->isRaw()) {
|
||||
$this->setRawCookie($name, $value, $options);
|
||||
} else {
|
||||
$this->setCookie($name, $value, $options);
|
||||
}
|
||||
}
|
||||
|
||||
$this->clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all cookie instances in store.
|
||||
*
|
||||
* @return array<string, Cookie>
|
||||
*/
|
||||
public function display(): array
|
||||
{
|
||||
return $this->cookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the cookie collection.
|
||||
*/
|
||||
public function clear(): void
|
||||
{
|
||||
$this->cookies = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Cookie count in this collection.
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->cookies);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the iterator for the cookie collection.
|
||||
*
|
||||
* @return Traversable<string, Cookie>
|
||||
*/
|
||||
public function getIterator(): Traversable
|
||||
{
|
||||
return new ArrayIterator($this->cookies);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates all cookies passed to be instances of Cookie.
|
||||
*
|
||||
* @throws CookieException
|
||||
*/
|
||||
protected function validateCookies(array $cookies): void
|
||||
{
|
||||
foreach ($cookies as $index => $cookie) {
|
||||
$type = is_object($cookie) ? get_class($cookie) : gettype($cookie);
|
||||
|
||||
if (! $cookie instanceof Cookie) {
|
||||
throw CookieException::forInvalidCookieInstance([static::class, Cookie::class, $type, $index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracted call to `setrawcookie()` in order to run unit tests on it.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
protected function setRawCookie(string $name, string $value, array $options): void
|
||||
{
|
||||
setrawcookie($name, $value, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracted call to `setcookie()` in order to run unit tests on it.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
protected function setCookie(string $name, string $value, array $options): void
|
||||
{
|
||||
setcookie($name, $value, $options);
|
||||
}
|
||||
}
|
||||
127
system/Cookie/Exceptions/CookieException.php
Normal file
127
system/Cookie/Exceptions/CookieException.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?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\Exceptions;
|
||||
|
||||
use CodeIgniter\Exceptions\FrameworkException;
|
||||
|
||||
/**
|
||||
* CookieException is thrown for invalid cookies initialization and management.
|
||||
*/
|
||||
class CookieException extends FrameworkException
|
||||
{
|
||||
/**
|
||||
* Thrown for invalid type given for the "Expires" attribute.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function forInvalidExpiresTime(string $type)
|
||||
{
|
||||
return new static(lang('Cookie.invalidExpiresTime', [$type]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when the value provided for "Expires" is invalid.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function forInvalidExpiresValue()
|
||||
{
|
||||
return new static(lang('Cookie.invalidExpiresValue'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when the cookie name contains invalid characters per RFC 2616.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function forInvalidCookieName(string $name)
|
||||
{
|
||||
return new static(lang('Cookie.invalidCookieName', [$name]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when the cookie name is empty.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function forEmptyCookieName()
|
||||
{
|
||||
return new static(lang('Cookie.emptyCookieName'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when using the `__Secure-` prefix but the `Secure` attribute
|
||||
* is not set to true.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function forInvalidSecurePrefix()
|
||||
{
|
||||
return new static(lang('Cookie.invalidSecurePrefix'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when using the `__Host-` prefix but the `Secure` flag is not
|
||||
* set, the `Domain` is set, and the `Path` is not `/`.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function forInvalidHostPrefix()
|
||||
{
|
||||
return new static(lang('Cookie.invalidHostPrefix'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when the `SameSite` attribute given is not of the valid types.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function forInvalidSameSite(string $sameSite)
|
||||
{
|
||||
return new static(lang('Cookie.invalidSameSite', [$sameSite]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when the `SameSite` attribute is set to `None` but the `Secure`
|
||||
* attribute is not set.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function forInvalidSameSiteNone()
|
||||
{
|
||||
return new static(lang('Cookie.invalidSameSiteNone'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when the `CookieStore` class is filled with invalid Cookie objects.
|
||||
*
|
||||
* @param array<int|string> $data
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function forInvalidCookieInstance(array $data)
|
||||
{
|
||||
return new static(lang('Cookie.invalidCookieInstance', $data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when the queried Cookie object does not exist in the cookie collection.
|
||||
*
|
||||
* @param string[] $data
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function forUnknownCookieInstance(array $data)
|
||||
{
|
||||
return new static(lang('Cookie.unknownCookieInstance', $data));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user