1 <?php 2 3 namespace GuzzleHttp\Cookie; 4 5 /** 6 * Set-Cookie object 7 */ 8 class 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 if (!is_string($name)) { 132 trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); 133 } 134 135 $this->data['Name'] = (string) $name; 136 } 137 138 /** 139 * Get the cookie value. 140 * 141 * @return string|null 142 */ 143 public function getValue() 144 { 145 return $this->data['Value']; 146 } 147 148 /** 149 * Set the cookie value. 150 * 151 * @param string $value Cookie value 152 */ 153 public function setValue($value): void 154 { 155 if (!is_string($value)) { 156 trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); 157 } 158 159 $this->data['Value'] = (string) $value; 160 } 161 162 /** 163 * Get the domain. 164 * 165 * @return string|null 166 */ 167 public function getDomain() 168 { 169 return $this->data['Domain']; 170 } 171 172 /** 173 * Set the domain of the cookie. 174 * 175 * @param string|null $domain 176 */ 177 public function setDomain($domain): void 178 { 179 if (!is_string($domain) && null !== $domain) { 180 trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string or null to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); 181 } 182 183 $this->data['Domain'] = null === $domain ? null : (string) $domain; 184 } 185 186 /** 187 * Get the path. 188 * 189 * @return string 190 */ 191 public function getPath() 192 { 193 return $this->data['Path']; 194 } 195 196 /** 197 * Set the path of the cookie. 198 * 199 * @param string $path Path of the cookie 200 */ 201 public function setPath($path): void 202 { 203 if (!is_string($path)) { 204 trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); 205 } 206 207 $this->data['Path'] = (string) $path; 208 } 209 210 /** 211 * Maximum lifetime of the cookie in seconds. 212 * 213 * @return int|null 214 */ 215 public function getMaxAge() 216 { 217 return null === $this->data['Max-Age'] ? null : (int) $this->data['Max-Age']; 218 } 219 220 /** 221 * Set the max-age of the cookie. 222 * 223 * @param int|null $maxAge Max age of the cookie in seconds 224 */ 225 public function setMaxAge($maxAge): void 226 { 227 if (!is_int($maxAge) && null !== $maxAge) { 228 trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing an int or null to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); 229 } 230 231 $this->data['Max-Age'] = $maxAge === null ? null : (int) $maxAge; 232 } 233 234 /** 235 * The UNIX timestamp when the cookie Expires. 236 * 237 * @return string|int|null 238 */ 239 public function getExpires() 240 { 241 return $this->data['Expires']; 242 } 243 244 /** 245 * Set the unix timestamp for which the cookie will expire. 246 * 247 * @param int|string|null $timestamp Unix timestamp or any English textual datetime description. 248 */ 249 public function setExpires($timestamp): void 250 { 251 if (!is_int($timestamp) && !is_string($timestamp) && null !== $timestamp) { 252 trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing an int, string or null to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); 253 } 254 255 $this->data['Expires'] = null === $timestamp ? null : (\is_numeric($timestamp) ? (int) $timestamp : \strtotime((string) $timestamp)); 256 } 257 258 /** 259 * Get whether or not this is a secure cookie. 260 * 261 * @return bool 262 */ 263 public function getSecure() 264 { 265 return $this->data['Secure']; 266 } 267 268 /** 269 * Set whether or not the cookie is secure. 270 * 271 * @param bool $secure Set to true or false if secure 272 */ 273 public function setSecure($secure): void 274 { 275 if (!is_bool($secure)) { 276 trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a bool to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); 277 } 278 279 $this->data['Secure'] = (bool) $secure; 280 } 281 282 /** 283 * Get whether or not this is a session cookie. 284 * 285 * @return bool|null 286 */ 287 public function getDiscard() 288 { 289 return $this->data['Discard']; 290 } 291 292 /** 293 * Set whether or not this is a session cookie. 294 * 295 * @param bool $discard Set to true or false if this is a session cookie 296 */ 297 public function setDiscard($discard): void 298 { 299 if (!is_bool($discard)) { 300 trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a bool to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); 301 } 302 303 $this->data['Discard'] = (bool) $discard; 304 } 305 306 /** 307 * Get whether or not this is an HTTP only cookie. 308 * 309 * @return bool 310 */ 311 public function getHttpOnly() 312 { 313 return $this->data['HttpOnly']; 314 } 315 316 /** 317 * Set whether or not this is an HTTP only cookie. 318 * 319 * @param bool $httpOnly Set to true or false if this is HTTP only 320 */ 321 public function setHttpOnly($httpOnly): void 322 { 323 if (!is_bool($httpOnly)) { 324 trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a bool to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); 325 } 326 327 $this->data['HttpOnly'] = (bool) $httpOnly; 328 } 329 330 /** 331 * Check if the cookie matches a path value. 332 * 333 * A request-path path-matches a given cookie-path if at least one of 334 * the following conditions holds: 335 * 336 * - The cookie-path and the request-path are identical. 337 * - The cookie-path is a prefix of the request-path, and the last 338 * character of the cookie-path is %x2F ("/"). 339 * - The cookie-path is a prefix of the request-path, and the first 340 * character of the request-path that is not included in the cookie- 341 * path is a %x2F ("/") character. 342 * 343 * @param string $requestPath Path to check against 344 */ 345 public function matchesPath(string $requestPath): bool 346 { 347 $cookiePath = $this->getPath(); 348 349 // Match on exact matches or when path is the default empty "/" 350 if ($cookiePath === '/' || $cookiePath == $requestPath) { 351 return true; 352 } 353 354 // Ensure that the cookie-path is a prefix of the request path. 355 if (0 !== \strpos($requestPath, $cookiePath)) { 356 return false; 357 } 358 359 // Match if the last character of the cookie-path is "/" 360 if (\substr($cookiePath, -1, 1) === '/') { 361 return true; 362 } 363 364 // Match if the first character not included in cookie path is "/" 365 return \substr($requestPath, \strlen($cookiePath), 1) === '/'; 366 } 367 368 /** 369 * Check if the cookie matches a domain value. 370 * 371 * @param string $domain Domain to check against 372 */ 373 public function matchesDomain(string $domain): bool 374 { 375 $cookieDomain = $this->getDomain(); 376 if (null === $cookieDomain) { 377 return true; 378 } 379 380 // Remove the leading '.' as per spec in RFC 6265. 381 // https://tools.ietf.org/html/rfc6265#section-5.2.3 382 $cookieDomain = \ltrim(\strtolower($cookieDomain), '.'); 383 384 $domain = \strtolower($domain); 385 386 // Domain not set or exact match. 387 if ('' === $cookieDomain || $domain === $cookieDomain) { 388 return true; 389 } 390 391 // Matching the subdomain according to RFC 6265. 392 // https://tools.ietf.org/html/rfc6265#section-5.1.3 393 if (\filter_var($domain, \FILTER_VALIDATE_IP)) { 394 return false; 395 } 396 397 return (bool) \preg_match('/\.' . \preg_quote($cookieDomain, '/') . '$/', $domain); 398 } 399 400 /** 401 * Check if the cookie is expired. 402 */ 403 public function isExpired(): bool 404 { 405 return $this->getExpires() !== null && \time() > $this->getExpires(); 406 } 407 408 /** 409 * Check if the cookie is valid according to RFC 6265. 410 * 411 * @return bool|string Returns true if valid or an error message if invalid 412 */ 413 public function validate() 414 { 415 $name = $this->getName(); 416 if ($name === '') { 417 return 'The cookie name must not be empty'; 418 } 419 420 // Check if any of the invalid characters are present in the cookie name 421 if (\preg_match( 422 '/[\x00-\x20\x22\x28-\x29\x2c\x2f\x3a-\x40\x5c\x7b\x7d\x7f]/', 423 $name 424 )) { 425 return 'Cookie name must not contain invalid characters: ASCII ' 426 . 'Control characters (0-31;127), space, tab and the ' 427 . 'following characters: ()<>@,;:\"/?={}'; 428 } 429 430 // Value must not be null. 0 and empty string are valid. Empty strings 431 // are technically against RFC 6265, but known to happen in the wild. 432 $value = $this->getValue(); 433 if ($value === null) { 434 return 'The cookie value must not be empty'; 435 } 436 437 // Domains must not be empty, but can be 0. "0" is not a valid internet 438 // domain, but may be used as server name in a private network. 439 $domain = $this->getDomain(); 440 if ($domain === null || $domain === '') { 441 return 'The cookie domain must not be empty'; 442 } 443 444 return true; 445 } 446 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body