This commit is contained in:
Markus
2022-04-28 09:40:10 +02:00
commit 795794f992
9586 changed files with 1146991 additions and 0 deletions

View 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
View 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();
}
}
}

View 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;
}

View 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);
}
}

View 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));
}
}