Instantly share code, notes, and snippets.
Last active
July 14, 2021 03:56
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save WinterSilence/778611ed6cbea401af4d605da5b9cc99 to your computer and use it in GitHub Desktop.
Class Enso\Http\Cookie -helper for work with HTTP cookies
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Enso\Http; | |
/** | |
* Helper for work with HTTP cookies. | |
* Based on code of class `Kohana_Cookie`. | |
* | |
* @license MIT | |
*/ | |
class Cookie | |
{ | |
/** | |
* @var string Cookies are allowed to be sent with top-level navigation and will be sent along with GET request | |
* initiated by third party website. | |
*/ | |
public const SAMESITE_LAX = 'Lax'; | |
/** | |
* @var string Cookies will only be sent in a first-party context and not be sent along with requests initiated by | |
* third party sites. | |
*/ | |
public const SAMESITE_STRICT = 'Strict'; | |
/** | |
* @var string Cookies will be sent in all contexts, i.e sending cross-origin is allowed. | |
*/ | |
public const SAMESITE_NONE = 'None'; | |
/** | |
* @var string Separate the salt and the value. | |
*/ | |
protected const SALT_SEPARATOR = '~'; | |
/** | |
* @var string Magic salt to add to the cookie. | |
*/ | |
protected $salt; | |
/** | |
* @var int Number of seconds before the cookie expires. | |
*/ | |
protected $expiration = 0; | |
/** | |
* @var string Restrict the path that the cookie is available to. | |
*/ | |
protected $path = '/'; | |
/** | |
* @var string Restrict the domain that the cookie is available to. | |
*/ | |
protected $domain = ''; | |
/** | |
* @var bool Only transmit cookies over secure connections. | |
*/ | |
protected $secure = false; | |
/** | |
* @var bool Only transmit cookies over HTTP, disabling JavaScript access | |
*/ | |
protected $httpOnly = true; | |
/** | |
* @var string Cookie should be restricted to a first-party or same-site context. | |
*/ | |
protected $sameSite = self::SAMESITE_STRICT; | |
/** | |
* Set salt and initialize cookie options. | |
* | |
* @param string $salt Secret key | |
* @param array $attributes The attributes to set | |
* @throws InvalidArgumentException If salt less than 3 chars | |
*/ | |
public function __construct(string $salt, array $attributes = []) | |
{ | |
if (strlen($salt) < 3) { | |
throw new InvalidArgumentException('Salt less than 3 chars'); | |
} | |
$this->salt = $salt; | |
foreach ($attributes as $name => $value) { | |
$method = 'set' . ucfirst($name); | |
$this->{$method}($value); | |
} | |
} | |
/** | |
* Gets the value of a signed cookie. Cookies without signatures will not be returned. If the cookie signature is | |
* present, but invalid, the cookie will be deleted. | |
* | |
* @param string $name Cookie name | |
* @param mixed $default Default value to return | |
* @return mixed | |
*/ | |
public function get(string $name, $default = null) | |
{ | |
if (filter_has_var(INPUT_COOKIE, $name)) { | |
// Find the position of the split between salt and contents | |
$posSplit = strlen($this->getSalt($name, '')); | |
// Get the cookie value | |
$cookie = filter_input(INPUT_COOKIE, $name, FILTER_SANITIZE_STRING); | |
if (isset($cookie[$posSplit]) && $cookie[$posSplit] === static::SALT_SEPARATOR) { | |
// Separate the salt and the value | |
[$salt, $value] = explode(static::SALT_SEPARATOR, $cookie, 2); | |
$value = $this->decodeValue($value); | |
if ($this->getSalt($name, $value) === $salt) { | |
// Cookie signature is valid | |
return $value; | |
} | |
} | |
// The cookie signature is invalid, delete it | |
$this->delete($name); | |
} | |
// The cookie does not exist | |
return $default; | |
} | |
/** | |
* Sets a signed cookie. | |
* | |
* Note: by default, expiration is 0, if you skip/pass null for the optional lifetime argument your | |
* cookies will expire immediately unless you have separately configured expiration. | |
* | |
* @param string $name The cookie name | |
* @param mixed $value The value to store | |
* @param int|null $lifeTime The cookie life time in seconds | |
* @return bool | |
*/ | |
public function set(string $name, $value, int $lifeTime = null): bool | |
{ | |
if ($lifeTime === null) { | |
// Use the default expiration | |
$lifeTime = $this->expiration; | |
} | |
if ($lifeTime !== 0) { | |
// The expiration is expected to be a UNIX timestamp | |
$lifeTime += $this->getTime(); | |
} | |
$value = $this->encodeValue($value); | |
// Add the salt to the cookie value | |
$value = $this->getSalt($name, $value) . static::SALT_SEPARATOR . $value; | |
return $this->setCookie( | |
$name, | |
$value, | |
$lifeTime, | |
$this->path, | |
$this->domain, | |
$this->secure, | |
$this->httpOnly, | |
$this->sameSite | |
); | |
} | |
/** | |
* Deletes a cookie by making the value NULL and expiring it. | |
* | |
* Note: this method use setter's options. | |
* | |
* @param string $name Cookie name | |
* @return bool | |
*/ | |
public function delete(string $name): bool | |
{ | |
return $this->setCookie( | |
$name, | |
null, | |
-86400, | |
$this->path, | |
$this->domain, | |
$this->secure, | |
$this->httpOnly, | |
$this->sameSite | |
); | |
} | |
/** | |
* Proxy for the `setcookie()` function to allow mocking in unit tests so that they don't fail when headers has | |
* been sent. | |
* | |
* @param string $name The name of the cookie. | |
* @param string|null $value The value of the cookie. | |
* @param int $expires The time the cookie expires. | |
* @param string $path The path on the server in which the cookie will be available on. | |
* @param string $domain The (sub)domain that the cookie is available to. | |
* @param bool $secure Should only be transmitted over a secure HTTPS connection from the client. | |
* @param bool $httpOnly Will be made accessible only through the HTTP protocol. | |
* @param string $sameSite Should be restricted to a first-party or same-site context. | |
* @return bool | |
*/ | |
protected function setCookie( | |
string $name, | |
$value, | |
int $expires, | |
string $path, | |
string $domain, | |
bool $secure, | |
bool $httpOnly, | |
string $sameSite | |
): bool { | |
if (PHP_VERSION_ID >= 70300) { | |
$options = [ | |
'expires' => $expires, | |
'path' => $path, | |
'domain' => $domain, | |
'secure' => $secure, | |
'httponly' => $httpOnly, | |
'samesite' => $sameSite, | |
]; | |
return setcookie($name, $value, $options); | |
} | |
// Hint to send "SameSite" option in PHP < 7.3 | |
$path .= '; SameSite=' . $sameSite; | |
return setcookie($name, $value, $expires, $path, $domain, $secure, $httpOnly); | |
} | |
/** | |
* Generates a salt string for a cookie based on the name and value. | |
* | |
* @param string $name Name of cookie | |
* @param string $value Value of cookie | |
* @return string|null | |
*/ | |
protected function getSalt(string $name, string $value): ?string | |
{ | |
return hash_hmac('sha1', $name . $value . $this->salt, $this->salt) ?: null; | |
} | |
/** | |
* Decodes JSON and returns value. | |
* | |
* @param string $value JSON string to decode | |
* @return mixed | |
* @throws JsonException Invalid JSON | |
*/ | |
protected function decodeValue(string $value) | |
{ | |
return json_decode($value, null, 512, JSON_BIGINT_AS_STRING | JSON_THROW_ON_ERROR); | |
} | |
/** | |
* Encodes value and returns as JSON string. | |
* | |
* @param mixed $value Value to encode | |
* @return string|null | |
* @throws JsonException | |
*/ | |
protected function encodeValue($value): ?string | |
{ | |
$flags = JSON_HEX_QUOT | JSON_NUMERIC_CHECK | JSON_PRESERVE_ZERO_FRACTION | JSON_THROW_ON_ERROR; | |
return json_encode($value, $flags) ?: null; | |
} | |
/** | |
* Proxy for the native time function - to allow mocking of time-related logic in unit tests. | |
* | |
* @return int | |
*/ | |
protected function getTime(): int | |
{ | |
return time(); | |
} | |
/** | |
* Sets expiration time. | |
* | |
* @param int $expiration New value. | |
* @return $this | |
*/ | |
public function setExpiration(int $expiration): self | |
{ | |
if ($expiration < 0) { | |
throw new InvalidArgumentException('Invalid expiration time: ' . $expiration); | |
} | |
$this->expiration = $expiration; | |
return $this; | |
} | |
/** | |
* Sets restrict the path that the cookie is available to. | |
* | |
* @param string $path New value. | |
* @return $this | |
*/ | |
public function setPath(string $path): self | |
{ | |
$this->path = '/' . ltrim($path, '/'); | |
return $this; | |
} | |
/** | |
* Sets restrict the domain that the cookie is available to. | |
* | |
* @param string $domain New value. | |
* @return $this | |
*/ | |
public function setDomain(string $domain): self | |
{ | |
// normalize domain name | |
$domain = ltrim(rtrim($domain, '/'), '.'); | |
if (filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) == false) { | |
throw new InvalidArgumentException('Invalid domain name: ' . $domain); | |
} | |
$this->domain = $domain; | |
return $this; | |
} | |
/** | |
* Sets transmit cookies over secure connections. | |
* | |
* @param bool $secure New value. | |
* @return $this | |
*/ | |
public function setSecure(bool $secure): self | |
{ | |
$this->secure = $secure; | |
return $this; | |
} | |
/** | |
* Sets transmit cookies over HTTP, disabling JavaScript access. | |
* | |
* @param bool $httpOnly New value. | |
* @return $this | |
*/ | |
public function setHttpOnly(bool $httpOnly): self | |
{ | |
$this->httpOnly = $httpOnly; | |
return $this; | |
} | |
/** | |
* Sets `SameSite` attribute. | |
* | |
* @param string $sameSite New value. | |
* @return $this | |
*/ | |
public function setSameSite(string $sameSite): self | |
{ | |
$this->sameSite = $sameSite; | |
return $this; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment