c
This commit is contained in:
606
inc/Hura8/System/Security/Cookie.php
Normal file
606
inc/Hura8/System/Security/Cookie.php
Normal file
@@ -0,0 +1,606 @@
|
||||
<?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 Hura8\System\Security;
|
||||
|
||||
/**
|
||||
* 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 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;
|
||||
}
|
||||
|
||||
}
|
||||
126
inc/Hura8/System/Security/DataClean.php
Normal file
126
inc/Hura8/System/Security/DataClean.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace Hura8\System\Security;
|
||||
|
||||
|
||||
class DataClean
|
||||
{
|
||||
|
||||
/**
|
||||
* @description limit max length. limitLength("toi ten nguyen", 10) => "toi ten ng"
|
||||
* @param string $text
|
||||
* @param int $max_length
|
||||
* @return string
|
||||
*/
|
||||
public static function limitLength(string $text, int $max_length = 100 ) : string
|
||||
{
|
||||
if(strlen($text) <= $max_length) {
|
||||
return $text;
|
||||
}
|
||||
|
||||
return substr($text, 0, $max_length-1);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @description limit max length but keep full words. limitLengthFullWords("toi ten nguyen", 10) => "toi ten"
|
||||
* @param string $text
|
||||
* @param int $max_length
|
||||
* @return string
|
||||
*/
|
||||
public static function limitLengthFullWords(string $text, int $max_length = 100 ) : string
|
||||
{
|
||||
|
||||
if(strlen($text) <= $max_length) {
|
||||
return $text;
|
||||
}
|
||||
|
||||
$char_at_max_length = $text[$max_length-1];
|
||||
if($char_at_max_length == ' ') {
|
||||
return substr($text, 0, $max_length-1);
|
||||
}
|
||||
|
||||
//else fall back to the last space
|
||||
$words = explode(" ", substr($text, 0, $max_length-1));
|
||||
array_pop($words);
|
||||
|
||||
return join(" ", $words);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param $input_list array
|
||||
* @param $type string
|
||||
* @return array
|
||||
*/
|
||||
public static function makeListOfInputSafe(array $input_list, $type){
|
||||
if(!sizeof($input_list)) return [];
|
||||
|
||||
$result = [];
|
||||
foreach ($input_list as $key => $str) {
|
||||
$result[$key] = self::makeInputSafe($str, $type);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param $input string data to process
|
||||
* @param $type string, type of data to validate against, enum : int|double|email|id|string|date|plain
|
||||
* @return mixed
|
||||
*/
|
||||
public static function makeInputSafe($input, $type){
|
||||
|
||||
if(is_array($input)) {
|
||||
return $input;
|
||||
}
|
||||
|
||||
if ( $type == DataType::ID ) {
|
||||
//is $input the database item_id ?
|
||||
return preg_replace('/[^a-z0-9_\-\.]/i', '', $input);
|
||||
}
|
||||
|
||||
if ( $type == DataType::EMAIL ) {
|
||||
return filter_var($input, FILTER_VALIDATE_EMAIL) ? $input : '';
|
||||
}
|
||||
|
||||
if ( $type == DataType::INTEGER ) {
|
||||
// support negative number
|
||||
return (int) preg_replace('/[^0-9\-]/', '', $input);
|
||||
}
|
||||
|
||||
if ( $type == DataType::DOUBLE ) {
|
||||
// support negative number
|
||||
$input = preg_replace('/[^0-9,\-]/', '', $input);
|
||||
//convert vietnamese style , to . for percentage
|
||||
$input = str_replace(",", ".", $input);
|
||||
|
||||
return (double) $input;
|
||||
}
|
||||
|
||||
if ( $type == DataType::DATE ) {
|
||||
// support pattern:
|
||||
// date = d-m-Y
|
||||
// datetime = d-m-Y H:i:a
|
||||
$pattern = "/([0-9]{2})-([0-9]{2})-([0-9]{2,4})(\s)?([0-9]{1,2}:[0-9]{1,2}(:[0-9]{1,2})?)?/i";
|
||||
return preg_replace($pattern, '', $input);
|
||||
}
|
||||
|
||||
if ( $type == DataType::PLAIN_TEXT || $type == DataType::STRING ) {
|
||||
return strip_tags($input);
|
||||
}
|
||||
|
||||
if ( $type == DataType::NON_VIETNAMESE ) {
|
||||
return preg_replace('/[^a-z0-9_\s\-]/i', '', $input);
|
||||
}
|
||||
|
||||
if ( $type == DataType::RICH_TEXT ) {
|
||||
return $input;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
22
inc/Hura8/System/Security/DataFormatter.php
Normal file
22
inc/Hura8/System/Security/DataFormatter.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Hura8\System\Security;
|
||||
|
||||
class DataFormatter
|
||||
{
|
||||
|
||||
//remove space, ., ,
|
||||
//add 84 to 0923232123 -> 84923232123
|
||||
//dont change 84923232123 -> 84923232123
|
||||
public static function formatMobile($mobile) {
|
||||
$mobile = preg_replace("/[^0-9]/i", "", $mobile);
|
||||
$first_2 = substr($mobile, 0, 2);
|
||||
if( $first_2 != '84') {
|
||||
return '84'.substr($mobile, 1);
|
||||
}
|
||||
|
||||
return $mobile;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
17
inc/Hura8/System/Security/DataType.php
Normal file
17
inc/Hura8/System/Security/DataType.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Hura8\System\Security;
|
||||
|
||||
class DataType
|
||||
{
|
||||
const ID = 'id';
|
||||
const EMAIL = 'email';
|
||||
const IP_ADDRESS = 'ip';
|
||||
const INTEGER = 'int';
|
||||
const DOUBLE = 'double';
|
||||
const DATE = 'date'; // date or date-time
|
||||
const STRING = 'plain'; // string without tags
|
||||
const PLAIN_TEXT = 'plain'; // same as STRING type, use any, prefer PLAIN_TEXT to make it clear
|
||||
const NON_VIETNAMESE = 'non_vi'; // same as PLAIN_TEXT and without vietnamese strokes (nguyen instead of Nguyễn)
|
||||
const RICH_TEXT = 'rich_text'; // with html tags
|
||||
}
|
||||
78
inc/Hura8/System/Security/DataValidator.php
Normal file
78
inc/Hura8/System/Security/DataValidator.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Hura8\System\Security;
|
||||
|
||||
|
||||
use Hura8\System\Constant;
|
||||
use Hura8\Traits\ClassCacheTrait;
|
||||
|
||||
class DataValidator
|
||||
{
|
||||
|
||||
use ClassCacheTrait;
|
||||
|
||||
|
||||
public static function isValidLength($str, $max_length=50) : bool {
|
||||
if(!is_string($str)) return '';
|
||||
|
||||
return (strlen($str) <= $max_length);
|
||||
}
|
||||
|
||||
|
||||
public static function isIPAddress($input, $type = '') : bool {
|
||||
//validates IPv4
|
||||
if($type == '4') {
|
||||
return filter_var($input, FILTER_VALIDATE_IP,FILTER_FLAG_IPV4);
|
||||
}
|
||||
|
||||
//validates IPv6
|
||||
if($type == '6') {
|
||||
return filter_var($input, FILTER_VALIDATE_IP,FILTER_FLAG_IPV6);
|
||||
}
|
||||
|
||||
// all type
|
||||
// return filter_var($input, FILTER_VALIDATE_IP,FILTER_FLAG_IPV4);
|
||||
|
||||
//validates IPv4 and IPv6, excluding reserved and private ranges
|
||||
return filter_var($input, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static function isDate($input) {
|
||||
// support pattern:
|
||||
// date = d-m-Y
|
||||
// datetime = d-m-Y H:i:a
|
||||
$pattern = "/([0-9]{2})-([0-9]{2})-([0-9]{2,4})(\s)?([0-9]{1,2}:[0-9]{1,2}(:[0-9]{1,2})?)?/i";
|
||||
return (preg_match($pattern, $input));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $input
|
||||
* @return string|int
|
||||
*/
|
||||
public static function isMobile(string $input) {
|
||||
$clean = preg_replace("/[^0-9]/", '', $input);
|
||||
return (preg_match('/[0-9]{8,14}$/', $clean));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param $input
|
||||
* @return bool
|
||||
*/
|
||||
public static function isEmail($input): bool
|
||||
{
|
||||
return filter_var(trim($input), FILTER_VALIDATE_EMAIL);
|
||||
}
|
||||
|
||||
|
||||
public static function isGender($input) : bool {
|
||||
$system_list = Constant::genderList();
|
||||
|
||||
return (array_key_exists($input, $system_list));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
38
inc/Hura8/System/Security/Encryption.php
Normal file
38
inc/Hura8/System/Security/Encryption.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Hura8\System\Security;
|
||||
|
||||
use Firebase\JWT\JWT;
|
||||
|
||||
class Encryption
|
||||
{
|
||||
|
||||
// list of algorithm supported in JWT::$supported_algs
|
||||
const JWT_ALG = 'HS256';
|
||||
|
||||
|
||||
public static function jwtDecode($jwt_code, $secret_key) {
|
||||
try {
|
||||
$decoded = (array) JWT::decode($jwt_code, $secret_key, array(Encryption::JWT_ALG));
|
||||
return (array) $decoded['data'];
|
||||
}catch (\Exception $exception) {
|
||||
// nothing
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public static function jwtEncode(array $data, $secret_key, $expire_timestamp = 0) {
|
||||
$payload = array(
|
||||
"iss" => $_SERVER['HTTP_HOST'],
|
||||
"data" => $data,
|
||||
"iat" => time(),
|
||||
);
|
||||
|
||||
if($expire_timestamp) $payload['exp'] = $expire_timestamp;
|
||||
|
||||
return JWT::encode($payload, $secret_key, Encryption::JWT_ALG);
|
||||
}
|
||||
|
||||
}
|
||||
18
inc/Hura8/System/Security/Hash.php
Normal file
18
inc/Hura8/System/Security/Hash.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Hura8\System\Security;
|
||||
|
||||
class Hash
|
||||
{
|
||||
|
||||
protected const CHECK_SUM_KEY = "as21321ASDAS"; // can change this key per project
|
||||
|
||||
public static function createCheckSum($txt) : string {
|
||||
return hash_hmac('sha256', $txt, static::CHECK_SUM_KEY);
|
||||
}
|
||||
|
||||
public static function verifyCheckSum($checksum, $txt) : bool {
|
||||
return ($checksum == static::createCheckSum($txt));
|
||||
}
|
||||
|
||||
}
|
||||
224
inc/Hura8/System/Security/Session.php
Normal file
224
inc/Hura8/System/Security/Session.php
Normal file
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
|
||||
namespace Hura8\System\Security;
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* 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)
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* 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 = Session::takeHeaderCookie('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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns and removes the header with the specified name (and optional value prefix)
|
||||
*
|
||||
* @param string $name the name of the header
|
||||
* @param string $valuePrefix the optional string to match at the beginning of the header's value
|
||||
* @return string|null the header (if found) or `null`
|
||||
*/
|
||||
private static function takeHeaderCookie($name, $valuePrefix = '') {
|
||||
if (empty($name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$nameLength = \strlen($name);
|
||||
$headers = \headers_list();
|
||||
|
||||
$first = null;
|
||||
$homonyms = [];
|
||||
|
||||
foreach ($headers as $header) {
|
||||
if (\strcasecmp(\substr($header, 0, $nameLength + 1), ($name . ':')) === 0) {
|
||||
$headerValue = \trim(\substr($header, $nameLength + 1), "\t ");
|
||||
|
||||
if ((empty($valuePrefix) || \substr($headerValue, 0, \strlen($valuePrefix)) === $valuePrefix) && $first === null) {
|
||||
$first = $header;
|
||||
}
|
||||
else {
|
||||
$homonyms[] = $header;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($first !== null) {
|
||||
\header_remove($name);
|
||||
|
||||
foreach ($homonyms as $homonym) {
|
||||
\header($homonym, false);
|
||||
}
|
||||
}
|
||||
|
||||
return $first;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user