1: <?php
2:
3: /*
4: * This file is part of the Symfony package.
5: *
6: * (c) Fabien Potencier <fabien@symfony.com>
7: *
8: * For the full copyright and license information, please view the LICENSE
9: * file that was distributed with this source code.
10: */
11:
12: namespace Symfony\Component\HttpFoundation;
13:
14: /**
15: * Response represents an HTTP response.
16: *
17: * @author Fabien Potencier <fabien@symfony.com>
18: *
19: * @api
20: */
21: class Response
22: {
23: /**
24: * @var \Symfony\Component\HttpFoundation\ResponseHeaderBag
25: */
26: public $headers;
27:
28: /**
29: * @var string
30: */
31: protected $content;
32:
33: /**
34: * @var string
35: */
36: protected $version;
37:
38: /**
39: * @var integer
40: */
41: protected $statusCode;
42:
43: /**
44: * @var string
45: */
46: protected $statusText;
47:
48: /**
49: * @var string
50: */
51: protected $charset;
52:
53: /**
54: * Status codes translation table.
55: *
56: * The list of codes is complete according to the
57: * {@link http://www.iana.org/assignments/http-status-codes/ Hypertext Transfer Protocol (HTTP) Status Code Registry}
58: * (last updated 2012-02-13).
59: *
60: * Unless otherwise noted, the status code is defined in RFC2616.
61: *
62: * @var array
63: */
64: public static $statusTexts = array(
65: 100 => 'Continue',
66: 101 => 'Switching Protocols',
67: 102 => 'Processing', // RFC2518
68: 200 => 'OK',
69: 201 => 'Created',
70: 202 => 'Accepted',
71: 203 => 'Non-Authoritative Information',
72: 204 => 'No Content',
73: 205 => 'Reset Content',
74: 206 => 'Partial Content',
75: 207 => 'Multi-Status', // RFC4918
76: 208 => 'Already Reported', // RFC5842
77: 226 => 'IM Used', // RFC3229
78: 300 => 'Multiple Choices',
79: 301 => 'Moved Permanently',
80: 302 => 'Found',
81: 303 => 'See Other',
82: 304 => 'Not Modified',
83: 305 => 'Use Proxy',
84: 306 => 'Reserved',
85: 307 => 'Temporary Redirect',
86: 308 => 'Permanent Redirect', // RFC-reschke-http-status-308-07
87: 400 => 'Bad Request',
88: 401 => 'Unauthorized',
89: 402 => 'Payment Required',
90: 403 => 'Forbidden',
91: 404 => 'Not Found',
92: 405 => 'Method Not Allowed',
93: 406 => 'Not Acceptable',
94: 407 => 'Proxy Authentication Required',
95: 408 => 'Request Timeout',
96: 409 => 'Conflict',
97: 410 => 'Gone',
98: 411 => 'Length Required',
99: 412 => 'Precondition Failed',
100: 413 => 'Request Entity Too Large',
101: 414 => 'Request-URI Too Long',
102: 415 => 'Unsupported Media Type',
103: 416 => 'Requested Range Not Satisfiable',
104: 417 => 'Expectation Failed',
105: 418 => 'I\'m a teapot', // RFC2324
106: 422 => 'Unprocessable Entity', // RFC4918
107: 423 => 'Locked', // RFC4918
108: 424 => 'Failed Dependency', // RFC4918
109: 425 => 'Reserved for WebDAV advanced collections expired proposal', // RFC2817
110: 426 => 'Upgrade Required', // RFC2817
111: 428 => 'Precondition Required', // RFC6585
112: 429 => 'Too Many Requests', // RFC6585
113: 431 => 'Request Header Fields Too Large', // RFC6585
114: 500 => 'Internal Server Error',
115: 501 => 'Not Implemented',
116: 502 => 'Bad Gateway',
117: 503 => 'Service Unavailable',
118: 504 => 'Gateway Timeout',
119: 505 => 'HTTP Version Not Supported',
120: 506 => 'Variant Also Negotiates (Experimental)', // RFC2295
121: 507 => 'Insufficient Storage', // RFC4918
122: 508 => 'Loop Detected', // RFC5842
123: 510 => 'Not Extended', // RFC2774
124: 511 => 'Network Authentication Required', // RFC6585
125: );
126:
127: /**
128: * Constructor.
129: *
130: * @param string $content The response content
131: * @param integer $status The response status code
132: * @param array $headers An array of response headers
133: *
134: * @throws \InvalidArgumentException When the HTTP status code is not valid
135: *
136: * @api
137: */
138: public function __construct($content = '', $status = 200, $headers = array())
139: {
140: $this->headers = new ResponseHeaderBag($headers);
141: $this->setContent($content);
142: $this->setStatusCode($status);
143: $this->setProtocolVersion('1.0');
144: if (!$this->headers->has('Date')) {
145: $this->setDate(new \DateTime(null, new \DateTimeZone('UTC')));
146: }
147: }
148:
149: /**
150: * Factory method for chainability
151: *
152: * Example:
153: *
154: * return Response::create($body, 200)
155: * ->setSharedMaxAge(300);
156: *
157: * @param string $content The response content
158: * @param integer $status The response status code
159: * @param array $headers An array of response headers
160: *
161: * @return Response
162: */
163: public static function create($content = '', $status = 200, $headers = array())
164: {
165: return new static($content, $status, $headers);
166: }
167:
168: /**
169: * Returns the Response as an HTTP string.
170: *
171: * The string representation of the Response is the same as the
172: * one that will be sent to the client only if the prepare() method
173: * has been called before.
174: *
175: * @return string The Response as an HTTP string
176: *
177: * @see prepare()
178: */
179: public function __toString()
180: {
181: return
182: sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)."\r\n".
183: $this->headers."\r\n".
184: $this->getContent();
185: }
186:
187: /**
188: * Clones the current Response instance.
189: */
190: public function __clone()
191: {
192: $this->headers = clone $this->headers;
193: }
194:
195: /**
196: * Prepares the Response before it is sent to the client.
197: *
198: * This method tweaks the Response to ensure that it is
199: * compliant with RFC 2616. Most of the changes are based on
200: * the Request that is "associated" with this Response.
201: *
202: * @param Request $request A Request instance
203: *
204: * @return Response The current response.
205: */
206: public function prepare(Request $request)
207: {
208: $headers = $this->headers;
209:
210: if ($this->isInformational() || in_array($this->statusCode, array(204, 304))) {
211: $this->setContent(null);
212: }
213:
214: // Content-type based on the Request
215: if (!$headers->has('Content-Type')) {
216: $format = $request->getRequestFormat();
217: if (null !== $format && $mimeType = $request->getMimeType($format)) {
218: $headers->set('Content-Type', $mimeType);
219: }
220: }
221:
222: // Fix Content-Type
223: $charset = $this->charset ?: 'UTF-8';
224: if (!$headers->has('Content-Type')) {
225: $headers->set('Content-Type', 'text/html; charset='.$charset);
226: } elseif (0 === strpos($headers->get('Content-Type'), 'text/') && false === strpos($headers->get('Content-Type'), 'charset')) {
227: // add the charset
228: $headers->set('Content-Type', $headers->get('Content-Type').'; charset='.$charset);
229: }
230:
231: // Fix Content-Length
232: if ($headers->has('Transfer-Encoding')) {
233: $headers->remove('Content-Length');
234: }
235:
236: if ($request->isMethod('HEAD')) {
237: // cf. RFC2616 14.13
238: $length = $headers->get('Content-Length');
239: $this->setContent(null);
240: if ($length) {
241: $headers->set('Content-Length', $length);
242: }
243: }
244:
245: // Fix protocol
246: if ('HTTP/1.0' != $request->server->get('SERVER_PROTOCOL')) {
247: $this->setProtocolVersion('1.1');
248: }
249:
250: // Check if we need to send extra expire info headers
251: if ('1.0' == $this->getProtocolVersion() && 'no-cache' == $this->headers->get('Cache-Control')) {
252: $this->headers->set('pragma', 'no-cache');
253: $this->headers->set('expires', -1);
254: }
255:
256: /**
257: * Check if we need to remove Cache-Control for ssl encrypted downloads when using IE < 9
258: * @link http://support.microsoft.com/kb/323308
259: */
260: if (false !== stripos($this->headers->get('Content-Disposition'), 'attachment') && preg_match('/MSIE (.*?);/i', $request->server->get('HTTP_USER_AGENT'), $match) == 1 && true === $request->isSecure()) {
261: if (intval(preg_replace("/(MSIE )(.*?);/", "$2", $match[0])) < 9) {
262: $this->headers->remove('Cache-Control');
263: }
264: }
265:
266: return $this;
267: }
268:
269: /**
270: * Sends HTTP headers.
271: *
272: * @return Response
273: */
274: public function sendHeaders()
275: {
276: // headers have already been sent by the developer
277: if (headers_sent()) {
278: return $this;
279: }
280:
281: // status
282: header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText));
283:
284: // headers
285: foreach ($this->headers->allPreserveCase() as $name => $values) {
286: foreach ($values as $value) {
287: header($name.': '.$value, false);
288: }
289: }
290:
291: // cookies
292: foreach ($this->headers->getCookies() as $cookie) {
293: setcookie($cookie->getName(), $cookie->getValue(), $cookie->getExpiresTime(), $cookie->getPath(), $cookie->getDomain(), $cookie->isSecure(), $cookie->isHttpOnly());
294: }
295:
296: return $this;
297: }
298:
299: /**
300: * Sends content for the current web response.
301: *
302: * @return Response
303: */
304: public function sendContent()
305: {
306: echo $this->content;
307:
308: return $this;
309: }
310:
311: /**
312: * Sends HTTP headers and content.
313: *
314: * @return Response
315: *
316: * @api
317: */
318: public function send()
319: {
320: $this->sendHeaders();
321: $this->sendContent();
322:
323: if (function_exists('fastcgi_finish_request')) {
324: fastcgi_finish_request();
325: } elseif ('cli' !== PHP_SAPI) {
326: // ob_get_level() never returns 0 on some Windows configurations, so if
327: // the level is the same two times in a row, the loop should be stopped.
328: $previous = null;
329: $obStatus = ob_get_status(1);
330: while (($level = ob_get_level()) > 0 && $level !== $previous) {
331: $previous = $level;
332: if ($obStatus[$level - 1] && isset($obStatus[$level - 1]['del']) && $obStatus[$level - 1]['del']) {
333: ob_end_flush();
334: }
335: }
336: flush();
337: }
338:
339: return $this;
340: }
341:
342: /**
343: * Sets the response content.
344: *
345: * Valid types are strings, numbers, and objects that implement a __toString() method.
346: *
347: * @param mixed $content
348: *
349: * @return Response
350: *
351: * @throws \UnexpectedValueException
352: *
353: * @api
354: */
355: public function setContent($content)
356: {
357: if (null !== $content && !is_string($content) && !is_numeric($content) && !is_callable(array($content, '__toString'))) {
358: throw new \UnexpectedValueException('The Response content must be a string or object implementing __toString(), "'.gettype($content).'" given.');
359: }
360:
361: $this->content = (string) $content;
362:
363: return $this;
364: }
365:
366: /**
367: * Gets the current response content.
368: *
369: * @return string Content
370: *
371: * @api
372: */
373: public function getContent()
374: {
375: return $this->content;
376: }
377:
378: /**
379: * Sets the HTTP protocol version (1.0 or 1.1).
380: *
381: * @param string $version The HTTP protocol version
382: *
383: * @return Response
384: *
385: * @api
386: */
387: public function setProtocolVersion($version)
388: {
389: $this->version = $version;
390:
391: return $this;
392: }
393:
394: /**
395: * Gets the HTTP protocol version.
396: *
397: * @return string The HTTP protocol version
398: *
399: * @api
400: */
401: public function getProtocolVersion()
402: {
403: return $this->version;
404: }
405:
406: /**
407: * Sets the response status code.
408: *
409: * @param integer $code HTTP status code
410: * @param mixed $text HTTP status text
411: *
412: * If the status text is null it will be automatically populated for the known
413: * status codes and left empty otherwise.
414: *
415: * @return Response
416: *
417: * @throws \InvalidArgumentException When the HTTP status code is not valid
418: *
419: * @api
420: */
421: public function setStatusCode($code, $text = null)
422: {
423: $this->statusCode = $code = (int) $code;
424: if ($this->isInvalid()) {
425: throw new \InvalidArgumentException(sprintf('The HTTP status code "%s" is not valid.', $code));
426: }
427:
428: if (null === $text) {
429: $this->statusText = isset(self::$statusTexts[$code]) ? self::$statusTexts[$code] : '';
430:
431: return $this;
432: }
433:
434: if (false === $text) {
435: $this->statusText = '';
436:
437: return $this;
438: }
439:
440: $this->statusText = $text;
441:
442: return $this;
443: }
444:
445: /**
446: * Retrieves the status code for the current web response.
447: *
448: * @return integer Status code
449: *
450: * @api
451: */
452: public function getStatusCode()
453: {
454: return $this->statusCode;
455: }
456:
457: /**
458: * Sets the response charset.
459: *
460: * @param string $charset Character set
461: *
462: * @return Response
463: *
464: * @api
465: */
466: public function setCharset($charset)
467: {
468: $this->charset = $charset;
469:
470: return $this;
471: }
472:
473: /**
474: * Retrieves the response charset.
475: *
476: * @return string Character set
477: *
478: * @api
479: */
480: public function getCharset()
481: {
482: return $this->charset;
483: }
484:
485: /**
486: * Returns true if the response is worth caching under any circumstance.
487: *
488: * Responses marked "private" with an explicit Cache-Control directive are
489: * considered uncacheable.
490: *
491: * Responses with neither a freshness lifetime (Expires, max-age) nor cache
492: * validator (Last-Modified, ETag) are considered uncacheable.
493: *
494: * @return Boolean true if the response is worth caching, false otherwise
495: *
496: * @api
497: */
498: public function isCacheable()
499: {
500: if (!in_array($this->statusCode, array(200, 203, 300, 301, 302, 404, 410))) {
501: return false;
502: }
503:
504: if ($this->headers->hasCacheControlDirective('no-store') || $this->headers->getCacheControlDirective('private')) {
505: return false;
506: }
507:
508: return $this->isValidateable() || $this->isFresh();
509: }
510:
511: /**
512: * Returns true if the response is "fresh".
513: *
514: * Fresh responses may be served from cache without any interaction with the
515: * origin. A response is considered fresh when it includes a Cache-Control/max-age
516: * indicator or Expires header and the calculated age is less than the freshness lifetime.
517: *
518: * @return Boolean true if the response is fresh, false otherwise
519: *
520: * @api
521: */
522: public function isFresh()
523: {
524: return $this->getTtl() > 0;
525: }
526:
527: /**
528: * Returns true if the response includes headers that can be used to validate
529: * the response with the origin server using a conditional GET request.
530: *
531: * @return Boolean true if the response is validateable, false otherwise
532: *
533: * @api
534: */
535: public function isValidateable()
536: {
537: return $this->headers->has('Last-Modified') || $this->headers->has('ETag');
538: }
539:
540: /**
541: * Marks the response as "private".
542: *
543: * It makes the response ineligible for serving other clients.
544: *
545: * @return Response
546: *
547: * @api
548: */
549: public function setPrivate()
550: {
551: $this->headers->removeCacheControlDirective('public');
552: $this->headers->addCacheControlDirective('private');
553:
554: return $this;
555: }
556:
557: /**
558: * Marks the response as "public".
559: *
560: * It makes the response eligible for serving other clients.
561: *
562: * @return Response
563: *
564: * @api
565: */
566: public function setPublic()
567: {
568: $this->headers->addCacheControlDirective('public');
569: $this->headers->removeCacheControlDirective('private');
570:
571: return $this;
572: }
573:
574: /**
575: * Returns true if the response must be revalidated by caches.
576: *
577: * This method indicates that the response must not be served stale by a
578: * cache in any circumstance without first revalidating with the origin.
579: * When present, the TTL of the response should not be overridden to be
580: * greater than the value provided by the origin.
581: *
582: * @return Boolean true if the response must be revalidated by a cache, false otherwise
583: *
584: * @api
585: */
586: public function mustRevalidate()
587: {
588: return $this->headers->hasCacheControlDirective('must-revalidate') || $this->headers->has('proxy-revalidate');
589: }
590:
591: /**
592: * Returns the Date header as a DateTime instance.
593: *
594: * @return \DateTime A \DateTime instance
595: *
596: * @throws \RuntimeException When the header is not parseable
597: *
598: * @api
599: */
600: public function getDate()
601: {
602: return $this->headers->getDate('Date', new \DateTime());
603: }
604:
605: /**
606: * Sets the Date header.
607: *
608: * @param \DateTime $date A \DateTime instance
609: *
610: * @return Response
611: *
612: * @api
613: */
614: public function setDate(\DateTime $date)
615: {
616: $date->setTimezone(new \DateTimeZone('UTC'));
617: $this->headers->set('Date', $date->format('D, d M Y H:i:s').' GMT');
618:
619: return $this;
620: }
621:
622: /**
623: * Returns the age of the response.
624: *
625: * @return integer The age of the response in seconds
626: */
627: public function getAge()
628: {
629: if (null !== $age = $this->headers->get('Age')) {
630: return (int) $age;
631: }
632:
633: return max(time() - $this->getDate()->format('U'), 0);
634: }
635:
636: /**
637: * Marks the response stale by setting the Age header to be equal to the maximum age of the response.
638: *
639: * @return Response
640: *
641: * @api
642: */
643: public function expire()
644: {
645: if ($this->isFresh()) {
646: $this->headers->set('Age', $this->getMaxAge());
647: }
648:
649: return $this;
650: }
651:
652: /**
653: * Returns the value of the Expires header as a DateTime instance.
654: *
655: * @return \DateTime|null A DateTime instance or null if the header does not exist
656: *
657: * @api
658: */
659: public function getExpires()
660: {
661: try {
662: return $this->headers->getDate('Expires');
663: } catch (\RuntimeException $e) {
664: // according to RFC 2616 invalid date formats (e.g. "0" and "-1") must be treated as in the past
665: return \DateTime::createFromFormat(DATE_RFC2822, 'Sat, 01 Jan 00 00:00:00 +0000');
666: }
667: }
668:
669: /**
670: * Sets the Expires HTTP header with a DateTime instance.
671: *
672: * Passing null as value will remove the header.
673: *
674: * @param \DateTime|null $date A \DateTime instance or null to remove the header
675: *
676: * @return Response
677: *
678: * @api
679: */
680: public function setExpires(\DateTime $date = null)
681: {
682: if (null === $date) {
683: $this->headers->remove('Expires');
684: } else {
685: $date = clone $date;
686: $date->setTimezone(new \DateTimeZone('UTC'));
687: $this->headers->set('Expires', $date->format('D, d M Y H:i:s').' GMT');
688: }
689:
690: return $this;
691: }
692:
693: /**
694: * Returns the number of seconds after the time specified in the response's Date
695: * header when the the response should no longer be considered fresh.
696: *
697: * First, it checks for a s-maxage directive, then a max-age directive, and then it falls
698: * back on an expires header. It returns null when no maximum age can be established.
699: *
700: * @return integer|null Number of seconds
701: *
702: * @api
703: */
704: public function getMaxAge()
705: {
706: if ($this->headers->hasCacheControlDirective('s-maxage')) {
707: return (int) $this->headers->getCacheControlDirective('s-maxage');
708: }
709:
710: if ($this->headers->hasCacheControlDirective('max-age')) {
711: return (int) $this->headers->getCacheControlDirective('max-age');
712: }
713:
714: if (null !== $this->getExpires()) {
715: return $this->getExpires()->format('U') - $this->getDate()->format('U');
716: }
717:
718: return null;
719: }
720:
721: /**
722: * Sets the number of seconds after which the response should no longer be considered fresh.
723: *
724: * This methods sets the Cache-Control max-age directive.
725: *
726: * @param integer $value Number of seconds
727: *
728: * @return Response
729: *
730: * @api
731: */
732: public function setMaxAge($value)
733: {
734: $this->headers->addCacheControlDirective('max-age', $value);
735:
736: return $this;
737: }
738:
739: /**
740: * Sets the number of seconds after which the response should no longer be considered fresh by shared caches.
741: *
742: * This methods sets the Cache-Control s-maxage directive.
743: *
744: * @param integer $value Number of seconds
745: *
746: * @return Response
747: *
748: * @api
749: */
750: public function setSharedMaxAge($value)
751: {
752: $this->setPublic();
753: $this->headers->addCacheControlDirective('s-maxage', $value);
754:
755: return $this;
756: }
757:
758: /**
759: * Returns the response's time-to-live in seconds.
760: *
761: * It returns null when no freshness information is present in the response.
762: *
763: * When the responses TTL is <= 0, the response may not be served from cache without first
764: * revalidating with the origin.
765: *
766: * @return integer|null The TTL in seconds
767: *
768: * @api
769: */
770: public function getTtl()
771: {
772: if (null !== $maxAge = $this->getMaxAge()) {
773: return $maxAge - $this->getAge();
774: }
775:
776: return null;
777: }
778:
779: /**
780: * Sets the response's time-to-live for shared caches.
781: *
782: * This method adjusts the Cache-Control/s-maxage directive.
783: *
784: * @param integer $seconds Number of seconds
785: *
786: * @return Response
787: *
788: * @api
789: */
790: public function setTtl($seconds)
791: {
792: $this->setSharedMaxAge($this->getAge() + $seconds);
793:
794: return $this;
795: }
796:
797: /**
798: * Sets the response's time-to-live for private/client caches.
799: *
800: * This method adjusts the Cache-Control/max-age directive.
801: *
802: * @param integer $seconds Number of seconds
803: *
804: * @return Response
805: *
806: * @api
807: */
808: public function setClientTtl($seconds)
809: {
810: $this->setMaxAge($this->getAge() + $seconds);
811:
812: return $this;
813: }
814:
815: /**
816: * Returns the Last-Modified HTTP header as a DateTime instance.
817: *
818: * @return \DateTime|null A DateTime instance or null if the header does not exist
819: *
820: * @throws \RuntimeException When the HTTP header is not parseable
821: *
822: * @api
823: */
824: public function getLastModified()
825: {
826: return $this->headers->getDate('Last-Modified');
827: }
828:
829: /**
830: * Sets the Last-Modified HTTP header with a DateTime instance.
831: *
832: * Passing null as value will remove the header.
833: *
834: * @param \DateTime|null $date A \DateTime instance or null to remove the header
835: *
836: * @return Response
837: *
838: * @api
839: */
840: public function setLastModified(\DateTime $date = null)
841: {
842: if (null === $date) {
843: $this->headers->remove('Last-Modified');
844: } else {
845: $date = clone $date;
846: $date->setTimezone(new \DateTimeZone('UTC'));
847: $this->headers->set('Last-Modified', $date->format('D, d M Y H:i:s').' GMT');
848: }
849:
850: return $this;
851: }
852:
853: /**
854: * Returns the literal value of the ETag HTTP header.
855: *
856: * @return string|null The ETag HTTP header or null if it does not exist
857: *
858: * @api
859: */
860: public function getEtag()
861: {
862: return $this->headers->get('ETag');
863: }
864:
865: /**
866: * Sets the ETag value.
867: *
868: * @param string|null $etag The ETag unique identifier or null to remove the header
869: * @param Boolean $weak Whether you want a weak ETag or not
870: *
871: * @return Response
872: *
873: * @api
874: */
875: public function setEtag($etag = null, $weak = false)
876: {
877: if (null === $etag) {
878: $this->headers->remove('Etag');
879: } else {
880: if (0 !== strpos($etag, '"')) {
881: $etag = '"'.$etag.'"';
882: }
883:
884: $this->headers->set('ETag', (true === $weak ? 'W/' : '').$etag);
885: }
886:
887: return $this;
888: }
889:
890: /**
891: * Sets the response's cache headers (validation and/or expiration).
892: *
893: * Available options are: etag, last_modified, max_age, s_maxage, private, and public.
894: *
895: * @param array $options An array of cache options
896: *
897: * @return Response
898: *
899: * @throws \InvalidArgumentException
900: *
901: * @api
902: */
903: public function setCache(array $options)
904: {
905: if ($diff = array_diff(array_keys($options), array('etag', 'last_modified', 'max_age', 's_maxage', 'private', 'public'))) {
906: throw new \InvalidArgumentException(sprintf('Response does not support the following options: "%s".', implode('", "', array_values($diff))));
907: }
908:
909: if (isset($options['etag'])) {
910: $this->setEtag($options['etag']);
911: }
912:
913: if (isset($options['last_modified'])) {
914: $this->setLastModified($options['last_modified']);
915: }
916:
917: if (isset($options['max_age'])) {
918: $this->setMaxAge($options['max_age']);
919: }
920:
921: if (isset($options['s_maxage'])) {
922: $this->setSharedMaxAge($options['s_maxage']);
923: }
924:
925: if (isset($options['public'])) {
926: if ($options['public']) {
927: $this->setPublic();
928: } else {
929: $this->setPrivate();
930: }
931: }
932:
933: if (isset($options['private'])) {
934: if ($options['private']) {
935: $this->setPrivate();
936: } else {
937: $this->setPublic();
938: }
939: }
940:
941: return $this;
942: }
943:
944: /**
945: * Modifies the response so that it conforms to the rules defined for a 304 status code.
946: *
947: * This sets the status, removes the body, and discards any headers
948: * that MUST NOT be included in 304 responses.
949: *
950: * @return Response
951: *
952: * @see http://tools.ietf.org/html/rfc2616#section-10.3.5
953: *
954: * @api
955: */
956: public function setNotModified()
957: {
958: $this->setStatusCode(304);
959: $this->setContent(null);
960:
961: // remove headers that MUST NOT be included with 304 Not Modified responses
962: foreach (array('Allow', 'Content-Encoding', 'Content-Language', 'Content-Length', 'Content-MD5', 'Content-Type', 'Last-Modified') as $header) {
963: $this->headers->remove($header);
964: }
965:
966: return $this;
967: }
968:
969: /**
970: * Returns true if the response includes a Vary header.
971: *
972: * @return Boolean true if the response includes a Vary header, false otherwise
973: *
974: * @api
975: */
976: public function hasVary()
977: {
978: return null !== $this->headers->get('Vary');
979: }
980:
981: /**
982: * Returns an array of header names given in the Vary header.
983: *
984: * @return array An array of Vary names
985: *
986: * @api
987: */
988: public function getVary()
989: {
990: if (!$vary = $this->headers->get('Vary')) {
991: return array();
992: }
993:
994: return is_array($vary) ? $vary : preg_split('/[\s,]+/', $vary);
995: }
996:
997: /**
998: * Sets the Vary header.
999: *
1000: * @param string|array $headers
1001: * @param Boolean $replace Whether to replace the actual value of not (true by default)
1002: *
1003: * @return Response
1004: *
1005: * @api
1006: */
1007: public function setVary($headers, $replace = true)
1008: {
1009: $this->headers->set('Vary', $headers, $replace);
1010:
1011: return $this;
1012: }
1013:
1014: /**
1015: * Determines if the Response validators (ETag, Last-Modified) match
1016: * a conditional value specified in the Request.
1017: *
1018: * If the Response is not modified, it sets the status code to 304 and
1019: * removes the actual content by calling the setNotModified() method.
1020: *
1021: * @param Request $request A Request instance
1022: *
1023: * @return Boolean true if the Response validators match the Request, false otherwise
1024: *
1025: * @api
1026: */
1027: public function isNotModified(Request $request)
1028: {
1029: if (!$request->isMethodSafe()) {
1030: return false;
1031: }
1032:
1033: $lastModified = $request->headers->get('If-Modified-Since');
1034: $notModified = false;
1035: if ($etags = $request->getEtags()) {
1036: $notModified = (in_array($this->getEtag(), $etags) || in_array('*', $etags)) && (!$lastModified || $this->headers->get('Last-Modified') == $lastModified);
1037: } elseif ($lastModified) {
1038: $notModified = $lastModified == $this->headers->get('Last-Modified');
1039: }
1040:
1041: if ($notModified) {
1042: $this->setNotModified();
1043: }
1044:
1045: return $notModified;
1046: }
1047:
1048: // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
1049: /**
1050: * Is response invalid?
1051: *
1052: * @return Boolean
1053: *
1054: * @api
1055: */
1056: public function isInvalid()
1057: {
1058: return $this->statusCode < 100 || $this->statusCode >= 600;
1059: }
1060:
1061: /**
1062: * Is response informative?
1063: *
1064: * @return Boolean
1065: *
1066: * @api
1067: */
1068: public function isInformational()
1069: {
1070: return $this->statusCode >= 100 && $this->statusCode < 200;
1071: }
1072:
1073: /**
1074: * Is response successful?
1075: *
1076: * @return Boolean
1077: *
1078: * @api
1079: */
1080: public function isSuccessful()
1081: {
1082: return $this->statusCode >= 200 && $this->statusCode < 300;
1083: }
1084:
1085: /**
1086: * Is the response a redirect?
1087: *
1088: * @return Boolean
1089: *
1090: * @api
1091: */
1092: public function isRedirection()
1093: {
1094: return $this->statusCode >= 300 && $this->statusCode < 400;
1095: }
1096:
1097: /**
1098: * Is there a client error?
1099: *
1100: * @return Boolean
1101: *
1102: * @api
1103: */
1104: public function isClientError()
1105: {
1106: return $this->statusCode >= 400 && $this->statusCode < 500;
1107: }
1108:
1109: /**
1110: * Was there a server side error?
1111: *
1112: * @return Boolean
1113: *
1114: * @api
1115: */
1116: public function isServerError()
1117: {
1118: return $this->statusCode >= 500 && $this->statusCode < 600;
1119: }
1120:
1121: /**
1122: * Is the response OK?
1123: *
1124: * @return Boolean
1125: *
1126: * @api
1127: */
1128: public function isOk()
1129: {
1130: return 200 === $this->statusCode;
1131: }
1132:
1133: /**
1134: * Is the response forbidden?
1135: *
1136: * @return Boolean
1137: *
1138: * @api
1139: */
1140: public function isForbidden()
1141: {
1142: return 403 === $this->statusCode;
1143: }
1144:
1145: /**
1146: * Is the response a not found error?
1147: *
1148: * @return Boolean
1149: *
1150: * @api
1151: */
1152: public function isNotFound()
1153: {
1154: return 404 === $this->statusCode;
1155: }
1156:
1157: /**
1158: * Is the response a redirect of some form?
1159: *
1160: * @param string $location
1161: *
1162: * @return Boolean
1163: *
1164: * @api
1165: */
1166: public function isRedirect($location = null)
1167: {
1168: return in_array($this->statusCode, array(201, 301, 302, 303, 307, 308)) && (null === $location ?: $location == $this->headers->get('Location'));
1169: }
1170:
1171: /**
1172: * Is the response empty?
1173: *
1174: * @return Boolean
1175: *
1176: * @api
1177: */
1178: public function isEmpty()
1179: {
1180: return in_array($this->statusCode, array(201, 204, 304));
1181: }
1182: }
1183: