Chris@0: null, Chris@0: 'Value' => null, Chris@0: 'Domain' => null, Chris@0: 'Path' => '/', Chris@0: 'Max-Age' => null, Chris@0: 'Expires' => null, Chris@0: 'Secure' => false, Chris@0: 'Discard' => false, Chris@0: 'HttpOnly' => false Chris@0: ]; Chris@0: Chris@0: /** @var array Cookie data */ Chris@0: private $data; Chris@0: Chris@0: /** Chris@0: * Create a new SetCookie object from a string Chris@0: * Chris@0: * @param string $cookie Set-Cookie header string Chris@0: * Chris@0: * @return self Chris@0: */ Chris@0: public static function fromString($cookie) Chris@0: { Chris@0: // Create the default return array Chris@0: $data = self::$defaults; Chris@0: // Explode the cookie string using a series of semicolons Chris@0: $pieces = array_filter(array_map('trim', explode(';', $cookie))); Chris@13: // The name of the cookie (first kvp) must exist and include an equal sign. Chris@13: if (empty($pieces[0]) || !strpos($pieces[0], '=')) { Chris@0: return new self($data); Chris@0: } Chris@0: Chris@0: // Add the cookie pieces into the parsed data array Chris@0: foreach ($pieces as $part) { Chris@0: $cookieParts = explode('=', $part, 2); Chris@0: $key = trim($cookieParts[0]); Chris@0: $value = isset($cookieParts[1]) Chris@0: ? trim($cookieParts[1], " \n\r\t\0\x0B") Chris@0: : true; Chris@0: Chris@0: // Only check for non-cookies when cookies have been found Chris@0: if (empty($data['Name'])) { Chris@0: $data['Name'] = $key; Chris@0: $data['Value'] = $value; Chris@0: } else { Chris@0: foreach (array_keys(self::$defaults) as $search) { Chris@0: if (!strcasecmp($search, $key)) { Chris@0: $data[$search] = $value; Chris@0: continue 2; Chris@0: } Chris@0: } Chris@0: $data[$key] = $value; Chris@0: } Chris@0: } Chris@0: Chris@0: return new self($data); Chris@0: } Chris@0: Chris@0: /** Chris@0: * @param array $data Array of cookie data provided by a Cookie parser Chris@0: */ Chris@0: public function __construct(array $data = []) Chris@0: { Chris@0: $this->data = array_replace(self::$defaults, $data); Chris@0: // Extract the Expires value and turn it into a UNIX timestamp if needed Chris@0: if (!$this->getExpires() && $this->getMaxAge()) { Chris@0: // Calculate the Expires date Chris@0: $this->setExpires(time() + $this->getMaxAge()); Chris@0: } elseif ($this->getExpires() && !is_numeric($this->getExpires())) { Chris@0: $this->setExpires($this->getExpires()); Chris@0: } Chris@0: } Chris@0: Chris@0: public function __toString() Chris@0: { Chris@0: $str = $this->data['Name'] . '=' . $this->data['Value'] . '; '; Chris@0: foreach ($this->data as $k => $v) { Chris@0: if ($k !== 'Name' && $k !== 'Value' && $v !== null && $v !== false) { Chris@0: if ($k === 'Expires') { Chris@0: $str .= 'Expires=' . gmdate('D, d M Y H:i:s \G\M\T', $v) . '; '; Chris@0: } else { Chris@0: $str .= ($v === true ? $k : "{$k}={$v}") . '; '; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: return rtrim($str, '; '); Chris@0: } Chris@0: Chris@0: public function toArray() Chris@0: { Chris@0: return $this->data; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Get the cookie name Chris@0: * Chris@0: * @return string Chris@0: */ Chris@0: public function getName() Chris@0: { Chris@0: return $this->data['Name']; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Set the cookie name Chris@0: * Chris@0: * @param string $name Cookie name Chris@0: */ Chris@0: public function setName($name) Chris@0: { Chris@0: $this->data['Name'] = $name; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Get the cookie value Chris@0: * Chris@0: * @return string Chris@0: */ Chris@0: public function getValue() Chris@0: { Chris@0: return $this->data['Value']; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Set the cookie value Chris@0: * Chris@0: * @param string $value Cookie value Chris@0: */ Chris@0: public function setValue($value) Chris@0: { Chris@0: $this->data['Value'] = $value; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Get the domain Chris@0: * Chris@0: * @return string|null Chris@0: */ Chris@0: public function getDomain() Chris@0: { Chris@0: return $this->data['Domain']; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Set the domain of the cookie Chris@0: * Chris@0: * @param string $domain Chris@0: */ Chris@0: public function setDomain($domain) Chris@0: { Chris@0: $this->data['Domain'] = $domain; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Get the path Chris@0: * Chris@0: * @return string Chris@0: */ Chris@0: public function getPath() Chris@0: { Chris@0: return $this->data['Path']; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Set the path of the cookie Chris@0: * Chris@0: * @param string $path Path of the cookie Chris@0: */ Chris@0: public function setPath($path) Chris@0: { Chris@0: $this->data['Path'] = $path; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Maximum lifetime of the cookie in seconds Chris@0: * Chris@0: * @return int|null Chris@0: */ Chris@0: public function getMaxAge() Chris@0: { Chris@0: return $this->data['Max-Age']; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Set the max-age of the cookie Chris@0: * Chris@0: * @param int $maxAge Max age of the cookie in seconds Chris@0: */ Chris@0: public function setMaxAge($maxAge) Chris@0: { Chris@0: $this->data['Max-Age'] = $maxAge; Chris@0: } Chris@0: Chris@0: /** Chris@0: * The UNIX timestamp when the cookie Expires Chris@0: * Chris@0: * @return mixed Chris@0: */ Chris@0: public function getExpires() Chris@0: { Chris@0: return $this->data['Expires']; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Set the unix timestamp for which the cookie will expire Chris@0: * Chris@0: * @param int $timestamp Unix timestamp Chris@0: */ Chris@0: public function setExpires($timestamp) Chris@0: { Chris@0: $this->data['Expires'] = is_numeric($timestamp) Chris@0: ? (int) $timestamp Chris@0: : strtotime($timestamp); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Get whether or not this is a secure cookie Chris@0: * Chris@0: * @return null|bool Chris@0: */ Chris@0: public function getSecure() Chris@0: { Chris@0: return $this->data['Secure']; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Set whether or not the cookie is secure Chris@0: * Chris@0: * @param bool $secure Set to true or false if secure Chris@0: */ Chris@0: public function setSecure($secure) Chris@0: { Chris@0: $this->data['Secure'] = $secure; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Get whether or not this is a session cookie Chris@0: * Chris@0: * @return null|bool Chris@0: */ Chris@0: public function getDiscard() Chris@0: { Chris@0: return $this->data['Discard']; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Set whether or not this is a session cookie Chris@0: * Chris@0: * @param bool $discard Set to true or false if this is a session cookie Chris@0: */ Chris@0: public function setDiscard($discard) Chris@0: { Chris@0: $this->data['Discard'] = $discard; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Get whether or not this is an HTTP only cookie Chris@0: * Chris@0: * @return bool Chris@0: */ Chris@0: public function getHttpOnly() Chris@0: { Chris@0: return $this->data['HttpOnly']; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Set whether or not this is an HTTP only cookie Chris@0: * Chris@0: * @param bool $httpOnly Set to true or false if this is HTTP only Chris@0: */ Chris@0: public function setHttpOnly($httpOnly) Chris@0: { Chris@0: $this->data['HttpOnly'] = $httpOnly; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Check if the cookie matches a path value. Chris@0: * Chris@0: * A request-path path-matches a given cookie-path if at least one of Chris@0: * the following conditions holds: Chris@0: * Chris@0: * - The cookie-path and the request-path are identical. Chris@0: * - The cookie-path is a prefix of the request-path, and the last Chris@0: * character of the cookie-path is %x2F ("/"). Chris@0: * - The cookie-path is a prefix of the request-path, and the first Chris@0: * character of the request-path that is not included in the cookie- Chris@0: * path is a %x2F ("/") character. Chris@0: * Chris@0: * @param string $requestPath Path to check against Chris@0: * Chris@0: * @return bool Chris@0: */ Chris@0: public function matchesPath($requestPath) Chris@0: { Chris@0: $cookiePath = $this->getPath(); Chris@0: Chris@0: // Match on exact matches or when path is the default empty "/" Chris@0: if ($cookiePath === '/' || $cookiePath == $requestPath) { Chris@0: return true; Chris@0: } Chris@0: Chris@0: // Ensure that the cookie-path is a prefix of the request path. Chris@0: if (0 !== strpos($requestPath, $cookiePath)) { Chris@0: return false; Chris@0: } Chris@0: Chris@0: // Match if the last character of the cookie-path is "/" Chris@0: if (substr($cookiePath, -1, 1) === '/') { Chris@0: return true; Chris@0: } Chris@0: Chris@0: // Match if the first character not included in cookie path is "/" Chris@0: return substr($requestPath, strlen($cookiePath), 1) === '/'; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Check if the cookie matches a domain value Chris@0: * Chris@0: * @param string $domain Domain to check against Chris@0: * Chris@0: * @return bool Chris@0: */ Chris@0: public function matchesDomain($domain) Chris@0: { Chris@0: // Remove the leading '.' as per spec in RFC 6265. Chris@0: // http://tools.ietf.org/html/rfc6265#section-5.2.3 Chris@0: $cookieDomain = ltrim($this->getDomain(), '.'); Chris@0: Chris@0: // Domain not set or exact match. Chris@0: if (!$cookieDomain || !strcasecmp($domain, $cookieDomain)) { Chris@0: return true; Chris@0: } Chris@0: Chris@0: // Matching the subdomain according to RFC 6265. Chris@0: // http://tools.ietf.org/html/rfc6265#section-5.1.3 Chris@0: if (filter_var($domain, FILTER_VALIDATE_IP)) { Chris@0: return false; Chris@0: } Chris@0: Chris@13: return (bool) preg_match('/\.' . preg_quote($cookieDomain, '/') . '$/', $domain); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Check if the cookie is expired Chris@0: * Chris@0: * @return bool Chris@0: */ Chris@0: public function isExpired() Chris@0: { Chris@13: return $this->getExpires() !== null && time() > $this->getExpires(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Check if the cookie is valid according to RFC 6265 Chris@0: * Chris@0: * @return bool|string Returns true if valid or an error message if invalid Chris@0: */ Chris@0: public function validate() Chris@0: { Chris@0: // Names must not be empty, but can be 0 Chris@0: $name = $this->getName(); Chris@0: if (empty($name) && !is_numeric($name)) { Chris@0: return 'The cookie name must not be empty'; Chris@0: } Chris@0: Chris@0: // Check if any of the invalid characters are present in the cookie name Chris@0: if (preg_match( Chris@0: '/[\x00-\x20\x22\x28-\x29\x2c\x2f\x3a-\x40\x5c\x7b\x7d\x7f]/', Chris@13: $name Chris@13: )) { Chris@0: return 'Cookie name must not contain invalid characters: ASCII ' Chris@0: . 'Control characters (0-31;127), space, tab and the ' Chris@0: . 'following characters: ()<>@,;:\"/?={}'; Chris@0: } Chris@0: Chris@0: // Value must not be empty, but can be 0 Chris@0: $value = $this->getValue(); Chris@0: if (empty($value) && !is_numeric($value)) { Chris@0: return 'The cookie value must not be empty'; Chris@0: } Chris@0: Chris@0: // Domains must not be empty, but can be 0 Chris@0: // A "0" is not a valid internet domain, but may be used as server name Chris@0: // in a private network. Chris@0: $domain = $this->getDomain(); Chris@0: if (empty($domain) && !is_numeric($domain)) { Chris@0: return 'The cookie domain must not be empty'; Chris@0: } Chris@0: Chris@0: return true; Chris@0: } Chris@0: }