1<?php 2 3namespace GuzzleHttp\Cookie; 4 5/** 6 * Set-Cookie object 7 */ 8class SetCookie 9{ 10 /** 11 * @var array 12 */ 13 private static $defaults = [ 14 'Name' => null, 15 'Value' => null, 16 'Domain' => null, 17 'Path' => '/', 18 'Max-Age' => null, 19 'Expires' => null, 20 'Secure' => false, 21 'Discard' => false, 22 'HttpOnly' => false 23 ]; 24 25 /** 26 * @var array Cookie data 27 */ 28 private $data; 29 30 /** 31 * Create a new SetCookie object from a string. 32 * 33 * @param string $cookie Set-Cookie header string 34 */ 35 public static function fromString(string $cookie): self 36 { 37 // Create the default return array 38 $data = self::$defaults; 39 // Explode the cookie string using a series of semicolons 40 $pieces = \array_filter(\array_map('trim', \explode(';', $cookie))); 41 // The name of the cookie (first kvp) must exist and include an equal sign. 42 if (!isset($pieces[0]) || \strpos($pieces[0], '=') === false) { 43 return new self($data); 44 } 45 46 // Add the cookie pieces into the parsed data array 47 foreach ($pieces as $part) { 48 $cookieParts = \explode('=', $part, 2); 49 $key = \trim($cookieParts[0]); 50 $value = isset($cookieParts[1]) 51 ? \trim($cookieParts[1], " \n\r\t\0\x0B") 52 : true; 53 54 // Only check for non-cookies when cookies have been found 55 if (!isset($data['Name'])) { 56 $data['Name'] = $key; 57 $data['Value'] = $value; 58 } else { 59 foreach (\array_keys(self::$defaults) as $search) { 60 if (!\strcasecmp($search, $key)) { 61 $data[$search] = $value; 62 continue 2; 63 } 64 } 65 $data[$key] = $value; 66 } 67 } 68 69 return new self($data); 70 } 71 72 /** 73 * @param array $data Array of cookie data provided by a Cookie parser 74 */ 75 public function __construct(array $data = []) 76 { 77 /** @var array|null $replaced will be null in case of replace error */ 78 $replaced = \array_replace(self::$defaults, $data); 79 if ($replaced === null) { 80 throw new \InvalidArgumentException('Unable to replace the default values for the Cookie.'); 81 } 82 83 $this->data = $replaced; 84 // Extract the Expires value and turn it into a UNIX timestamp if needed 85 if (!$this->getExpires() && $this->getMaxAge()) { 86 // Calculate the Expires date 87 $this->setExpires(\time() + $this->getMaxAge()); 88 } elseif (null !== ($expires = $this->getExpires()) && !\is_numeric($expires)) { 89 $this->setExpires($expires); 90 } 91 } 92 93 public function __toString() 94 { 95 $str = $this->data['Name'] . '=' . $this->data['Value'] . '; '; 96 foreach ($this->data as $k => $v) { 97 if ($k !== 'Name' && $k !== 'Value' && $v !== null && $v !== false) { 98 if ($k === 'Expires') { 99 $str .= 'Expires=' . \gmdate('D, d M Y H:i:s \G\M\T', $v) . '; '; 100 } else { 101 $str .= ($v === true ? $k : "{$k}={$v}") . '; '; 102 } 103 } 104 } 105 106 return \rtrim($str, '; '); 107 } 108 109 public function toArray(): array 110 { 111 return $this->data; 112 } 113 114 /** 115 * Get the cookie name. 116 * 117 * @return string 118 */ 119 public function getName() 120 { 121 return $this->data['Name']; 122 } 123 124 /** 125 * Set the cookie name. 126 * 127 * @param string $name Cookie name 128 */ 129 public function setName($name): void 130 { 131 $this->data['Name'] = $name; 132 } 133 134 /** 135 * Get the cookie value. 136 * 137 * @return string|null 138 */ 139 public function getValue() 140 { 141 return $this->data['Value']; 142 } 143 144 /** 145 * Set the cookie value. 146 * 147 * @param string $value Cookie value 148 */ 149 public function setValue($value): void 150 { 151 $this->data['Value'] = $value; 152 } 153 154 /** 155 * Get the domain. 156 * 157 * @return string|null 158 */ 159 public function getDomain() 160 { 161 return $this->data['Domain']; 162 } 163 164 /** 165 * Set the domain of the cookie. 166 * 167 * @param string $domain 168 */ 169 public function setDomain($domain): void 170 { 171 $this->data['Domain'] = $domain; 172 } 173 174 /** 175 * Get the path. 176 * 177 * @return string 178 */ 179 public function getPath() 180 { 181 return $this->data['Path']; 182 } 183 184 /** 185 * Set the path of the cookie. 186 * 187 * @param string $path Path of the cookie 188 */ 189 public function setPath($path): void 190 { 191 $this->data['Path'] = $path; 192 } 193 194 /** 195 * Maximum lifetime of the cookie in seconds. 196 * 197 * @return int|null 198 */ 199 public function getMaxAge() 200 { 201 return $this->data['Max-Age']; 202 } 203 204 /** 205 * Set the max-age of the cookie. 206 * 207 * @param int $maxAge Max age of the cookie in seconds 208 */ 209 public function setMaxAge($maxAge): void 210 { 211 $this->data['Max-Age'] = $maxAge; 212 } 213 214 /** 215 * The UNIX timestamp when the cookie Expires. 216 * 217 * @return string|int|null 218 */ 219 public function getExpires() 220 { 221 return $this->data['Expires']; 222 } 223 224 /** 225 * Set the unix timestamp for which the cookie will expire. 226 * 227 * @param int|string $timestamp Unix timestamp or any English textual datetime description. 228 */ 229 public function setExpires($timestamp): void 230 { 231 $this->data['Expires'] = \is_numeric($timestamp) 232 ? (int) $timestamp 233 : \strtotime($timestamp); 234 } 235 236 /** 237 * Get whether or not this is a secure cookie. 238 * 239 * @return bool|null 240 */ 241 public function getSecure() 242 { 243 return $this->data['Secure']; 244 } 245 246 /** 247 * Set whether or not the cookie is secure. 248 * 249 * @param bool $secure Set to true or false if secure 250 */ 251 public function setSecure($secure): void 252 { 253 $this->data['Secure'] = $secure; 254 } 255 256 /** 257 * Get whether or not this is a session cookie. 258 * 259 * @return bool|null 260 */ 261 public function getDiscard() 262 { 263 return $this->data['Discard']; 264 } 265 266 /** 267 * Set whether or not this is a session cookie. 268 * 269 * @param bool $discard Set to true or false if this is a session cookie 270 */ 271 public function setDiscard($discard): void 272 { 273 $this->data['Discard'] = $discard; 274 } 275 276 /** 277 * Get whether or not this is an HTTP only cookie. 278 * 279 * @return bool 280 */ 281 public function getHttpOnly() 282 { 283 return $this->data['HttpOnly']; 284 } 285 286 /** 287 * Set whether or not this is an HTTP only cookie. 288 * 289 * @param bool $httpOnly Set to true or false if this is HTTP only 290 */ 291 public function setHttpOnly($httpOnly): void 292 { 293 $this->data['HttpOnly'] = $httpOnly; 294 } 295 296 /** 297 * Check if the cookie matches a path value. 298 * 299 * A request-path path-matches a given cookie-path if at least one of 300 * the following conditions holds: 301 * 302 * - The cookie-path and the request-path are identical. 303 * - The cookie-path is a prefix of the request-path, and the last 304 * character of the cookie-path is %x2F ("/"). 305 * - The cookie-path is a prefix of the request-path, and the first 306 * character of the request-path that is not included in the cookie- 307 * path is a %x2F ("/") character. 308 * 309 * @param string $requestPath Path to check against 310 */ 311 public function matchesPath(string $requestPath): bool 312 { 313 $cookiePath = $this->getPath(); 314 315 // Match on exact matches or when path is the default empty "/" 316 if ($cookiePath === '/' || $cookiePath == $requestPath) { 317 return true; 318 } 319 320 // Ensure that the cookie-path is a prefix of the request path. 321 if (0 !== \strpos($requestPath, $cookiePath)) { 322 return false; 323 } 324 325 // Match if the last character of the cookie-path is "/" 326 if (\substr($cookiePath, -1, 1) === '/') { 327 return true; 328 } 329 330 // Match if the first character not included in cookie path is "/" 331 return \substr($requestPath, \strlen($cookiePath), 1) === '/'; 332 } 333 334 /** 335 * Check if the cookie matches a domain value. 336 * 337 * @param string $domain Domain to check against 338 */ 339 public function matchesDomain(string $domain): bool 340 { 341 $cookieDomain = $this->getDomain(); 342 if (null === $cookieDomain) { 343 return true; 344 } 345 346 // Remove the leading '.' as per spec in RFC 6265. 347 // https://tools.ietf.org/html/rfc6265#section-5.2.3 348 $cookieDomain = \ltrim($cookieDomain, '.'); 349 350 // Domain not set or exact match. 351 if (!$cookieDomain || !\strcasecmp($domain, $cookieDomain)) { 352 return true; 353 } 354 355 // Matching the subdomain according to RFC 6265. 356 // https://tools.ietf.org/html/rfc6265#section-5.1.3 357 if (\filter_var($domain, \FILTER_VALIDATE_IP)) { 358 return false; 359 } 360 361 return (bool) \preg_match('/\.' . \preg_quote($cookieDomain, '/') . '$/', $domain); 362 } 363 364 /** 365 * Check if the cookie is expired. 366 */ 367 public function isExpired(): bool 368 { 369 return $this->getExpires() !== null && \time() > $this->getExpires(); 370 } 371 372 /** 373 * Check if the cookie is valid according to RFC 6265. 374 * 375 * @return bool|string Returns true if valid or an error message if invalid 376 */ 377 public function validate() 378 { 379 $name = $this->getName(); 380 if ($name === '') { 381 return 'The cookie name must not be empty'; 382 } 383 384 // Check if any of the invalid characters are present in the cookie name 385 if (\preg_match( 386 '/[\x00-\x20\x22\x28-\x29\x2c\x2f\x3a-\x40\x5c\x7b\x7d\x7f]/', 387 $name 388 )) { 389 return 'Cookie name must not contain invalid characters: ASCII ' 390 . 'Control characters (0-31;127), space, tab and the ' 391 . 'following characters: ()<>@,;:\"/?={}'; 392 } 393 394 // Value must not be null. 0 and empty string are valid. Empty strings 395 // are technically against RFC 6265, but known to happen in the wild. 396 $value = $this->getValue(); 397 if ($value === null) { 398 return 'The cookie value must not be empty'; 399 } 400 401 // Domains must not be empty, but can be 0. "0" is not a valid internet 402 // domain, but may be used as server name in a private network. 403 $domain = $this->getDomain(); 404 if ($domain === null || $domain === '') { 405 return 'The cookie domain must not be empty'; 406 } 407 408 return true; 409 } 410} 411