PluginProbe ʕ •ᴥ•ʔ
GiveWP – Donation Plugin and Fundraising Platform / 2.20.0
GiveWP – Donation Plugin and Fundraising Platform v2.20.0
4.16.3 4.16.2 4.16.1 4.16.0 4.15.5 4.15.4 4.15.3 4.15.2 4.15.1 4.15.0 2.3.0 2.3.1 2.3.2 2.30.0 2.31.0 2.31.1 2.32.0 2.33.0 2.33.1 2.33.2 2.33.3 2.33.4 2.33.5 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.4.7 2.5.0 2.5.1 2.5.10 2.5.11 2.5.12 2.5.13 2.5.2 2.5.3 2.5.4 2.5.5 2.5.6 2.5.7 2.5.8 2.5.9 2.6.0 2.6.1 2.6.2 2.6.3 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.8.0 2.8.1 2.9.0 2.9.1 2.9.2 2.9.3 2.9.4 2.9.5 2.9.6 2.9.7 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.1.2 3.10.0 3.11.0 3.12.0 3.12.1 3.12.2 3.12.3 3.13.0 3.14.0 3.14.1 3.14.2 3.15.0 3.15.1 3.16.0 3.16.1 3.16.2 3.16.3 3.16.4 3.16.5 3.17.0 3.17.1 3.17.2 3.18.0 3.19.0 3.19.1 3.19.2 3.19.3 3.19.4 3.2.0 3.2.1 3.2.2 3.20.0 3.21.0 3.21.1 3.22.0 3.22.1 3.22.2 3.3.0 3.3.1 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 3.6.0 3.6.1 3.6.2 3.7.0 3.8.0 3.9.0 4.0.0 4.1.0 4.1.1 4.10.0 4.10.1 4.11.0 4.12.0 4.13.0 4.13.1 4.13.2 4.14.0 4.14.1 4.14.2 4.14.3 4.14.4 4.14.5 4.14.6 4.2.0 4.2.1 4.3.0 4.3.1 4.3.2 4.4.0 4.5.0 4.6.1 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 trunk 1.9.0 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.10.0 2.10.1 2.10.2 2.10.3 2.10.4 2.11.0 2.11.1 2.11.2 2.11.3 2.12.0 2.12.1 2.12.2 2.12.3 2.13.0 2.13.1 2.13.2 2.13.3 2.13.4 2.14.0 2.15.0 2.16.0 2.16.1 2.17.0 2.17.1 2.17.3 2.18.0 2.18.1 2.19.1 2.19.2 2.19.3 2.19.4 2.19.5 2.19.6 2.19.7 2.19.8 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.20.0 2.20.1 2.20.2 2.21.0 2.21.1 2.21.2 2.21.3 2.21.4 2.22.0 2.22.1 2.22.2 2.22.3 2.23.0 2.23.1 2.23.2 2.24.0 2.24.1 2.24.2 2.25.0 2.25.1 2.25.2 2.25.3 2.26.0 2.27.0 2.27.1 2.27.2 2.27.3 2.28.0 2.29.0 2.29.1 2.29.2
give / vendor / symfony / http-foundation / Response.php
give / vendor / symfony / http-foundation Last commit date
Exception 4 years ago File 4 years ago Session 4 years ago Tests 4 years ago AcceptHeader.php 4 years ago AcceptHeaderItem.php 4 years ago ApacheRequest.php 4 years ago BinaryFileResponse.php 4 years ago Cookie.php 4 years ago ExpressionRequestMatcher.php 4 years ago FileBag.php 4 years ago HeaderBag.php 4 years ago IpUtils.php 4 years ago JsonResponse.php 4 years ago LICENSE 4 years ago ParameterBag.php 4 years ago RedirectResponse.php 4 years ago Request.php 4 years ago RequestMatcher.php 4 years ago RequestMatcherInterface.php 4 years ago RequestStack.php 4 years ago Response.php 4 years ago ResponseHeaderBag.php 4 years ago ServerBag.php 4 years ago StreamedResponse.php 4 years ago
Response.php
1313 lines
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 class Response
20 {
21 const HTTP_CONTINUE = 100;
22 const HTTP_SWITCHING_PROTOCOLS = 101;
23 const HTTP_PROCESSING = 102; // RFC2518
24 const HTTP_EARLY_HINTS = 103; // RFC8297
25 const HTTP_OK = 200;
26 const HTTP_CREATED = 201;
27 const HTTP_ACCEPTED = 202;
28 const HTTP_NON_AUTHORITATIVE_INFORMATION = 203;
29 const HTTP_NO_CONTENT = 204;
30 const HTTP_RESET_CONTENT = 205;
31 const HTTP_PARTIAL_CONTENT = 206;
32 const HTTP_MULTI_STATUS = 207; // RFC4918
33 const HTTP_ALREADY_REPORTED = 208; // RFC5842
34 const HTTP_IM_USED = 226; // RFC3229
35 const HTTP_MULTIPLE_CHOICES = 300;
36 const HTTP_MOVED_PERMANENTLY = 301;
37 const HTTP_FOUND = 302;
38 const HTTP_SEE_OTHER = 303;
39 const HTTP_NOT_MODIFIED = 304;
40 const HTTP_USE_PROXY = 305;
41 const HTTP_RESERVED = 306;
42 const HTTP_TEMPORARY_REDIRECT = 307;
43 const HTTP_PERMANENTLY_REDIRECT = 308; // RFC7238
44 const HTTP_BAD_REQUEST = 400;
45 const HTTP_UNAUTHORIZED = 401;
46 const HTTP_PAYMENT_REQUIRED = 402;
47 const HTTP_FORBIDDEN = 403;
48 const HTTP_NOT_FOUND = 404;
49 const HTTP_METHOD_NOT_ALLOWED = 405;
50 const HTTP_NOT_ACCEPTABLE = 406;
51 const HTTP_PROXY_AUTHENTICATION_REQUIRED = 407;
52 const HTTP_REQUEST_TIMEOUT = 408;
53 const HTTP_CONFLICT = 409;
54 const HTTP_GONE = 410;
55 const HTTP_LENGTH_REQUIRED = 411;
56 const HTTP_PRECONDITION_FAILED = 412;
57 const HTTP_REQUEST_ENTITY_TOO_LARGE = 413;
58 const HTTP_REQUEST_URI_TOO_LONG = 414;
59 const HTTP_UNSUPPORTED_MEDIA_TYPE = 415;
60 const HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
61 const HTTP_EXPECTATION_FAILED = 417;
62 const HTTP_I_AM_A_TEAPOT = 418; // RFC2324
63 const HTTP_MISDIRECTED_REQUEST = 421; // RFC7540
64 const HTTP_UNPROCESSABLE_ENTITY = 422; // RFC4918
65 const HTTP_LOCKED = 423; // RFC4918
66 const HTTP_FAILED_DEPENDENCY = 424; // RFC4918
67
68 /**
69 * @deprecated
70 */
71 const HTTP_RESERVED_FOR_WEBDAV_ADVANCED_COLLECTIONS_EXPIRED_PROPOSAL = 425; // RFC2817
72 const HTTP_TOO_EARLY = 425; // RFC-ietf-httpbis-replay-04
73 const HTTP_UPGRADE_REQUIRED = 426; // RFC2817
74 const HTTP_PRECONDITION_REQUIRED = 428; // RFC6585
75 const HTTP_TOO_MANY_REQUESTS = 429; // RFC6585
76 const HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE = 431; // RFC6585
77 const HTTP_UNAVAILABLE_FOR_LEGAL_REASONS = 451;
78 const HTTP_INTERNAL_SERVER_ERROR = 500;
79 const HTTP_NOT_IMPLEMENTED = 501;
80 const HTTP_BAD_GATEWAY = 502;
81 const HTTP_SERVICE_UNAVAILABLE = 503;
82 const HTTP_GATEWAY_TIMEOUT = 504;
83 const HTTP_VERSION_NOT_SUPPORTED = 505;
84 const HTTP_VARIANT_ALSO_NEGOTIATES_EXPERIMENTAL = 506; // RFC2295
85 const HTTP_INSUFFICIENT_STORAGE = 507; // RFC4918
86 const HTTP_LOOP_DETECTED = 508; // RFC5842
87 const HTTP_NOT_EXTENDED = 510; // RFC2774
88 const HTTP_NETWORK_AUTHENTICATION_REQUIRED = 511; // RFC6585
89
90 /**
91 * @var ResponseHeaderBag
92 */
93 public $headers;
94
95 /**
96 * @var string
97 */
98 protected $content;
99
100 /**
101 * @var string
102 */
103 protected $version;
104
105 /**
106 * @var int
107 */
108 protected $statusCode;
109
110 /**
111 * @var string
112 */
113 protected $statusText;
114
115 /**
116 * @var string
117 */
118 protected $charset;
119
120 /**
121 * Status codes translation table.
122 *
123 * The list of codes is complete according to the
124 * {@link https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml Hypertext Transfer Protocol (HTTP) Status Code Registry}
125 * (last updated 2016-03-01).
126 *
127 * Unless otherwise noted, the status code is defined in RFC2616.
128 *
129 * @var array
130 */
131 public static $statusTexts = [
132 100 => 'Continue',
133 101 => 'Switching Protocols',
134 102 => 'Processing', // RFC2518
135 103 => 'Early Hints',
136 200 => 'OK',
137 201 => 'Created',
138 202 => 'Accepted',
139 203 => 'Non-Authoritative Information',
140 204 => 'No Content',
141 205 => 'Reset Content',
142 206 => 'Partial Content',
143 207 => 'Multi-Status', // RFC4918
144 208 => 'Already Reported', // RFC5842
145 226 => 'IM Used', // RFC3229
146 300 => 'Multiple Choices',
147 301 => 'Moved Permanently',
148 302 => 'Found',
149 303 => 'See Other',
150 304 => 'Not Modified',
151 305 => 'Use Proxy',
152 307 => 'Temporary Redirect',
153 308 => 'Permanent Redirect', // RFC7238
154 400 => 'Bad Request',
155 401 => 'Unauthorized',
156 402 => 'Payment Required',
157 403 => 'Forbidden',
158 404 => 'Not Found',
159 405 => 'Method Not Allowed',
160 406 => 'Not Acceptable',
161 407 => 'Proxy Authentication Required',
162 408 => 'Request Timeout',
163 409 => 'Conflict',
164 410 => 'Gone',
165 411 => 'Length Required',
166 412 => 'Precondition Failed',
167 413 => 'Payload Too Large',
168 414 => 'URI Too Long',
169 415 => 'Unsupported Media Type',
170 416 => 'Range Not Satisfiable',
171 417 => 'Expectation Failed',
172 418 => 'I\'m a teapot', // RFC2324
173 421 => 'Misdirected Request', // RFC7540
174 422 => 'Unprocessable Entity', // RFC4918
175 423 => 'Locked', // RFC4918
176 424 => 'Failed Dependency', // RFC4918
177 425 => 'Too Early', // RFC-ietf-httpbis-replay-04
178 426 => 'Upgrade Required', // RFC2817
179 428 => 'Precondition Required', // RFC6585
180 429 => 'Too Many Requests', // RFC6585
181 431 => 'Request Header Fields Too Large', // RFC6585
182 451 => 'Unavailable For Legal Reasons', // RFC7725
183 500 => 'Internal Server Error',
184 501 => 'Not Implemented',
185 502 => 'Bad Gateway',
186 503 => 'Service Unavailable',
187 504 => 'Gateway Timeout',
188 505 => 'HTTP Version Not Supported',
189 506 => 'Variant Also Negotiates', // RFC2295
190 507 => 'Insufficient Storage', // RFC4918
191 508 => 'Loop Detected', // RFC5842
192 510 => 'Not Extended', // RFC2774
193 511 => 'Network Authentication Required', // RFC6585
194 ];
195
196 /**
197 * @param mixed $content The response content, see setContent()
198 * @param int $status The response status code
199 * @param array $headers An array of response headers
200 *
201 * @throws \InvalidArgumentException When the HTTP status code is not valid
202 */
203 public function __construct($content = '', $status = 200, $headers = [])
204 {
205 $this->headers = new ResponseHeaderBag($headers);
206 $this->setContent($content);
207 $this->setStatusCode($status);
208 $this->setProtocolVersion('1.0');
209 }
210
211 /**
212 * Factory method for chainability.
213 *
214 * Example:
215 *
216 * return Response::create($body, 200)
217 * ->setSharedMaxAge(300);
218 *
219 * @param mixed $content The response content, see setContent()
220 * @param int $status The response status code
221 * @param array $headers An array of response headers
222 *
223 * @return static
224 */
225 public static function create($content = '', $status = 200, $headers = [])
226 {
227 return new static($content, $status, $headers);
228 }
229
230 /**
231 * Returns the Response as an HTTP string.
232 *
233 * The string representation of the Response is the same as the
234 * one that will be sent to the client only if the prepare() method
235 * has been called before.
236 *
237 * @return string The Response as an HTTP string
238 *
239 * @see prepare()
240 */
241 public function __toString()
242 {
243 return
244 sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)."\r\n".
245 $this->headers."\r\n".
246 $this->getContent();
247 }
248
249 /**
250 * Clones the current Response instance.
251 */
252 public function __clone()
253 {
254 $this->headers = clone $this->headers;
255 }
256
257 /**
258 * Prepares the Response before it is sent to the client.
259 *
260 * This method tweaks the Response to ensure that it is
261 * compliant with RFC 2616. Most of the changes are based on
262 * the Request that is "associated" with this Response.
263 *
264 * @return $this
265 */
266 public function prepare(Request $request)
267 {
268 $headers = $this->headers;
269
270 if ($this->isInformational() || $this->isEmpty()) {
271 $this->setContent(null);
272 $headers->remove('Content-Type');
273 $headers->remove('Content-Length');
274 } else {
275 // Content-type based on the Request
276 if (!$headers->has('Content-Type')) {
277 $format = $request->getRequestFormat();
278 if (null !== $format && $mimeType = $request->getMimeType($format)) {
279 $headers->set('Content-Type', $mimeType);
280 }
281 }
282
283 // Fix Content-Type
284 $charset = $this->charset ?: 'UTF-8';
285 if (!$headers->has('Content-Type')) {
286 $headers->set('Content-Type', 'text/html; charset='.$charset);
287 } elseif (0 === stripos($headers->get('Content-Type'), 'text/') && false === stripos($headers->get('Content-Type'), 'charset')) {
288 // add the charset
289 $headers->set('Content-Type', $headers->get('Content-Type').'; charset='.$charset);
290 }
291
292 // Fix Content-Length
293 if ($headers->has('Transfer-Encoding')) {
294 $headers->remove('Content-Length');
295 }
296
297 if ($request->isMethod('HEAD')) {
298 // cf. RFC2616 14.13
299 $length = $headers->get('Content-Length');
300 $this->setContent(null);
301 if ($length) {
302 $headers->set('Content-Length', $length);
303 }
304 }
305 }
306
307 // Fix protocol
308 if ('HTTP/1.0' != $request->server->get('SERVER_PROTOCOL')) {
309 $this->setProtocolVersion('1.1');
310 }
311
312 // Check if we need to send extra expire info headers
313 if ('1.0' == $this->getProtocolVersion() && false !== strpos($headers->get('Cache-Control'), 'no-cache')) {
314 $headers->set('pragma', 'no-cache');
315 $headers->set('expires', -1);
316 }
317
318 $this->ensureIEOverSSLCompatibility($request);
319
320 return $this;
321 }
322
323 /**
324 * Sends HTTP headers.
325 *
326 * @return $this
327 */
328 public function sendHeaders()
329 {
330 // headers have already been sent by the developer
331 if (headers_sent()) {
332 return $this;
333 }
334
335 // headers
336 foreach ($this->headers->allPreserveCaseWithoutCookies() as $name => $values) {
337 $replace = 0 === strcasecmp($name, 'Content-Type');
338 foreach ($values as $value) {
339 header($name.': '.$value, $replace, $this->statusCode);
340 }
341 }
342
343 // cookies
344 foreach ($this->headers->getCookies() as $cookie) {
345 header('Set-Cookie: '.$cookie, false, $this->statusCode);
346 }
347
348 // status
349 header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode);
350
351 return $this;
352 }
353
354 /**
355 * Sends content for the current web response.
356 *
357 * @return $this
358 */
359 public function sendContent()
360 {
361 echo $this->content;
362
363 return $this;
364 }
365
366 /**
367 * Sends HTTP headers and content.
368 *
369 * @return $this
370 */
371 public function send()
372 {
373 $this->sendHeaders();
374 $this->sendContent();
375
376 if (\function_exists('fastcgi_finish_request')) {
377 fastcgi_finish_request();
378 } elseif (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) {
379 static::closeOutputBuffers(0, true);
380 }
381
382 return $this;
383 }
384
385 /**
386 * Sets the response content.
387 *
388 * Valid types are strings, numbers, null, and objects that implement a __toString() method.
389 *
390 * @param mixed $content Content that can be cast to string
391 *
392 * @return $this
393 *
394 * @throws \UnexpectedValueException
395 */
396 public function setContent($content)
397 {
398 if (null !== $content && !\is_string($content) && !is_numeric($content) && !\is_callable([$content, '__toString'])) {
399 throw new \UnexpectedValueException(sprintf('The Response content must be a string or object implementing __toString(), "%s" given.', \gettype($content)));
400 }
401
402 $this->content = (string) $content;
403
404 return $this;
405 }
406
407 /**
408 * Gets the current response content.
409 *
410 * @return string|false
411 */
412 public function getContent()
413 {
414 return $this->content;
415 }
416
417 /**
418 * Sets the HTTP protocol version (1.0 or 1.1).
419 *
420 * @param string $version The HTTP protocol version
421 *
422 * @return $this
423 *
424 * @final since version 3.2
425 */
426 public function setProtocolVersion($version)
427 {
428 $this->version = $version;
429
430 return $this;
431 }
432
433 /**
434 * Gets the HTTP protocol version.
435 *
436 * @return string The HTTP protocol version
437 *
438 * @final since version 3.2
439 */
440 public function getProtocolVersion()
441 {
442 return $this->version;
443 }
444
445 /**
446 * Sets the response status code.
447 *
448 * If the status text is null it will be automatically populated for the known
449 * status codes and left empty otherwise.
450 *
451 * @param int $code HTTP status code
452 * @param mixed $text HTTP status text
453 *
454 * @return $this
455 *
456 * @throws \InvalidArgumentException When the HTTP status code is not valid
457 *
458 * @final since version 3.2
459 */
460 public function setStatusCode($code, $text = null)
461 {
462 $this->statusCode = $code = (int) $code;
463 if ($this->isInvalid()) {
464 throw new \InvalidArgumentException(sprintf('The HTTP status code "%s" is not valid.', $code));
465 }
466
467 if (null === $text) {
468 $this->statusText = isset(self::$statusTexts[$code]) ? self::$statusTexts[$code] : 'unknown status';
469
470 return $this;
471 }
472
473 if (false === $text) {
474 $this->statusText = '';
475
476 return $this;
477 }
478
479 $this->statusText = $text;
480
481 return $this;
482 }
483
484 /**
485 * Retrieves the status code for the current web response.
486 *
487 * @return int Status code
488 *
489 * @final since version 3.2
490 */
491 public function getStatusCode()
492 {
493 return $this->statusCode;
494 }
495
496 /**
497 * Sets the response charset.
498 *
499 * @param string $charset Character set
500 *
501 * @return $this
502 *
503 * @final since version 3.2
504 */
505 public function setCharset($charset)
506 {
507 $this->charset = $charset;
508
509 return $this;
510 }
511
512 /**
513 * Retrieves the response charset.
514 *
515 * @return string Character set
516 *
517 * @final since version 3.2
518 */
519 public function getCharset()
520 {
521 return $this->charset;
522 }
523
524 /**
525 * Returns true if the response may safely be kept in a shared (surrogate) cache.
526 *
527 * Responses marked "private" with an explicit Cache-Control directive are
528 * considered uncacheable.
529 *
530 * Responses with neither a freshness lifetime (Expires, max-age) nor cache
531 * validator (Last-Modified, ETag) are considered uncacheable because there is
532 * no way to tell when or how to remove them from the cache.
533 *
534 * Note that RFC 7231 and RFC 7234 possibly allow for a more permissive implementation,
535 * for example "status codes that are defined as cacheable by default [...]
536 * can be reused by a cache with heuristic expiration unless otherwise indicated"
537 * (https://tools.ietf.org/html/rfc7231#section-6.1)
538 *
539 * @return bool true if the response is worth caching, false otherwise
540 *
541 * @final since version 3.3
542 */
543 public function isCacheable()
544 {
545 if (!\in_array($this->statusCode, [200, 203, 300, 301, 302, 404, 410])) {
546 return false;
547 }
548
549 if ($this->headers->hasCacheControlDirective('no-store') || $this->headers->getCacheControlDirective('private')) {
550 return false;
551 }
552
553 return $this->isValidateable() || $this->isFresh();
554 }
555
556 /**
557 * Returns true if the response is "fresh".
558 *
559 * Fresh responses may be served from cache without any interaction with the
560 * origin. A response is considered fresh when it includes a Cache-Control/max-age
561 * indicator or Expires header and the calculated age is less than the freshness lifetime.
562 *
563 * @return bool true if the response is fresh, false otherwise
564 *
565 * @final since version 3.3
566 */
567 public function isFresh()
568 {
569 return $this->getTtl() > 0;
570 }
571
572 /**
573 * Returns true if the response includes headers that can be used to validate
574 * the response with the origin server using a conditional GET request.
575 *
576 * @return bool true if the response is validateable, false otherwise
577 *
578 * @final since version 3.3
579 */
580 public function isValidateable()
581 {
582 return $this->headers->has('Last-Modified') || $this->headers->has('ETag');
583 }
584
585 /**
586 * Marks the response as "private".
587 *
588 * It makes the response ineligible for serving other clients.
589 *
590 * @return $this
591 *
592 * @final since version 3.2
593 */
594 public function setPrivate()
595 {
596 $this->headers->removeCacheControlDirective('public');
597 $this->headers->addCacheControlDirective('private');
598
599 return $this;
600 }
601
602 /**
603 * Marks the response as "public".
604 *
605 * It makes the response eligible for serving other clients.
606 *
607 * @return $this
608 *
609 * @final since version 3.2
610 */
611 public function setPublic()
612 {
613 $this->headers->addCacheControlDirective('public');
614 $this->headers->removeCacheControlDirective('private');
615
616 return $this;
617 }
618
619 /**
620 * Marks the response as "immutable".
621 *
622 * @param bool $immutable enables or disables the immutable directive
623 *
624 * @return $this
625 *
626 * @final
627 */
628 public function setImmutable($immutable = true)
629 {
630 if ($immutable) {
631 $this->headers->addCacheControlDirective('immutable');
632 } else {
633 $this->headers->removeCacheControlDirective('immutable');
634 }
635
636 return $this;
637 }
638
639 /**
640 * Returns true if the response is marked as "immutable".
641 *
642 * @return bool returns true if the response is marked as "immutable"; otherwise false
643 *
644 * @final
645 */
646 public function isImmutable()
647 {
648 return $this->headers->hasCacheControlDirective('immutable');
649 }
650
651 /**
652 * Returns true if the response must be revalidated by shared caches once it has become stale.
653 *
654 * This method indicates that the response must not be served stale by a
655 * cache in any circumstance without first revalidating with the origin.
656 * When present, the TTL of the response should not be overridden to be
657 * greater than the value provided by the origin.
658 *
659 * @return bool true if the response must be revalidated by a cache, false otherwise
660 *
661 * @final since version 3.3
662 */
663 public function mustRevalidate()
664 {
665 return $this->headers->hasCacheControlDirective('must-revalidate') || $this->headers->hasCacheControlDirective('proxy-revalidate');
666 }
667
668 /**
669 * Returns the Date header as a DateTime instance.
670 *
671 * @return \DateTime A \DateTime instance
672 *
673 * @throws \RuntimeException When the header is not parseable
674 *
675 * @final since version 3.2
676 */
677 public function getDate()
678 {
679 return $this->headers->getDate('Date');
680 }
681
682 /**
683 * Sets the Date header.
684 *
685 * @return $this
686 *
687 * @final since version 3.2
688 */
689 public function setDate(\DateTime $date)
690 {
691 $date->setTimezone(new \DateTimeZone('UTC'));
692 $this->headers->set('Date', $date->format('D, d M Y H:i:s').' GMT');
693
694 return $this;
695 }
696
697 /**
698 * Returns the age of the response.
699 *
700 * @return int The age of the response in seconds
701 *
702 * @final since version 3.2
703 */
704 public function getAge()
705 {
706 if (null !== $age = $this->headers->get('Age')) {
707 return (int) $age;
708 }
709
710 return max(time() - (int) $this->getDate()->format('U'), 0);
711 }
712
713 /**
714 * Marks the response stale by setting the Age header to be equal to the maximum age of the response.
715 *
716 * @return $this
717 */
718 public function expire()
719 {
720 if ($this->isFresh()) {
721 $this->headers->set('Age', $this->getMaxAge());
722 $this->headers->remove('Expires');
723 }
724
725 return $this;
726 }
727
728 /**
729 * Returns the value of the Expires header as a DateTime instance.
730 *
731 * @return \DateTime|null A DateTime instance or null if the header does not exist
732 *
733 * @final since version 3.2
734 */
735 public function getExpires()
736 {
737 try {
738 return $this->headers->getDate('Expires');
739 } catch (\RuntimeException $e) {
740 // according to RFC 2616 invalid date formats (e.g. "0" and "-1") must be treated as in the past
741 return \DateTime::createFromFormat(\DATE_RFC2822, 'Sat, 01 Jan 00 00:00:00 +0000');
742 }
743 }
744
745 /**
746 * Sets the Expires HTTP header with a DateTime instance.
747 *
748 * Passing null as value will remove the header.
749 *
750 * @param \DateTime|null $date A \DateTime instance or null to remove the header
751 *
752 * @return $this
753 *
754 * @final since version 3.2
755 */
756 public function setExpires(\DateTime $date = null)
757 {
758 if (null === $date) {
759 $this->headers->remove('Expires');
760 } else {
761 $date = clone $date;
762 $date->setTimezone(new \DateTimeZone('UTC'));
763 $this->headers->set('Expires', $date->format('D, d M Y H:i:s').' GMT');
764 }
765
766 return $this;
767 }
768
769 /**
770 * Returns the number of seconds after the time specified in the response's Date
771 * header when the response should no longer be considered fresh.
772 *
773 * First, it checks for a s-maxage directive, then a max-age directive, and then it falls
774 * back on an expires header. It returns null when no maximum age can be established.
775 *
776 * @return int|null Number of seconds
777 *
778 * @final since version 3.2
779 */
780 public function getMaxAge()
781 {
782 if ($this->headers->hasCacheControlDirective('s-maxage')) {
783 return (int) $this->headers->getCacheControlDirective('s-maxage');
784 }
785
786 if ($this->headers->hasCacheControlDirective('max-age')) {
787 return (int) $this->headers->getCacheControlDirective('max-age');
788 }
789
790 if (null !== $this->getExpires()) {
791 return (int) $this->getExpires()->format('U') - (int) $this->getDate()->format('U');
792 }
793
794 return null;
795 }
796
797 /**
798 * Sets the number of seconds after which the response should no longer be considered fresh.
799 *
800 * This methods sets the Cache-Control max-age directive.
801 *
802 * @param int $value Number of seconds
803 *
804 * @return $this
805 *
806 * @final since version 3.2
807 */
808 public function setMaxAge($value)
809 {
810 $this->headers->addCacheControlDirective('max-age', $value);
811
812 return $this;
813 }
814
815 /**
816 * Sets the number of seconds after which the response should no longer be considered fresh by shared caches.
817 *
818 * This methods sets the Cache-Control s-maxage directive.
819 *
820 * @param int $value Number of seconds
821 *
822 * @return $this
823 *
824 * @final since version 3.2
825 */
826 public function setSharedMaxAge($value)
827 {
828 $this->setPublic();
829 $this->headers->addCacheControlDirective('s-maxage', $value);
830
831 return $this;
832 }
833
834 /**
835 * Returns the response's time-to-live in seconds.
836 *
837 * It returns null when no freshness information is present in the response.
838 *
839 * When the responses TTL is <= 0, the response may not be served from cache without first
840 * revalidating with the origin.
841 *
842 * @return int|null The TTL in seconds
843 *
844 * @final since version 3.2
845 */
846 public function getTtl()
847 {
848 if (null !== $maxAge = $this->getMaxAge()) {
849 return $maxAge - $this->getAge();
850 }
851
852 return null;
853 }
854
855 /**
856 * Sets the response's time-to-live for shared caches.
857 *
858 * This method adjusts the Cache-Control/s-maxage directive.
859 *
860 * @param int $seconds Number of seconds
861 *
862 * @return $this
863 *
864 * @final since version 3.2
865 */
866 public function setTtl($seconds)
867 {
868 $this->setSharedMaxAge($this->getAge() + $seconds);
869
870 return $this;
871 }
872
873 /**
874 * Sets the response's time-to-live for private/client caches.
875 *
876 * This method adjusts the Cache-Control/max-age directive.
877 *
878 * @param int $seconds Number of seconds
879 *
880 * @return $this
881 *
882 * @final since version 3.2
883 */
884 public function setClientTtl($seconds)
885 {
886 $this->setMaxAge($this->getAge() + $seconds);
887
888 return $this;
889 }
890
891 /**
892 * Returns the Last-Modified HTTP header as a DateTime instance.
893 *
894 * @return \DateTime|null A DateTime instance or null if the header does not exist
895 *
896 * @throws \RuntimeException When the HTTP header is not parseable
897 *
898 * @final since version 3.2
899 */
900 public function getLastModified()
901 {
902 return $this->headers->getDate('Last-Modified');
903 }
904
905 /**
906 * Sets the Last-Modified HTTP header with a DateTime instance.
907 *
908 * Passing null as value will remove the header.
909 *
910 * @param \DateTime|null $date A \DateTime instance or null to remove the header
911 *
912 * @return $this
913 *
914 * @final since version 3.2
915 */
916 public function setLastModified(\DateTime $date = null)
917 {
918 if (null === $date) {
919 $this->headers->remove('Last-Modified');
920 } else {
921 $date = clone $date;
922 $date->setTimezone(new \DateTimeZone('UTC'));
923 $this->headers->set('Last-Modified', $date->format('D, d M Y H:i:s').' GMT');
924 }
925
926 return $this;
927 }
928
929 /**
930 * Returns the literal value of the ETag HTTP header.
931 *
932 * @return string|null The ETag HTTP header or null if it does not exist
933 *
934 * @final since version 3.2
935 */
936 public function getEtag()
937 {
938 return $this->headers->get('ETag');
939 }
940
941 /**
942 * Sets the ETag value.
943 *
944 * @param string|null $etag The ETag unique identifier or null to remove the header
945 * @param bool $weak Whether you want a weak ETag or not
946 *
947 * @return $this
948 *
949 * @final since version 3.2
950 */
951 public function setEtag($etag = null, $weak = false)
952 {
953 if (null === $etag) {
954 $this->headers->remove('Etag');
955 } else {
956 if (0 !== strpos($etag, '"')) {
957 $etag = '"'.$etag.'"';
958 }
959
960 $this->headers->set('ETag', (true === $weak ? 'W/' : '').$etag);
961 }
962
963 return $this;
964 }
965
966 /**
967 * Sets the response's cache headers (validation and/or expiration).
968 *
969 * Available options are: etag, last_modified, max_age, s_maxage, private, public and immutable.
970 *
971 * @param array $options An array of cache options
972 *
973 * @return $this
974 *
975 * @throws \InvalidArgumentException
976 *
977 * @final since version 3.3
978 */
979 public function setCache(array $options)
980 {
981 if ($diff = array_diff(array_keys($options), ['etag', 'last_modified', 'max_age', 's_maxage', 'private', 'public', 'immutable'])) {
982 throw new \InvalidArgumentException(sprintf('Response does not support the following options: "%s".', implode('", "', $diff)));
983 }
984
985 if (isset($options['etag'])) {
986 $this->setEtag($options['etag']);
987 }
988
989 if (isset($options['last_modified'])) {
990 $this->setLastModified($options['last_modified']);
991 }
992
993 if (isset($options['max_age'])) {
994 $this->setMaxAge($options['max_age']);
995 }
996
997 if (isset($options['s_maxage'])) {
998 $this->setSharedMaxAge($options['s_maxage']);
999 }
1000
1001 if (isset($options['public'])) {
1002 if ($options['public']) {
1003 $this->setPublic();
1004 } else {
1005 $this->setPrivate();
1006 }
1007 }
1008
1009 if (isset($options['private'])) {
1010 if ($options['private']) {
1011 $this->setPrivate();
1012 } else {
1013 $this->setPublic();
1014 }
1015 }
1016
1017 if (isset($options['immutable'])) {
1018 $this->setImmutable((bool) $options['immutable']);
1019 }
1020
1021 return $this;
1022 }
1023
1024 /**
1025 * Modifies the response so that it conforms to the rules defined for a 304 status code.
1026 *
1027 * This sets the status, removes the body, and discards any headers
1028 * that MUST NOT be included in 304 responses.
1029 *
1030 * @return $this
1031 *
1032 * @see https://tools.ietf.org/html/rfc2616#section-10.3.5
1033 *
1034 * @final since version 3.3
1035 */
1036 public function setNotModified()
1037 {
1038 $this->setStatusCode(304);
1039 $this->setContent(null);
1040
1041 // remove headers that MUST NOT be included with 304 Not Modified responses
1042 foreach (['Allow', 'Content-Encoding', 'Content-Language', 'Content-Length', 'Content-MD5', 'Content-Type', 'Last-Modified'] as $header) {
1043 $this->headers->remove($header);
1044 }
1045
1046 return $this;
1047 }
1048
1049 /**
1050 * Returns true if the response includes a Vary header.
1051 *
1052 * @return bool true if the response includes a Vary header, false otherwise
1053 *
1054 * @final since version 3.2
1055 */
1056 public function hasVary()
1057 {
1058 return null !== $this->headers->get('Vary');
1059 }
1060
1061 /**
1062 * Returns an array of header names given in the Vary header.
1063 *
1064 * @return array An array of Vary names
1065 *
1066 * @final since version 3.2
1067 */
1068 public function getVary()
1069 {
1070 if (!$vary = $this->headers->get('Vary', null, false)) {
1071 return [];
1072 }
1073
1074 $ret = [];
1075 foreach ($vary as $item) {
1076 $ret = array_merge($ret, preg_split('/[\s,]+/', $item));
1077 }
1078
1079 return $ret;
1080 }
1081
1082 /**
1083 * Sets the Vary header.
1084 *
1085 * @param string|array $headers
1086 * @param bool $replace Whether to replace the actual value or not (true by default)
1087 *
1088 * @return $this
1089 *
1090 * @final since version 3.2
1091 */
1092 public function setVary($headers, $replace = true)
1093 {
1094 $this->headers->set('Vary', $headers, $replace);
1095
1096 return $this;
1097 }
1098
1099 /**
1100 * Determines if the Response validators (ETag, Last-Modified) match
1101 * a conditional value specified in the Request.
1102 *
1103 * If the Response is not modified, it sets the status code to 304 and
1104 * removes the actual content by calling the setNotModified() method.
1105 *
1106 * @return bool true if the Response validators match the Request, false otherwise
1107 *
1108 * @final since version 3.3
1109 */
1110 public function isNotModified(Request $request)
1111 {
1112 if (!$request->isMethodCacheable()) {
1113 return false;
1114 }
1115
1116 $notModified = false;
1117 $lastModified = $this->headers->get('Last-Modified');
1118 $modifiedSince = $request->headers->get('If-Modified-Since');
1119
1120 if ($etags = $request->getETags()) {
1121 $notModified = \in_array($this->getEtag(), $etags) || \in_array('*', $etags);
1122 }
1123
1124 if ($modifiedSince && $lastModified) {
1125 $notModified = strtotime($modifiedSince) >= strtotime($lastModified) && (!$etags || $notModified);
1126 }
1127
1128 if ($notModified) {
1129 $this->setNotModified();
1130 }
1131
1132 return $notModified;
1133 }
1134
1135 /**
1136 * Is response invalid?
1137 *
1138 * @return bool
1139 *
1140 * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
1141 *
1142 * @final since version 3.2
1143 */
1144 public function isInvalid()
1145 {
1146 return $this->statusCode < 100 || $this->statusCode >= 600;
1147 }
1148
1149 /**
1150 * Is response informative?
1151 *
1152 * @return bool
1153 *
1154 * @final since version 3.3
1155 */
1156 public function isInformational()
1157 {
1158 return $this->statusCode >= 100 && $this->statusCode < 200;
1159 }
1160
1161 /**
1162 * Is response successful?
1163 *
1164 * @return bool
1165 *
1166 * @final since version 3.2
1167 */
1168 public function isSuccessful()
1169 {
1170 return $this->statusCode >= 200 && $this->statusCode < 300;
1171 }
1172
1173 /**
1174 * Is the response a redirect?
1175 *
1176 * @return bool
1177 *
1178 * @final since version 3.2
1179 */
1180 public function isRedirection()
1181 {
1182 return $this->statusCode >= 300 && $this->statusCode < 400;
1183 }
1184
1185 /**
1186 * Is there a client error?
1187 *
1188 * @return bool
1189 *
1190 * @final since version 3.2
1191 */
1192 public function isClientError()
1193 {
1194 return $this->statusCode >= 400 && $this->statusCode < 500;
1195 }
1196
1197 /**
1198 * Was there a server side error?
1199 *
1200 * @return bool
1201 *
1202 * @final since version 3.3
1203 */
1204 public function isServerError()
1205 {
1206 return $this->statusCode >= 500 && $this->statusCode < 600;
1207 }
1208
1209 /**
1210 * Is the response OK?
1211 *
1212 * @return bool
1213 *
1214 * @final since version 3.2
1215 */
1216 public function isOk()
1217 {
1218 return 200 === $this->statusCode;
1219 }
1220
1221 /**
1222 * Is the response forbidden?
1223 *
1224 * @return bool
1225 *
1226 * @final since version 3.2
1227 */
1228 public function isForbidden()
1229 {
1230 return 403 === $this->statusCode;
1231 }
1232
1233 /**
1234 * Is the response a not found error?
1235 *
1236 * @return bool
1237 *
1238 * @final since version 3.2
1239 */
1240 public function isNotFound()
1241 {
1242 return 404 === $this->statusCode;
1243 }
1244
1245 /**
1246 * Is the response a redirect of some form?
1247 *
1248 * @param string $location
1249 *
1250 * @return bool
1251 *
1252 * @final since version 3.2
1253 */
1254 public function isRedirect($location = null)
1255 {
1256 return \in_array($this->statusCode, [201, 301, 302, 303, 307, 308]) && (null === $location ?: $location == $this->headers->get('Location'));
1257 }
1258
1259 /**
1260 * Is the response empty?
1261 *
1262 * @return bool
1263 *
1264 * @final since version 3.2
1265 */
1266 public function isEmpty()
1267 {
1268 return \in_array($this->statusCode, [204, 304]);
1269 }
1270
1271 /**
1272 * Cleans or flushes output buffers up to target level.
1273 *
1274 * Resulting level can be greater than target level if a non-removable buffer has been encountered.
1275 *
1276 * @param int $targetLevel The target output buffering level
1277 * @param bool $flush Whether to flush or clean the buffers
1278 *
1279 * @final since version 3.3
1280 */
1281 public static function closeOutputBuffers($targetLevel, $flush)
1282 {
1283 $status = ob_get_status(true);
1284 $level = \count($status);
1285 // PHP_OUTPUT_HANDLER_* are not defined on HHVM 3.3
1286 $flags = \defined('PHP_OUTPUT_HANDLER_REMOVABLE') ? \PHP_OUTPUT_HANDLER_REMOVABLE | ($flush ? \PHP_OUTPUT_HANDLER_FLUSHABLE : \PHP_OUTPUT_HANDLER_CLEANABLE) : -1;
1287
1288 while ($level-- > $targetLevel && ($s = $status[$level]) && (!isset($s['del']) ? !isset($s['flags']) || ($s['flags'] & $flags) === $flags : $s['del'])) {
1289 if ($flush) {
1290 ob_end_flush();
1291 } else {
1292 ob_end_clean();
1293 }
1294 }
1295 }
1296
1297 /**
1298 * Checks if we need to remove Cache-Control for SSL encrypted downloads when using IE < 9.
1299 *
1300 * @see http://support.microsoft.com/kb/323308
1301 *
1302 * @final since version 3.3
1303 */
1304 protected function ensureIEOverSSLCompatibility(Request $request)
1305 {
1306 if (false !== stripos($this->headers->get('Content-Disposition'), 'attachment') && 1 == preg_match('/MSIE (.*?);/i', $request->server->get('HTTP_USER_AGENT'), $match) && true === $request->isSecure()) {
1307 if ((int) preg_replace('/(MSIE )(.*?);/', '$2', $match[0]) < 9) {
1308 $this->headers->remove('Cache-Control');
1309 }
1310 }
1311 }
1312 }
1313