1<?php 2/** 3 * Zend Framework (http://framework.zend.com/) 4 * 5 * @link http://github.com/zendframework/zf2 for the canonical source repository 6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 7 * @license http://framework.zend.com/license/new-bsd New BSD License 8 */ 9 10namespace Zend\Http\Header; 11 12use DateTime; 13use Zend\Uri\UriFactory; 14 15/** 16 * @throws Exception\InvalidArgumentException 17 * @see http://www.ietf.org/rfc/rfc2109.txt 18 * @see http://www.w3.org/Protocols/rfc2109/rfc2109 19 */ 20class SetCookie implements MultipleHeaderInterface 21{ 22 /** 23 * Cookie name 24 * 25 * @var string|null 26 */ 27 protected $name = null; 28 29 /** 30 * Cookie value 31 * 32 * @var string|null 33 */ 34 protected $value = null; 35 36 /** 37 * Version 38 * 39 * @var int|null 40 */ 41 protected $version = null; 42 43 /** 44 * Max Age 45 * 46 * @var int|null 47 */ 48 protected $maxAge = null; 49 50 /** 51 * Cookie expiry date 52 * 53 * @var int|null 54 */ 55 protected $expires = null; 56 57 /** 58 * Cookie domain 59 * 60 * @var string|null 61 */ 62 protected $domain = null; 63 64 /** 65 * Cookie path 66 * 67 * @var string|null 68 */ 69 protected $path = null; 70 71 /** 72 * Whether the cookie is secure or not 73 * 74 * @var bool|null 75 */ 76 protected $secure = null; 77 78 /** 79 * If the value need to be quoted or not 80 * 81 * @var bool 82 */ 83 protected $quoteFieldValue = false; 84 85 /** 86 * @var bool|null 87 */ 88 protected $httponly = null; 89 90 /** 91 * @static 92 * @throws Exception\InvalidArgumentException 93 * @param $headerLine 94 * @param bool $bypassHeaderFieldName 95 * @return array|SetCookie 96 */ 97 public static function fromString($headerLine, $bypassHeaderFieldName = false) 98 { 99 static $setCookieProcessor = null; 100 101 if ($setCookieProcessor === null) { 102 $setCookieClass = get_called_class(); 103 $setCookieProcessor = function ($headerLine) use ($setCookieClass) { 104 $header = new $setCookieClass; 105 $keyValuePairs = preg_split('#;\s*#', $headerLine); 106 107 foreach ($keyValuePairs as $keyValue) { 108 if (preg_match('#^(?P<headerKey>[^=]+)=\s*("?)(?P<headerValue>[^"]*)\2#', $keyValue, $matches)) { 109 $headerKey = $matches['headerKey']; 110 $headerValue= $matches['headerValue']; 111 } else { 112 $headerKey = $keyValue; 113 $headerValue = null; 114 } 115 116 // First K=V pair is always the cookie name and value 117 if ($header->getName() === null) { 118 $header->setName($headerKey); 119 $header->setValue(urldecode($headerValue)); 120 continue; 121 } 122 123 // Process the remaining elements 124 switch (str_replace(array('-', '_'), '', strtolower($headerKey))) { 125 case 'expires': 126 $header->setExpires($headerValue); 127 break; 128 case 'domain': 129 $header->setDomain($headerValue); 130 break; 131 case 'path': 132 $header->setPath($headerValue); 133 break; 134 case 'secure': 135 $header->setSecure(true); 136 break; 137 case 'httponly': 138 $header->setHttponly(true); 139 break; 140 case 'version': 141 $header->setVersion((int) $headerValue); 142 break; 143 case 'maxage': 144 $header->setMaxAge((int) $headerValue); 145 break; 146 default: 147 // Intentionally omitted 148 } 149 } 150 151 return $header; 152 }; 153 } 154 155 list($name, $value) = GenericHeader::splitHeaderLine($headerLine); 156 HeaderValue::assertValid($value); 157 158 // some sites return set-cookie::value, this is to get rid of the second : 159 $name = (strtolower($name) =='set-cookie:') ? 'set-cookie' : $name; 160 161 // check to ensure proper header type for this factory 162 if (strtolower($name) !== 'set-cookie') { 163 throw new Exception\InvalidArgumentException('Invalid header line for Set-Cookie string: "' . $name . '"'); 164 } 165 166 $multipleHeaders = preg_split('#(?<!Sun|Mon|Tue|Wed|Thu|Fri|Sat),\s*#', $value); 167 168 if (count($multipleHeaders) <= 1) { 169 return $setCookieProcessor(array_pop($multipleHeaders)); 170 } else { 171 $headers = array(); 172 foreach ($multipleHeaders as $headerLine) { 173 $headers[] = $setCookieProcessor($headerLine); 174 } 175 return $headers; 176 } 177 } 178 179 /** 180 * Cookie object constructor 181 * 182 * @todo Add validation of each one of the parameters (legal domain, etc.) 183 * 184 * @param string $name 185 * @param string $value 186 * @param int|string|DateTime $expires 187 * @param string $path 188 * @param string $domain 189 * @param bool $secure 190 * @param bool $httponly 191 * @param string $maxAge 192 * @param int $version 193 */ 194 public function __construct( 195 $name = null, 196 $value = null, 197 $expires = null, 198 $path = null, 199 $domain = null, 200 $secure = false, 201 $httponly = false, 202 $maxAge = null, 203 $version = null 204 ) { 205 $this->type = 'Cookie'; 206 207 $this->setName($name) 208 ->setValue($value) 209 ->setVersion($version) 210 ->setMaxAge($maxAge) 211 ->setDomain($domain) 212 ->setExpires($expires) 213 ->setPath($path) 214 ->setSecure($secure) 215 ->setHttpOnly($httponly); 216 } 217 218 /** 219 * @return string 'Set-Cookie' 220 */ 221 public function getFieldName() 222 { 223 return 'Set-Cookie'; 224 } 225 226 /** 227 * @throws Exception\RuntimeException 228 * @return string 229 */ 230 public function getFieldValue() 231 { 232 if ($this->getName() == '') { 233 return ''; 234 } 235 236 $value = urlencode($this->getValue()); 237 if ($this->hasQuoteFieldValue()) { 238 $value = '"'. $value . '"'; 239 } 240 241 $fieldValue = $this->getName() . '=' . $value; 242 243 $version = $this->getVersion(); 244 if ($version !== null) { 245 $fieldValue .= '; Version=' . $version; 246 } 247 248 $maxAge = $this->getMaxAge(); 249 if ($maxAge!==null) { 250 $fieldValue .= '; Max-Age=' . $maxAge; 251 } 252 253 $expires = $this->getExpires(); 254 if ($expires) { 255 $fieldValue .= '; Expires=' . $expires; 256 } 257 258 $domain = $this->getDomain(); 259 if ($domain) { 260 $fieldValue .= '; Domain=' . $domain; 261 } 262 263 $path = $this->getPath(); 264 if ($path) { 265 $fieldValue .= '; Path=' . $path; 266 } 267 268 if ($this->isSecure()) { 269 $fieldValue .= '; Secure'; 270 } 271 272 if ($this->isHttponly()) { 273 $fieldValue .= '; HttpOnly'; 274 } 275 276 return $fieldValue; 277 } 278 279 /** 280 * @param string $name 281 * @throws Exception\InvalidArgumentException 282 * @return SetCookie 283 */ 284 public function setName($name) 285 { 286 HeaderValue::assertValid($name); 287 $this->name = $name; 288 return $this; 289 } 290 291 /** 292 * @return string 293 */ 294 public function getName() 295 { 296 return $this->name; 297 } 298 299 /** 300 * @param string $value 301 * @return SetCookie 302 */ 303 public function setValue($value) 304 { 305 $this->value = $value; 306 return $this; 307 } 308 309 /** 310 * @return string 311 */ 312 public function getValue() 313 { 314 return $this->value; 315 } 316 317 /** 318 * Set version 319 * 320 * @param int $version 321 * @throws Exception\InvalidArgumentException 322 * @return SetCookie 323 */ 324 public function setVersion($version) 325 { 326 if ($version !== null && !is_int($version)) { 327 throw new Exception\InvalidArgumentException('Invalid Version number specified'); 328 } 329 $this->version = $version; 330 return $this; 331 } 332 333 /** 334 * Get version 335 * 336 * @return int 337 */ 338 public function getVersion() 339 { 340 return $this->version; 341 } 342 343 /** 344 * Set Max-Age 345 * 346 * @param int $maxAge 347 * @throws Exception\InvalidArgumentException 348 * @return SetCookie 349 */ 350 public function setMaxAge($maxAge) 351 { 352 if ($maxAge !== null && (!is_int($maxAge) || ($maxAge < 0))) { 353 throw new Exception\InvalidArgumentException('Invalid Max-Age number specified'); 354 } 355 $this->maxAge = $maxAge; 356 return $this; 357 } 358 359 /** 360 * Get Max-Age 361 * 362 * @return int 363 */ 364 public function getMaxAge() 365 { 366 return $this->maxAge; 367 } 368 369 /** 370 * Set Expires 371 * 372 * @param int|string|DateTime $expires 373 * 374 * @return self 375 * 376 * @throws Exception\InvalidArgumentException 377 */ 378 public function setExpires($expires) 379 { 380 if ($expires === null) { 381 $this->expires = null; 382 return $this; 383 } 384 385 if ($expires instanceof DateTime) { 386 $expires = $expires->format(DateTime::COOKIE); 387 } 388 389 $tsExpires = $expires; 390 391 if (is_string($expires)) { 392 $tsExpires = strtotime($expires); 393 394 // if $tsExpires is invalid and PHP is compiled as 32bit. Check if it fail reason is the 2038 bug 395 if (!is_int($tsExpires) && PHP_INT_SIZE === 4) { 396 $dateTime = new DateTime($expires); 397 if ($dateTime->format('Y') > 2038) { 398 $tsExpires = PHP_INT_MAX; 399 } 400 } 401 } 402 403 if (!is_int($tsExpires) || $tsExpires < 0) { 404 throw new Exception\InvalidArgumentException('Invalid expires time specified'); 405 } 406 407 $this->expires = $tsExpires; 408 409 return $this; 410 } 411 412 /** 413 * @param bool $inSeconds 414 * @return int|string 415 */ 416 public function getExpires($inSeconds = false) 417 { 418 if ($this->expires === null) { 419 return; 420 } 421 if ($inSeconds) { 422 return $this->expires; 423 } 424 return gmdate('D, d-M-Y H:i:s', $this->expires) . ' GMT'; 425 } 426 427 /** 428 * @param string $domain 429 * @return SetCookie 430 */ 431 public function setDomain($domain) 432 { 433 HeaderValue::assertValid($domain); 434 $this->domain = $domain; 435 return $this; 436 } 437 438 /** 439 * @return string 440 */ 441 public function getDomain() 442 { 443 return $this->domain; 444 } 445 446 /** 447 * @param string $path 448 * @return SetCookie 449 */ 450 public function setPath($path) 451 { 452 HeaderValue::assertValid($path); 453 $this->path = $path; 454 return $this; 455 } 456 457 /** 458 * @return string 459 */ 460 public function getPath() 461 { 462 return $this->path; 463 } 464 465 /** 466 * @param bool $secure 467 * @return SetCookie 468 */ 469 public function setSecure($secure) 470 { 471 if (null !== $secure) { 472 $secure = (bool) $secure; 473 } 474 $this->secure = $secure; 475 return $this; 476 } 477 478 /** 479 * Set whether the value for this cookie should be quoted 480 * 481 * @param bool $quotedValue 482 * @return SetCookie 483 */ 484 public function setQuoteFieldValue($quotedValue) 485 { 486 $this->quoteFieldValue = (bool) $quotedValue; 487 return $this; 488 } 489 490 /** 491 * @return bool 492 */ 493 public function isSecure() 494 { 495 return $this->secure; 496 } 497 498 /** 499 * @param bool $httponly 500 * @return SetCookie 501 */ 502 public function setHttponly($httponly) 503 { 504 if (null !== $httponly) { 505 $httponly = (bool) $httponly; 506 } 507 $this->httponly = $httponly; 508 return $this; 509 } 510 511 /** 512 * @return bool 513 */ 514 public function isHttponly() 515 { 516 return $this->httponly; 517 } 518 519 /** 520 * Check whether the cookie has expired 521 * 522 * Always returns false if the cookie is a session cookie (has no expiry time) 523 * 524 * @param int $now Timestamp to consider as "now" 525 * @return bool 526 */ 527 public function isExpired($now = null) 528 { 529 if ($now === null) { 530 $now = time(); 531 } 532 533 if (is_int($this->expires) && $this->expires < $now) { 534 return true; 535 } 536 537 return false; 538 } 539 540 /** 541 * Check whether the cookie is a session cookie (has no expiry time set) 542 * 543 * @return bool 544 */ 545 public function isSessionCookie() 546 { 547 return ($this->expires === null); 548 } 549 550 /** 551 * Check whether the value for this cookie should be quoted 552 * 553 * @return bool 554 */ 555 public function hasQuoteFieldValue() 556 { 557 return $this->quoteFieldValue; 558 } 559 560 public function isValidForRequest($requestDomain, $path, $isSecure = false) 561 { 562 if ($this->getDomain() && (strrpos($requestDomain, $this->getDomain()) === false)) { 563 return false; 564 } 565 566 if ($this->getPath() && (strpos($path, $this->getPath()) !== 0)) { 567 return false; 568 } 569 570 if ($this->secure && $this->isSecure()!==$isSecure) { 571 return false; 572 } 573 574 return true; 575 } 576 577 /** 578 * Checks whether the cookie should be sent or not in a specific scenario 579 * 580 * @param string|\Zend\Uri\Uri $uri URI to check against (secure, domain, path) 581 * @param bool $matchSessionCookies Whether to send session cookies 582 * @param int $now Override the current time when checking for expiry time 583 * @return bool 584 * @throws Exception\InvalidArgumentException If URI does not have HTTP or HTTPS scheme. 585 */ 586 public function match($uri, $matchSessionCookies = true, $now = null) 587 { 588 if (is_string($uri)) { 589 $uri = UriFactory::factory($uri); 590 } 591 592 // Make sure we have a valid Zend_Uri_Http object 593 if (! ($uri->isValid() && ($uri->getScheme() == 'http' || $uri->getScheme() =='https'))) { 594 throw new Exception\InvalidArgumentException('Passed URI is not a valid HTTP or HTTPS URI'); 595 } 596 597 // Check that the cookie is secure (if required) and not expired 598 if ($this->secure && $uri->getScheme() != 'https') { 599 return false; 600 } 601 if ($this->isExpired($now)) { 602 return false; 603 } 604 if ($this->isSessionCookie() && ! $matchSessionCookies) { 605 return false; 606 } 607 608 // Check if the domain matches 609 if (! self::matchCookieDomain($this->getDomain(), $uri->getHost())) { 610 return false; 611 } 612 613 // Check that path matches using prefix match 614 if (! self::matchCookiePath($this->getPath(), $uri->getPath())) { 615 return false; 616 } 617 618 // If we didn't die until now, return true. 619 return true; 620 } 621 622 /** 623 * Check if a cookie's domain matches a host name. 624 * 625 * Used by Zend\Http\Cookies for cookie matching 626 * 627 * @param string $cookieDomain 628 * @param string $host 629 * 630 * @return bool 631 */ 632 public static function matchCookieDomain($cookieDomain, $host) 633 { 634 $cookieDomain = strtolower($cookieDomain); 635 $host = strtolower($host); 636 // Check for either exact match or suffix match 637 return ($cookieDomain == $host || 638 preg_match('/' . preg_quote($cookieDomain) . '$/', $host)); 639 } 640 641 /** 642 * Check if a cookie's path matches a URL path 643 * 644 * Used by Zend\Http\Cookies for cookie matching 645 * 646 * @param string $cookiePath 647 * @param string $path 648 * @return bool 649 */ 650 public static function matchCookiePath($cookiePath, $path) 651 { 652 return (strpos($path, $cookiePath) === 0); 653 } 654 655 public function toString() 656 { 657 return 'Set-Cookie: ' . $this->getFieldValue(); 658 } 659 660 public function toStringMultipleHeaders(array $headers) 661 { 662 $headerLine = $this->toString(); 663 /* @var $header SetCookie */ 664 foreach ($headers as $header) { 665 if (!$header instanceof SetCookie) { 666 throw new Exception\RuntimeException( 667 'The SetCookie multiple header implementation can only accept an array of SetCookie headers' 668 ); 669 } 670 $headerLine .= "\n" . $header->toString(); 671 } 672 return $headerLine; 673 } 674} 675