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,522 @@
<?php
/*
* PHP-Cookie (https://github.com/delight-im/PHP-Cookie)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace WP_Ultimo\Dependencies\Delight\Cookie;
/**
* Modern cookie management for PHP
*
* Cookies are a mechanism for storing data in the client's web browser and identifying returning clients on subsequent visits
*
* All cookies that have successfully been set will automatically be included in the global `$_COOKIE` array with future requests
*
* You can set a new cookie using the static method `Cookie::setcookie(...)` which is compatible to PHP's built-in `setcookie(...)` function
*
* Alternatively, you can construct an instance of this class, set properties individually, and finally call `save()`
*
* Note that cookies must always be set before the HTTP headers are sent to the client, i.e. before the actual output starts
*/
final class Cookie
{
/** @var string name prefix indicating that the cookie must be from a secure origin (i.e. HTTPS) and the 'secure' attribute must be set */
const PREFIX_SECURE = '__Secure-';
/** @var string name prefix indicating that the 'domain' attribute must *not* be set, the 'path' attribute must be '/' and the effects of {@see PREFIX_SECURE} apply as well */
const PREFIX_HOST = '__Host-';
const HEADER_PREFIX = 'Set-Cookie: ';
const SAME_SITE_RESTRICTION_NONE = 'None';
const SAME_SITE_RESTRICTION_LAX = 'Lax';
const SAME_SITE_RESTRICTION_STRICT = 'Strict';
/** @var string the name of the cookie which is also the key for future accesses via `$_COOKIE[...]` */
private $name;
/** @var mixed|null the value of the cookie that will be stored on the client's machine */
private $value;
/** @var int the Unix timestamp indicating the time that the cookie will expire at, i.e. usually `time() + $seconds` */
private $expiryTime;
/** @var string the path on the server that the cookie will be valid for (including all sub-directories), e.g. an empty string for the current directory or `/` for the root directory */
private $path;
/** @var string|null the domain that the cookie will be valid for (including subdomains) or `null` for the current host (excluding subdomains) */
private $domain;
/** @var bool indicates that the cookie should be accessible through the HTTP protocol only and not through scripting languages */
private $httpOnly;
/** @var bool indicates that the cookie should be sent back by the client over secure HTTPS connections only */
private $secureOnly;
/** @var string|null indicates that the cookie should not be sent along with cross-site requests (either `null`, `None`, `Lax` or `Strict`) */
private $sameSiteRestriction;
/**
* Prepares a new cookie
*
* @param string $name the name of the cookie which is also the key for future accesses via `$_COOKIE[...]`
*/
public function __construct($name)
{
$this->name = $name;
$this->value = null;
$this->expiryTime = 0;
$this->path = '/';
$this->domain = null;
$this->httpOnly = \true;
$this->secureOnly = \false;
$this->sameSiteRestriction = self::SAME_SITE_RESTRICTION_LAX;
}
/**
* Returns the name of the cookie
*
* @return string the name of the cookie which is also the key for future accesses via `$_COOKIE[...]`
*/
public function getName()
{
return $this->name;
}
/**
* Returns the value of the cookie
*
* @return mixed|null the value of the cookie that will be stored on the client's machine
*/
public function getValue()
{
return $this->value;
}
/**
* Sets the value for the cookie
*
* @param mixed|null $value the value of the cookie that will be stored on the client's machine
* @return static this instance for chaining
*/
public function setValue($value)
{
$this->value = $value;
return $this;
}
/**
* Returns the expiry time of the cookie
*
* @return int the Unix timestamp indicating the time that the cookie will expire at, i.e. usually `time() + $seconds`
*/
public function getExpiryTime()
{
return $this->expiryTime;
}
/**
* Sets the expiry time for the cookie
*
* @param int $expiryTime the Unix timestamp indicating the time that the cookie will expire at, i.e. usually `time() + $seconds`
* @return static this instance for chaining
*/
public function setExpiryTime($expiryTime)
{
$this->expiryTime = $expiryTime;
return $this;
}
/**
* Returns the maximum age of the cookie (i.e. the remaining lifetime)
*
* @return int the maximum age of the cookie in seconds
*/
public function getMaxAge()
{
return $this->expiryTime - \time();
}
/**
* Sets the expiry time for the cookie based on the specified maximum age (i.e. the remaining lifetime)
*
* @param int $maxAge the maximum age for the cookie in seconds
* @return static this instance for chaining
*/
public function setMaxAge($maxAge)
{
$this->expiryTime = \time() + $maxAge;
return $this;
}
/**
* Returns the path of the cookie
*
* @return string the path on the server that the cookie will be valid for (including all sub-directories), e.g. an empty string for the current directory or `/` for the root directory
*/
public function getPath()
{
return $this->path;
}
/**
* Sets the path for the cookie
*
* @param string $path the path on the server that the cookie will be valid for (including all sub-directories), e.g. an empty string for the current directory or `/` for the root directory
* @return static this instance for chaining
*/
public function setPath($path)
{
$this->path = $path;
return $this;
}
/**
* Returns the domain of the cookie
*
* @return string|null the domain that the cookie will be valid for (including subdomains) or `null` for the current host (excluding subdomains)
*/
public function getDomain()
{
return $this->domain;
}
/**
* Sets the domain for the cookie
*
* @param string|null $domain the domain that the cookie will be valid for (including subdomains) or `null` for the current host (excluding subdomains)
* @return static this instance for chaining
*/
public function setDomain($domain = null)
{
$this->domain = self::normalizeDomain($domain);
return $this;
}
/**
* Returns whether the cookie should be accessible through HTTP only
*
* @return bool whether the cookie should be accessible through the HTTP protocol only and not through scripting languages
*/
public function isHttpOnly()
{
return $this->httpOnly;
}
/**
* Sets whether the cookie should be accessible through HTTP only
*
* @param bool $httpOnly indicates that the cookie should be accessible through the HTTP protocol only and not through scripting languages
* @return static this instance for chaining
*/
public function setHttpOnly($httpOnly)
{
$this->httpOnly = $httpOnly;
return $this;
}
/**
* Returns whether the cookie should be sent over HTTPS only
*
* @return bool whether the cookie should be sent back by the client over secure HTTPS connections only
*/
public function isSecureOnly()
{
return $this->secureOnly;
}
/**
* Sets whether the cookie should be sent over HTTPS only
*
* @param bool $secureOnly indicates that the cookie should be sent back by the client over secure HTTPS connections only
* @return static this instance for chaining
*/
public function setSecureOnly($secureOnly)
{
$this->secureOnly = $secureOnly;
return $this;
}
/**
* Returns the same-site restriction of the cookie
*
* @return string|null whether the cookie should not be sent along with cross-site requests (either `null`, `None`, `Lax` or `Strict`)
*/
public function getSameSiteRestriction()
{
return $this->sameSiteRestriction;
}
/**
* Sets the same-site restriction for the cookie
*
* @param string|null $sameSiteRestriction indicates that the cookie should not be sent along with cross-site requests (either `null`, `None`, `Lax` or `Strict`)
* @return static this instance for chaining
*/
public function setSameSiteRestriction($sameSiteRestriction)
{
$this->sameSiteRestriction = $sameSiteRestriction;
return $this;
}
/**
* Saves the cookie
*
* @return bool whether the cookie header has successfully been sent (and will *probably* cause the client to set the cookie)
*/
public function save()
{
return self::addHttpHeader((string) $this);
}
/**
* Saves the cookie and immediately creates the corresponding variable in the superglobal `$_COOKIE` array
*
* The variable would otherwise only be available starting from the next HTTP request
*
* @return bool whether the cookie header has successfully been sent (and will *probably* cause the client to set the cookie)
*/
public function saveAndSet()
{
$_COOKIE[$this->name] = $this->value;
return $this->save();
}
/**
* Deletes the cookie
*
* @return bool whether the cookie header has successfully been sent (and will *probably* cause the client to delete the cookie)
*/
public function delete()
{
// create a temporary copy of this cookie so that it isn't corrupted
$copiedCookie = clone $this;
// set the copied cookie's value to an empty string which internally sets the required options for a deletion
$copiedCookie->setValue('');
// save the copied "deletion" cookie
return $copiedCookie->save();
}
/**
* Deletes the cookie and immediately removes the corresponding variable from the superglobal `$_COOKIE` array
*
* The variable would otherwise only be deleted at the start of the next HTTP request
*
* @return bool whether the cookie header has successfully been sent (and will *probably* cause the client to delete the cookie)
*/
public function deleteAndUnset()
{
unset($_COOKIE[$this->name]);
return $this->delete();
}
public function __toString()
{
return self::buildCookieHeader($this->name, $this->value, $this->expiryTime, $this->path, $this->domain, $this->secureOnly, $this->httpOnly, $this->sameSiteRestriction);
}
/**
* Sets a new cookie in a way compatible to PHP's `setcookie(...)` function
*
* @param string $name the name of the cookie which is also the key for future accesses via `$_COOKIE[...]`
* @param mixed|null $value the value of the cookie that will be stored on the client's machine
* @param int $expiryTime the Unix timestamp indicating the time that the cookie will expire at, i.e. usually `time() + $seconds`
* @param string|null $path the path on the server that the cookie will be valid for (including all sub-directories), e.g. an empty string for the current directory or `/` for the root directory
* @param string|null $domain the domain that the cookie will be valid for (including subdomains) or `null` for the current host (excluding subdomains)
* @param bool $secureOnly indicates that the cookie should be sent back by the client over secure HTTPS connections only
* @param bool $httpOnly indicates that the cookie should be accessible through the HTTP protocol only and not through scripting languages
* @param string|null $sameSiteRestriction indicates that the cookie should not be sent along with cross-site requests (either `null`, `None`, `Lax` or `Strict`)
* @return bool whether the cookie header has successfully been sent (and will *probably* cause the client to set the cookie)
*/
public static function setcookie($name, $value = null, $expiryTime = 0, $path = null, $domain = null, $secureOnly = \false, $httpOnly = \false, $sameSiteRestriction = null)
{
return self::addHttpHeader(self::buildCookieHeader($name, $value, $expiryTime, $path, $domain, $secureOnly, $httpOnly, $sameSiteRestriction));
}
/**
* Builds the HTTP header that can be used to set a cookie with the specified options
*
* @param string $name the name of the cookie which is also the key for future accesses via `$_COOKIE[...]`
* @param mixed|null $value the value of the cookie that will be stored on the client's machine
* @param int $expiryTime the Unix timestamp indicating the time that the cookie will expire at, i.e. usually `time() + $seconds`
* @param string|null $path the path on the server that the cookie will be valid for (including all sub-directories), e.g. an empty string for the current directory or `/` for the root directory
* @param string|null $domain the domain that the cookie will be valid for (including subdomains) or `null` for the current host (excluding subdomains)
* @param bool $secureOnly indicates that the cookie should be sent back by the client over secure HTTPS connections only
* @param bool $httpOnly indicates that the cookie should be accessible through the HTTP protocol only and not through scripting languages
* @param string|null $sameSiteRestriction indicates that the cookie should not be sent along with cross-site requests (either `null`, `None`, `Lax` or `Strict`)
* @return string the HTTP header
*/
public static function buildCookieHeader($name, $value = null, $expiryTime = 0, $path = null, $domain = null, $secureOnly = \false, $httpOnly = \false, $sameSiteRestriction = null)
{
if (self::isNameValid($name)) {
$name = (string) $name;
} else {
return null;
}
if (self::isExpiryTimeValid($expiryTime)) {
$expiryTime = (int) $expiryTime;
} else {
return null;
}
$forceShowExpiry = \false;
if (\is_null($value) || $value === \false || $value === '') {
$value = 'deleted';
$expiryTime = 0;
$forceShowExpiry = \true;
}
$maxAgeStr = self::formatMaxAge($expiryTime, $forceShowExpiry);
$expiryTimeStr = self::formatExpiryTime($expiryTime, $forceShowExpiry);
$headerStr = self::HEADER_PREFIX . $name . '=' . \urlencode($value);
if (!\is_null($expiryTimeStr)) {
$headerStr .= '; expires=' . $expiryTimeStr;
}
// The `Max-Age` property is supported on PHP 5.5+ only (https://bugs.php.net/bug.php?id=23955).
if (\PHP_VERSION_ID >= 50500) {
if (!\is_null($maxAgeStr)) {
$headerStr .= '; Max-Age=' . $maxAgeStr;
}
}
if (!empty($path) || $path === 0) {
$headerStr .= '; path=' . $path;
}
if (!empty($domain) || $domain === 0) {
$headerStr .= '; domain=' . $domain;
}
if ($secureOnly) {
$headerStr .= '; secure';
}
if ($httpOnly) {
$headerStr .= '; httponly';
}
if ($sameSiteRestriction === self::SAME_SITE_RESTRICTION_NONE) {
// if the 'secure' attribute is missing
if (!$secureOnly) {
\trigger_error('When the \'SameSite\' attribute is set to \'None\', the \'secure\' attribute should be set as well', \E_USER_WARNING);
}
$headerStr .= '; SameSite=None';
} elseif ($sameSiteRestriction === self::SAME_SITE_RESTRICTION_LAX) {
$headerStr .= '; SameSite=Lax';
} elseif ($sameSiteRestriction === self::SAME_SITE_RESTRICTION_STRICT) {
$headerStr .= '; SameSite=Strict';
}
return $headerStr;
}
/**
* Parses the given cookie header and returns an equivalent cookie instance
*
* @param string $cookieHeader the cookie header to parse
* @return \Delight\Cookie\Cookie|null the cookie instance or `null`
*/
public static function parse($cookieHeader)
{
if (empty($cookieHeader)) {
return null;
}
if (\preg_match('/^' . self::HEADER_PREFIX . '(.*?)=(.*?)(?:; (.*?))?$/i', $cookieHeader, $matches)) {
$cookie = new self($matches[1]);
$cookie->setPath(null);
$cookie->setHttpOnly(\false);
$cookie->setValue(\urldecode($matches[2]));
$cookie->setSameSiteRestriction(null);
if (\count($matches) >= 4) {
$attributes = \explode('; ', $matches[3]);
foreach ($attributes as $attribute) {
if (\strcasecmp($attribute, 'HttpOnly') === 0) {
$cookie->setHttpOnly(\true);
} elseif (\strcasecmp($attribute, 'Secure') === 0) {
$cookie->setSecureOnly(\true);
} elseif (\stripos($attribute, 'Expires=') === 0) {
$cookie->setExpiryTime((int) \strtotime(\substr($attribute, 8)));
} elseif (\stripos($attribute, 'Domain=') === 0) {
$cookie->setDomain(\substr($attribute, 7));
} elseif (\stripos($attribute, 'Path=') === 0) {
$cookie->setPath(\substr($attribute, 5));
} elseif (\stripos($attribute, 'SameSite=') === 0) {
$cookie->setSameSiteRestriction(\substr($attribute, 9));
}
}
}
return $cookie;
} else {
return null;
}
}
/**
* Checks whether a cookie with the specified name exists
*
* @param string $name the name of the cookie to check
* @return bool whether there is a cookie with the specified name
*/
public static function exists($name)
{
return isset($_COOKIE[$name]);
}
/**
* Returns the value from the requested cookie or, if not found, the specified default value
*
* @param string $name the name of the cookie to retrieve the value from
* @param mixed $defaultValue the default value to return if the requested cookie cannot be found
* @return mixed the value from the requested cookie or the default value
*/
public static function get($name, $defaultValue = null)
{
if (isset($_COOKIE[$name])) {
return $_COOKIE[$name];
} else {
return $defaultValue;
}
}
private static function isNameValid($name)
{
$name = (string) $name;
// The name of a cookie must not be empty on PHP 7+ (https://bugs.php.net/bug.php?id=69523).
if ($name !== '' || \PHP_VERSION_ID < 70000) {
if (!\preg_match('/[=,; \\t\\r\\n\\013\\014]/', $name)) {
return \true;
}
}
return \false;
}
private static function isExpiryTimeValid($expiryTime)
{
return \is_numeric($expiryTime) || \is_null($expiryTime) || \is_bool($expiryTime);
}
private static function calculateMaxAge($expiryTime)
{
if ($expiryTime === 0) {
return 0;
} else {
$maxAge = $expiryTime - \time();
// The value of the `Max-Age` property must not be negative on PHP 7.0.19+ (< 7.1) and
// PHP 7.1.5+ (https://bugs.php.net/bug.php?id=72071).
if (\PHP_VERSION_ID >= 70019 && \PHP_VERSION_ID < 70100 || \PHP_VERSION_ID >= 70105) {
if ($maxAge < 0) {
$maxAge = 0;
}
}
return $maxAge;
}
}
private static function formatExpiryTime($expiryTime, $forceShow = \false)
{
if ($expiryTime > 0 || $forceShow) {
if ($forceShow) {
$expiryTime = 1;
}
return \gmdate('D, d-M-Y H:i:s T', $expiryTime);
} else {
return null;
}
}
private static function formatMaxAge($expiryTime, $forceShow = \false)
{
if ($expiryTime > 0 || $forceShow) {
return (string) self::calculateMaxAge($expiryTime);
} else {
return null;
}
}
private static function normalizeDomain($domain = null)
{
// make sure that the domain is a string
$domain = (string) $domain;
// if the cookie should be valid for the current host only
if ($domain === '') {
// no need for further normalization
return null;
}
// if the provided domain is actually an IP address
if (\filter_var($domain, \FILTER_VALIDATE_IP) !== \false) {
// let the cookie be valid for the current host
return null;
}
// for local hostnames (which either have no dot at all or a leading dot only)
if (\strpos($domain, '.') === \false || \strrpos($domain, '.') === 0) {
// let the cookie be valid for the current host while ensuring maximum compatibility
return null;
}
// unless the domain already starts with a dot
if ($domain[0] !== '.') {
// prepend a dot for maximum compatibility (e.g. with RFC 2109)
$domain = '.' . $domain;
}
// return the normalized domain
return $domain;
}
private static function addHttpHeader($header)
{
if (!\headers_sent()) {
if (!empty($header)) {
\header($header, \false);
return \true;
}
}
return \false;
}
}

View File

@ -0,0 +1,158 @@
<?php
/*
* PHP-Cookie (https://github.com/delight-im/PHP-Cookie)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace WP_Ultimo\Dependencies\Delight\Cookie;
use WP_Ultimo\Dependencies\Delight\Http\ResponseHeader;
/**
* Session management with improved cookie handling
*
* You can start a session using the static method `Session::start(...)` which is compatible to PHP's built-in `session_start()` function
*
* Note that sessions must always be started before the HTTP headers are sent to the client, i.e. before the actual output starts
*/
final class Session
{
private function __construct()
{
}
/**
* Starts or resumes a session in a way compatible to PHP's built-in `session_start()` function
*
* @param string|null $sameSiteRestriction indicates that the cookie should not be sent along with cross-site requests (either `null`, `None`, `Lax` or `Strict`)
*/
public static function start($sameSiteRestriction = Cookie::SAME_SITE_RESTRICTION_LAX)
{
// run PHP's built-in equivalent
\session_start();
// intercept the cookie header (if any) and rewrite it
self::rewriteCookieHeader($sameSiteRestriction);
}
/**
* Returns or sets the ID of the current session
*
* In order to change the current session ID, pass the new ID as the only argument to this method
*
* Please note that there is rarely a need for the version of this method that *updates* the ID
*
* For most purposes, you may find the method `regenerate` from this same class more helpful
*
* @param string|null $newId (optional) a new session ID to replace the current session ID
* @return string the (old) session ID or an empty string
*/
public static function id($newId = null)
{
if ($newId === null) {
return \session_id();
} else {
return \session_id($newId);
}
}
/**
* Re-generates the session ID in a way compatible to PHP's built-in `session_regenerate_id()` function
*
* @param bool $deleteOldSession whether to delete the old session or not
* @param string|null $sameSiteRestriction indicates that the cookie should not be sent along with cross-site requests (either `null`, `None`, `Lax` or `Strict`)
*/
public static function regenerate($deleteOldSession = \false, $sameSiteRestriction = Cookie::SAME_SITE_RESTRICTION_LAX)
{
// run PHP's built-in equivalent
\session_regenerate_id($deleteOldSession);
// intercept the cookie header (if any) and rewrite it
self::rewriteCookieHeader($sameSiteRestriction);
}
/**
* Checks whether a value for the specified key exists in the session
*
* @param string $key the key to check
* @return bool whether there is a value for the specified key or not
*/
public static function has($key)
{
return isset($_SESSION[$key]);
}
/**
* Returns the requested value from the session or, if not found, the specified default value
*
* @param string $key the key to retrieve the value for
* @param mixed $defaultValue the default value to return if the requested value cannot be found
* @return mixed the requested value or the default value
*/
public static function get($key, $defaultValue = null)
{
if (isset($_SESSION[$key])) {
return $_SESSION[$key];
} else {
return $defaultValue;
}
}
/**
* Returns the requested value and removes it from the session
*
* This is identical to calling `get` first and then `remove` for the same key
*
* @param string $key the key to retrieve and remove the value for
* @param mixed $defaultValue the default value to return if the requested value cannot be found
* @return mixed the requested value or the default value
*/
public static function take($key, $defaultValue = null)
{
if (isset($_SESSION[$key])) {
$value = $_SESSION[$key];
unset($_SESSION[$key]);
return $value;
} else {
return $defaultValue;
}
}
/**
* Sets the value for the specified key to the given value
*
* Any data that already exists for the specified key will be overwritten
*
* @param string $key the key to set the value for
* @param mixed $value the value to set
*/
public static function set($key, $value)
{
$_SESSION[$key] = $value;
}
/**
* Removes the value for the specified key from the session
*
* @param string $key the key to remove the value for
*/
public static function delete($key)
{
unset($_SESSION[$key]);
}
/**
* Intercepts and rewrites the session cookie header
*
* @param string|null $sameSiteRestriction indicates that the cookie should not be sent along with cross-site requests (either `null`, `None`, `Lax` or `Strict`)
*/
private static function rewriteCookieHeader($sameSiteRestriction = Cookie::SAME_SITE_RESTRICTION_LAX)
{
// get and remove the original cookie header set by PHP
$originalCookieHeader = ResponseHeader::take('Set-Cookie', \session_name() . '=');
// if a cookie header has been found
if (isset($originalCookieHeader)) {
// parse it into a cookie instance
$parsedCookie = Cookie::parse($originalCookieHeader);
// if the cookie has successfully been parsed
if (isset($parsedCookie)) {
// apply the supplied same-site restriction
$parsedCookie->setSameSiteRestriction($sameSiteRestriction);
if ($parsedCookie->getSameSiteRestriction() === Cookie::SAME_SITE_RESTRICTION_NONE && !$parsedCookie->isSecureOnly()) {
\trigger_error('You may have to enable the \'session.cookie_secure\' directive in the configuration in \'php.ini\' or via the \'ini_set\' function', \E_USER_WARNING);
}
// save the cookie
$parsedCookie->save();
}
}
}
}