PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.2.0
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.2.0
5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / vendor / prefixed / symfony / http-foundation / Response.php
matomo / app / vendor / prefixed / symfony / http-foundation Last commit date
Exception 2 years ago File 1 year ago RateLimiter 2 years ago Session 1 year ago Test 1 year ago AcceptHeader.php 1 year ago AcceptHeaderItem.php 2 years ago BinaryFileResponse.php 1 year ago Cookie.php 1 year ago ExpressionRequestMatcher.php 2 years ago FileBag.php 1 year ago HeaderBag.php 1 year ago HeaderUtils.php 1 year ago InputBag.php 2 years ago IpUtils.php 1 year ago JsonResponse.php 1 year ago LICENSE 2 years ago ParameterBag.php 1 year ago README.md 2 years ago RedirectResponse.php 2 years ago Request.php 1 year ago RequestMatcher.php 1 year ago RequestMatcherInterface.php 2 years ago RequestStack.php 2 years ago Response.php 1 year ago ResponseHeaderBag.php 1 year ago ServerBag.php 1 year ago StreamedResponse.php 1 year ago UrlHelper.php 2 years ago
Response.php
1169 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 namespace Matomo\Dependencies\Symfony\Component\HttpFoundation;
12
13 // Help opcache.preload discover always-needed symbols
14 class_exists(ResponseHeaderBag::class);
15 /**
16 * Response represents an HTTP response.
17 *
18 * @author Fabien Potencier <fabien@symfony.com>
19 */
20 class Response
21 {
22 public const HTTP_CONTINUE = 100;
23 public const HTTP_SWITCHING_PROTOCOLS = 101;
24 public const HTTP_PROCESSING = 102;
25 // RFC2518
26 public const HTTP_EARLY_HINTS = 103;
27 // RFC8297
28 public const HTTP_OK = 200;
29 public const HTTP_CREATED = 201;
30 public const HTTP_ACCEPTED = 202;
31 public const HTTP_NON_AUTHORITATIVE_INFORMATION = 203;
32 public const HTTP_NO_CONTENT = 204;
33 public const HTTP_RESET_CONTENT = 205;
34 public const HTTP_PARTIAL_CONTENT = 206;
35 public const HTTP_MULTI_STATUS = 207;
36 // RFC4918
37 public const HTTP_ALREADY_REPORTED = 208;
38 // RFC5842
39 public const HTTP_IM_USED = 226;
40 // RFC3229
41 public const HTTP_MULTIPLE_CHOICES = 300;
42 public const HTTP_MOVED_PERMANENTLY = 301;
43 public const HTTP_FOUND = 302;
44 public const HTTP_SEE_OTHER = 303;
45 public const HTTP_NOT_MODIFIED = 304;
46 public const HTTP_USE_PROXY = 305;
47 public const HTTP_RESERVED = 306;
48 public const HTTP_TEMPORARY_REDIRECT = 307;
49 public const HTTP_PERMANENTLY_REDIRECT = 308;
50 // RFC7238
51 public const HTTP_BAD_REQUEST = 400;
52 public const HTTP_UNAUTHORIZED = 401;
53 public const HTTP_PAYMENT_REQUIRED = 402;
54 public const HTTP_FORBIDDEN = 403;
55 public const HTTP_NOT_FOUND = 404;
56 public const HTTP_METHOD_NOT_ALLOWED = 405;
57 public const HTTP_NOT_ACCEPTABLE = 406;
58 public const HTTP_PROXY_AUTHENTICATION_REQUIRED = 407;
59 public const HTTP_REQUEST_TIMEOUT = 408;
60 public const HTTP_CONFLICT = 409;
61 public const HTTP_GONE = 410;
62 public const HTTP_LENGTH_REQUIRED = 411;
63 public const HTTP_PRECONDITION_FAILED = 412;
64 public const HTTP_REQUEST_ENTITY_TOO_LARGE = 413;
65 public const HTTP_REQUEST_URI_TOO_LONG = 414;
66 public const HTTP_UNSUPPORTED_MEDIA_TYPE = 415;
67 public const HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
68 public const HTTP_EXPECTATION_FAILED = 417;
69 public const HTTP_I_AM_A_TEAPOT = 418;
70 // RFC2324
71 public const HTTP_MISDIRECTED_REQUEST = 421;
72 // RFC7540
73 public const HTTP_UNPROCESSABLE_ENTITY = 422;
74 // RFC4918
75 public const HTTP_LOCKED = 423;
76 // RFC4918
77 public const HTTP_FAILED_DEPENDENCY = 424;
78 // RFC4918
79 public const HTTP_TOO_EARLY = 425;
80 // RFC-ietf-httpbis-replay-04
81 public const HTTP_UPGRADE_REQUIRED = 426;
82 // RFC2817
83 public const HTTP_PRECONDITION_REQUIRED = 428;
84 // RFC6585
85 public const HTTP_TOO_MANY_REQUESTS = 429;
86 // RFC6585
87 public const HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE = 431;
88 // RFC6585
89 public const HTTP_UNAVAILABLE_FOR_LEGAL_REASONS = 451;
90 // RFC7725
91 public const HTTP_INTERNAL_SERVER_ERROR = 500;
92 public const HTTP_NOT_IMPLEMENTED = 501;
93 public const HTTP_BAD_GATEWAY = 502;
94 public const HTTP_SERVICE_UNAVAILABLE = 503;
95 public const HTTP_GATEWAY_TIMEOUT = 504;
96 public const HTTP_VERSION_NOT_SUPPORTED = 505;
97 public const HTTP_VARIANT_ALSO_NEGOTIATES_EXPERIMENTAL = 506;
98 // RFC2295
99 public const HTTP_INSUFFICIENT_STORAGE = 507;
100 // RFC4918
101 public const HTTP_LOOP_DETECTED = 508;
102 // RFC5842
103 public const HTTP_NOT_EXTENDED = 510;
104 // RFC2774
105 public const HTTP_NETWORK_AUTHENTICATION_REQUIRED = 511;
106 // RFC6585
107 /**
108 * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
109 */
110 private const HTTP_RESPONSE_CACHE_CONTROL_DIRECTIVES = ['must_revalidate' => \false, 'no_cache' => \false, 'no_store' => \false, 'no_transform' => \false, 'public' => \false, 'private' => \false, 'proxy_revalidate' => \false, 'max_age' => \true, 's_maxage' => \true, 'immutable' => \false, 'last_modified' => \true, 'etag' => \true];
111 /**
112 * @var ResponseHeaderBag
113 */
114 public $headers;
115 /**
116 * @var string
117 */
118 protected $content;
119 /**
120 * @var string
121 */
122 protected $version;
123 /**
124 * @var int
125 */
126 protected $statusCode;
127 /**
128 * @var string
129 */
130 protected $statusText;
131 /**
132 * @var string
133 */
134 protected $charset;
135 /**
136 * Status codes translation table.
137 *
138 * The list of codes is complete according to the
139 * {@link https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml Hypertext Transfer Protocol (HTTP) Status Code Registry}
140 * (last updated 2021-10-01).
141 *
142 * Unless otherwise noted, the status code is defined in RFC2616.
143 *
144 * @var array
145 */
146 public static $statusTexts = [
147 100 => 'Continue',
148 101 => 'Switching Protocols',
149 102 => 'Processing',
150 // RFC2518
151 103 => 'Early Hints',
152 200 => 'OK',
153 201 => 'Created',
154 202 => 'Accepted',
155 203 => 'Non-Authoritative Information',
156 204 => 'No Content',
157 205 => 'Reset Content',
158 206 => 'Partial Content',
159 207 => 'Multi-Status',
160 // RFC4918
161 208 => 'Already Reported',
162 // RFC5842
163 226 => 'IM Used',
164 // RFC3229
165 300 => 'Multiple Choices',
166 301 => 'Moved Permanently',
167 302 => 'Found',
168 303 => 'See Other',
169 304 => 'Not Modified',
170 305 => 'Use Proxy',
171 307 => 'Temporary Redirect',
172 308 => 'Permanent Redirect',
173 // RFC7238
174 400 => 'Bad Request',
175 401 => 'Unauthorized',
176 402 => 'Payment Required',
177 403 => 'Forbidden',
178 404 => 'Not Found',
179 405 => 'Method Not Allowed',
180 406 => 'Not Acceptable',
181 407 => 'Proxy Authentication Required',
182 408 => 'Request Timeout',
183 409 => 'Conflict',
184 410 => 'Gone',
185 411 => 'Length Required',
186 412 => 'Precondition Failed',
187 413 => 'Content Too Large',
188 // RFC-ietf-httpbis-semantics
189 414 => 'URI Too Long',
190 415 => 'Unsupported Media Type',
191 416 => 'Range Not Satisfiable',
192 417 => 'Expectation Failed',
193 418 => 'I\'m a teapot',
194 // RFC2324
195 421 => 'Misdirected Request',
196 // RFC7540
197 422 => 'Unprocessable Content',
198 // RFC-ietf-httpbis-semantics
199 423 => 'Locked',
200 // RFC4918
201 424 => 'Failed Dependency',
202 // RFC4918
203 425 => 'Too Early',
204 // RFC-ietf-httpbis-replay-04
205 426 => 'Upgrade Required',
206 // RFC2817
207 428 => 'Precondition Required',
208 // RFC6585
209 429 => 'Too Many Requests',
210 // RFC6585
211 431 => 'Request Header Fields Too Large',
212 // RFC6585
213 451 => 'Unavailable For Legal Reasons',
214 // RFC7725
215 500 => 'Internal Server Error',
216 501 => 'Not Implemented',
217 502 => 'Bad Gateway',
218 503 => 'Service Unavailable',
219 504 => 'Gateway Timeout',
220 505 => 'HTTP Version Not Supported',
221 506 => 'Variant Also Negotiates',
222 // RFC2295
223 507 => 'Insufficient Storage',
224 // RFC4918
225 508 => 'Loop Detected',
226 // RFC5842
227 510 => 'Not Extended',
228 // RFC2774
229 511 => 'Network Authentication Required',
230 ];
231 /**
232 * @throws \InvalidArgumentException When the HTTP status code is not valid
233 */
234 public function __construct(?string $content = '', int $status = 200, array $headers = [])
235 {
236 $this->headers = new ResponseHeaderBag($headers);
237 $this->setContent($content);
238 $this->setStatusCode($status);
239 $this->setProtocolVersion('1.0');
240 }
241 /**
242 * Factory method for chainability.
243 *
244 * Example:
245 *
246 * return Response::create($body, 200)
247 * ->setSharedMaxAge(300);
248 *
249 * @return static
250 *
251 * @deprecated since Symfony 5.1, use __construct() instead.
252 */
253 public static function create(?string $content = '', int $status = 200, array $headers = [])
254 {
255 trigger_deprecation('symfony/http-foundation', '5.1', 'The "%s()" method is deprecated, use "new %s()" instead.', __METHOD__, static::class);
256 return new static($content, $status, $headers);
257 }
258 /**
259 * Returns the Response as an HTTP string.
260 *
261 * The string representation of the Response is the same as the
262 * one that will be sent to the client only if the prepare() method
263 * has been called before.
264 *
265 * @return string
266 *
267 * @see prepare()
268 */
269 public function __toString()
270 {
271 return sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText) . "\r\n" . $this->headers . "\r\n" . $this->getContent();
272 }
273 /**
274 * Clones the current Response instance.
275 */
276 public function __clone()
277 {
278 $this->headers = clone $this->headers;
279 }
280 /**
281 * Prepares the Response before it is sent to the client.
282 *
283 * This method tweaks the Response to ensure that it is
284 * compliant with RFC 2616. Most of the changes are based on
285 * the Request that is "associated" with this Response.
286 *
287 * @return $this
288 */
289 public function prepare(Request $request)
290 {
291 $headers = $this->headers;
292 if ($this->isInformational() || $this->isEmpty()) {
293 $this->setContent(null);
294 $headers->remove('Content-Type');
295 $headers->remove('Content-Length');
296 // prevent PHP from sending the Content-Type header based on default_mimetype
297 ini_set('default_mimetype', '');
298 } else {
299 // Content-type based on the Request
300 if (!$headers->has('Content-Type')) {
301 $format = $request->getRequestFormat(null);
302 if (null !== $format && ($mimeType = $request->getMimeType($format))) {
303 $headers->set('Content-Type', $mimeType);
304 }
305 }
306 // Fix Content-Type
307 $charset = $this->charset ?: 'UTF-8';
308 if (!$headers->has('Content-Type')) {
309 $headers->set('Content-Type', 'text/html; charset=' . $charset);
310 } elseif (0 === stripos($headers->get('Content-Type') ?? '', 'text/') && \false === stripos($headers->get('Content-Type') ?? '', 'charset')) {
311 // add the charset
312 $headers->set('Content-Type', $headers->get('Content-Type') . '; charset=' . $charset);
313 }
314 // Fix Content-Length
315 if ($headers->has('Transfer-Encoding')) {
316 $headers->remove('Content-Length');
317 }
318 if ($request->isMethod('HEAD')) {
319 // cf. RFC2616 14.13
320 $length = $headers->get('Content-Length');
321 $this->setContent(null);
322 if ($length) {
323 $headers->set('Content-Length', $length);
324 }
325 }
326 }
327 // Fix protocol
328 if ('HTTP/1.0' != $request->server->get('SERVER_PROTOCOL')) {
329 $this->setProtocolVersion('1.1');
330 }
331 // Check if we need to send extra expire info headers
332 if ('1.0' == $this->getProtocolVersion() && str_contains($headers->get('Cache-Control', ''), 'no-cache')) {
333 $headers->set('pragma', 'no-cache');
334 $headers->set('expires', -1);
335 }
336 $this->ensureIEOverSSLCompatibility($request);
337 if ($request->isSecure()) {
338 foreach ($headers->getCookies() as $cookie) {
339 $cookie->setSecureDefault(\true);
340 }
341 }
342 return $this;
343 }
344 /**
345 * Sends HTTP headers.
346 *
347 * @return $this
348 */
349 public function sendHeaders()
350 {
351 // headers have already been sent by the developer
352 if (headers_sent()) {
353 return $this;
354 }
355 // headers
356 foreach ($this->headers->allPreserveCaseWithoutCookies() as $name => $values) {
357 $replace = 0 === strcasecmp($name, 'Content-Type');
358 foreach ($values as $value) {
359 header($name . ': ' . $value, $replace, $this->statusCode);
360 }
361 }
362 // cookies
363 foreach ($this->headers->getCookies() as $cookie) {
364 header('Set-Cookie: ' . $cookie, \false, $this->statusCode);
365 }
366 // status
367 header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), \true, $this->statusCode);
368 return $this;
369 }
370 /**
371 * Sends content for the current web response.
372 *
373 * @return $this
374 */
375 public function sendContent()
376 {
377 echo $this->content;
378 return $this;
379 }
380 /**
381 * Sends HTTP headers and content.
382 *
383 * @return $this
384 */
385 public function send()
386 {
387 $this->sendHeaders();
388 $this->sendContent();
389 if (\function_exists('fastcgi_finish_request')) {
390 fastcgi_finish_request();
391 } elseif (\function_exists('litespeed_finish_request')) {
392 litespeed_finish_request();
393 } elseif (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], \true)) {
394 static::closeOutputBuffers(0, \true);
395 flush();
396 }
397 return $this;
398 }
399 /**
400 * Sets the response content.
401 *
402 * @return $this
403 */
404 public function setContent(?string $content)
405 {
406 $this->content = $content ?? '';
407 return $this;
408 }
409 /**
410 * Gets the current response content.
411 *
412 * @return string|false
413 */
414 public function getContent()
415 {
416 return $this->content;
417 }
418 /**
419 * Sets the HTTP protocol version (1.0 or 1.1).
420 *
421 * @return $this
422 *
423 * @final
424 */
425 public function setProtocolVersion(string $version) : object
426 {
427 $this->version = $version;
428 return $this;
429 }
430 /**
431 * Gets the HTTP protocol version.
432 *
433 * @final
434 */
435 public function getProtocolVersion() : string
436 {
437 return $this->version;
438 }
439 /**
440 * Sets the response status code.
441 *
442 * If the status text is null it will be automatically populated for the known
443 * status codes and left empty otherwise.
444 *
445 * @return $this
446 *
447 * @throws \InvalidArgumentException When the HTTP status code is not valid
448 *
449 * @final
450 */
451 public function setStatusCode(int $code, ?string $text = null) : object
452 {
453 $this->statusCode = $code;
454 if ($this->isInvalid()) {
455 throw new \InvalidArgumentException(sprintf('The HTTP status code "%s" is not valid.', $code));
456 }
457 if (null === $text) {
458 $this->statusText = self::$statusTexts[$code] ?? 'unknown status';
459 return $this;
460 }
461 if (\false === $text) {
462 $this->statusText = '';
463 return $this;
464 }
465 $this->statusText = $text;
466 return $this;
467 }
468 /**
469 * Retrieves the status code for the current web response.
470 *
471 * @final
472 */
473 public function getStatusCode() : int
474 {
475 return $this->statusCode;
476 }
477 /**
478 * Sets the response charset.
479 *
480 * @return $this
481 *
482 * @final
483 */
484 public function setCharset(string $charset) : object
485 {
486 $this->charset = $charset;
487 return $this;
488 }
489 /**
490 * Retrieves the response charset.
491 *
492 * @final
493 */
494 public function getCharset() : ?string
495 {
496 return $this->charset;
497 }
498 /**
499 * Returns true if the response may safely be kept in a shared (surrogate) cache.
500 *
501 * Responses marked "private" with an explicit Cache-Control directive are
502 * considered uncacheable.
503 *
504 * Responses with neither a freshness lifetime (Expires, max-age) nor cache
505 * validator (Last-Modified, ETag) are considered uncacheable because there is
506 * no way to tell when or how to remove them from the cache.
507 *
508 * Note that RFC 7231 and RFC 7234 possibly allow for a more permissive implementation,
509 * for example "status codes that are defined as cacheable by default [...]
510 * can be reused by a cache with heuristic expiration unless otherwise indicated"
511 * (https://tools.ietf.org/html/rfc7231#section-6.1)
512 *
513 * @final
514 */
515 public function isCacheable() : bool
516 {
517 if (!\in_array($this->statusCode, [200, 203, 300, 301, 302, 404, 410])) {
518 return \false;
519 }
520 if ($this->headers->hasCacheControlDirective('no-store') || $this->headers->getCacheControlDirective('private')) {
521 return \false;
522 }
523 return $this->isValidateable() || $this->isFresh();
524 }
525 /**
526 * Returns true if the response is "fresh".
527 *
528 * Fresh responses may be served from cache without any interaction with the
529 * origin. A response is considered fresh when it includes a Cache-Control/max-age
530 * indicator or Expires header and the calculated age is less than the freshness lifetime.
531 *
532 * @final
533 */
534 public function isFresh() : bool
535 {
536 return $this->getTtl() > 0;
537 }
538 /**
539 * Returns true if the response includes headers that can be used to validate
540 * the response with the origin server using a conditional GET request.
541 *
542 * @final
543 */
544 public function isValidateable() : bool
545 {
546 return $this->headers->has('Last-Modified') || $this->headers->has('ETag');
547 }
548 /**
549 * Marks the response as "private".
550 *
551 * It makes the response ineligible for serving other clients.
552 *
553 * @return $this
554 *
555 * @final
556 */
557 public function setPrivate() : object
558 {
559 $this->headers->removeCacheControlDirective('public');
560 $this->headers->addCacheControlDirective('private');
561 return $this;
562 }
563 /**
564 * Marks the response as "public".
565 *
566 * It makes the response eligible for serving other clients.
567 *
568 * @return $this
569 *
570 * @final
571 */
572 public function setPublic() : object
573 {
574 $this->headers->addCacheControlDirective('public');
575 $this->headers->removeCacheControlDirective('private');
576 return $this;
577 }
578 /**
579 * Marks the response as "immutable".
580 *
581 * @return $this
582 *
583 * @final
584 */
585 public function setImmutable(bool $immutable = \true) : object
586 {
587 if ($immutable) {
588 $this->headers->addCacheControlDirective('immutable');
589 } else {
590 $this->headers->removeCacheControlDirective('immutable');
591 }
592 return $this;
593 }
594 /**
595 * Returns true if the response is marked as "immutable".
596 *
597 * @final
598 */
599 public function isImmutable() : bool
600 {
601 return $this->headers->hasCacheControlDirective('immutable');
602 }
603 /**
604 * Returns true if the response must be revalidated by shared caches once it has become stale.
605 *
606 * This method indicates that the response must not be served stale by a
607 * cache in any circumstance without first revalidating with the origin.
608 * When present, the TTL of the response should not be overridden to be
609 * greater than the value provided by the origin.
610 *
611 * @final
612 */
613 public function mustRevalidate() : bool
614 {
615 return $this->headers->hasCacheControlDirective('must-revalidate') || $this->headers->hasCacheControlDirective('proxy-revalidate');
616 }
617 /**
618 * Returns the Date header as a DateTime instance.
619 *
620 * @throws \RuntimeException When the header is not parseable
621 *
622 * @final
623 */
624 public function getDate() : ?\DateTimeInterface
625 {
626 return $this->headers->getDate('Date');
627 }
628 /**
629 * Sets the Date header.
630 *
631 * @return $this
632 *
633 * @final
634 */
635 public function setDate(\DateTimeInterface $date) : object
636 {
637 if ($date instanceof \DateTime) {
638 $date = \DateTimeImmutable::createFromMutable($date);
639 }
640 $date = $date->setTimezone(new \DateTimeZone('UTC'));
641 $this->headers->set('Date', $date->format('D, d M Y H:i:s') . ' GMT');
642 return $this;
643 }
644 /**
645 * Returns the age of the response in seconds.
646 *
647 * @final
648 */
649 public function getAge() : int
650 {
651 if (null !== ($age = $this->headers->get('Age'))) {
652 return (int) $age;
653 }
654 return max(time() - (int) $this->getDate()->format('U'), 0);
655 }
656 /**
657 * Marks the response stale by setting the Age header to be equal to the maximum age of the response.
658 *
659 * @return $this
660 */
661 public function expire()
662 {
663 if ($this->isFresh()) {
664 $this->headers->set('Age', $this->getMaxAge());
665 $this->headers->remove('Expires');
666 }
667 return $this;
668 }
669 /**
670 * Returns the value of the Expires header as a DateTime instance.
671 *
672 * @final
673 */
674 public function getExpires() : ?\DateTimeInterface
675 {
676 try {
677 return $this->headers->getDate('Expires');
678 } catch (\RuntimeException $e) {
679 // according to RFC 2616 invalid date formats (e.g. "0" and "-1") must be treated as in the past
680 return \DateTime::createFromFormat('U', time() - 172800);
681 }
682 }
683 /**
684 * Sets the Expires HTTP header with a DateTime instance.
685 *
686 * Passing null as value will remove the header.
687 *
688 * @return $this
689 *
690 * @final
691 */
692 public function setExpires(?\DateTimeInterface $date = null) : object
693 {
694 if (null === $date) {
695 $this->headers->remove('Expires');
696 return $this;
697 }
698 if ($date instanceof \DateTime) {
699 $date = \DateTimeImmutable::createFromMutable($date);
700 }
701 $date = $date->setTimezone(new \DateTimeZone('UTC'));
702 $this->headers->set('Expires', $date->format('D, d M Y H:i:s') . ' GMT');
703 return $this;
704 }
705 /**
706 * Returns the number of seconds after the time specified in the response's Date
707 * header when the response should no longer be considered fresh.
708 *
709 * First, it checks for a s-maxage directive, then a max-age directive, and then it falls
710 * back on an expires header. It returns null when no maximum age can be established.
711 *
712 * @final
713 */
714 public function getMaxAge() : ?int
715 {
716 if ($this->headers->hasCacheControlDirective('s-maxage')) {
717 return (int) $this->headers->getCacheControlDirective('s-maxage');
718 }
719 if ($this->headers->hasCacheControlDirective('max-age')) {
720 return (int) $this->headers->getCacheControlDirective('max-age');
721 }
722 if (null !== ($expires = $this->getExpires())) {
723 $maxAge = (int) $expires->format('U') - (int) $this->getDate()->format('U');
724 return max($maxAge, 0);
725 }
726 return null;
727 }
728 /**
729 * Sets the number of seconds after which the response should no longer be considered fresh.
730 *
731 * This methods sets the Cache-Control max-age directive.
732 *
733 * @return $this
734 *
735 * @final
736 */
737 public function setMaxAge(int $value) : object
738 {
739 $this->headers->addCacheControlDirective('max-age', $value);
740 return $this;
741 }
742 /**
743 * Sets the number of seconds after which the response should no longer be considered fresh by shared caches.
744 *
745 * This methods sets the Cache-Control s-maxage directive.
746 *
747 * @return $this
748 *
749 * @final
750 */
751 public function setSharedMaxAge(int $value) : object
752 {
753 $this->setPublic();
754 $this->headers->addCacheControlDirective('s-maxage', $value);
755 return $this;
756 }
757 /**
758 * Returns the response's time-to-live in seconds.
759 *
760 * It returns null when no freshness information is present in the response.
761 *
762 * When the response's TTL is 0, the response may not be served from cache without first
763 * revalidating with the origin.
764 *
765 * @final
766 */
767 public function getTtl() : ?int
768 {
769 $maxAge = $this->getMaxAge();
770 return null !== $maxAge ? max($maxAge - $this->getAge(), 0) : null;
771 }
772 /**
773 * Sets the response's time-to-live for shared caches in seconds.
774 *
775 * This method adjusts the Cache-Control/s-maxage directive.
776 *
777 * @return $this
778 *
779 * @final
780 */
781 public function setTtl(int $seconds) : object
782 {
783 $this->setSharedMaxAge($this->getAge() + $seconds);
784 return $this;
785 }
786 /**
787 * Sets the response's time-to-live for private/client caches in seconds.
788 *
789 * This method adjusts the Cache-Control/max-age directive.
790 *
791 * @return $this
792 *
793 * @final
794 */
795 public function setClientTtl(int $seconds) : object
796 {
797 $this->setMaxAge($this->getAge() + $seconds);
798 return $this;
799 }
800 /**
801 * Returns the Last-Modified HTTP header as a DateTime instance.
802 *
803 * @throws \RuntimeException When the HTTP header is not parseable
804 *
805 * @final
806 */
807 public function getLastModified() : ?\DateTimeInterface
808 {
809 return $this->headers->getDate('Last-Modified');
810 }
811 /**
812 * Sets the Last-Modified HTTP header with a DateTime instance.
813 *
814 * Passing null as value will remove the header.
815 *
816 * @return $this
817 *
818 * @final
819 */
820 public function setLastModified(?\DateTimeInterface $date = null) : object
821 {
822 if (null === $date) {
823 $this->headers->remove('Last-Modified');
824 return $this;
825 }
826 if ($date instanceof \DateTime) {
827 $date = \DateTimeImmutable::createFromMutable($date);
828 }
829 $date = $date->setTimezone(new \DateTimeZone('UTC'));
830 $this->headers->set('Last-Modified', $date->format('D, d M Y H:i:s') . ' GMT');
831 return $this;
832 }
833 /**
834 * Returns the literal value of the ETag HTTP header.
835 *
836 * @final
837 */
838 public function getEtag() : ?string
839 {
840 return $this->headers->get('ETag');
841 }
842 /**
843 * Sets the ETag value.
844 *
845 * @param string|null $etag The ETag unique identifier or null to remove the header
846 * @param bool $weak Whether you want a weak ETag or not
847 *
848 * @return $this
849 *
850 * @final
851 */
852 public function setEtag(?string $etag = null, bool $weak = \false) : object
853 {
854 if (null === $etag) {
855 $this->headers->remove('Etag');
856 } else {
857 if (!str_starts_with($etag, '"')) {
858 $etag = '"' . $etag . '"';
859 }
860 $this->headers->set('ETag', (\true === $weak ? 'W/' : '') . $etag);
861 }
862 return $this;
863 }
864 /**
865 * Sets the response's cache headers (validation and/or expiration).
866 *
867 * Available options are: must_revalidate, no_cache, no_store, no_transform, public, private, proxy_revalidate, max_age, s_maxage, immutable, last_modified and etag.
868 *
869 * @return $this
870 *
871 * @throws \InvalidArgumentException
872 *
873 * @final
874 */
875 public function setCache(array $options) : object
876 {
877 if ($diff = array_diff(array_keys($options), array_keys(self::HTTP_RESPONSE_CACHE_CONTROL_DIRECTIVES))) {
878 throw new \InvalidArgumentException(sprintf('Response does not support the following options: "%s".', implode('", "', $diff)));
879 }
880 if (isset($options['etag'])) {
881 $this->setEtag($options['etag']);
882 }
883 if (isset($options['last_modified'])) {
884 $this->setLastModified($options['last_modified']);
885 }
886 if (isset($options['max_age'])) {
887 $this->setMaxAge($options['max_age']);
888 }
889 if (isset($options['s_maxage'])) {
890 $this->setSharedMaxAge($options['s_maxage']);
891 }
892 foreach (self::HTTP_RESPONSE_CACHE_CONTROL_DIRECTIVES as $directive => $hasValue) {
893 if (!$hasValue && isset($options[$directive])) {
894 if ($options[$directive]) {
895 $this->headers->addCacheControlDirective(str_replace('_', '-', $directive));
896 } else {
897 $this->headers->removeCacheControlDirective(str_replace('_', '-', $directive));
898 }
899 }
900 }
901 if (isset($options['public'])) {
902 if ($options['public']) {
903 $this->setPublic();
904 } else {
905 $this->setPrivate();
906 }
907 }
908 if (isset($options['private'])) {
909 if ($options['private']) {
910 $this->setPrivate();
911 } else {
912 $this->setPublic();
913 }
914 }
915 return $this;
916 }
917 /**
918 * Modifies the response so that it conforms to the rules defined for a 304 status code.
919 *
920 * This sets the status, removes the body, and discards any headers
921 * that MUST NOT be included in 304 responses.
922 *
923 * @return $this
924 *
925 * @see https://tools.ietf.org/html/rfc2616#section-10.3.5
926 *
927 * @final
928 */
929 public function setNotModified() : object
930 {
931 $this->setStatusCode(304);
932 $this->setContent(null);
933 // remove headers that MUST NOT be included with 304 Not Modified responses
934 foreach (['Allow', 'Content-Encoding', 'Content-Language', 'Content-Length', 'Content-MD5', 'Content-Type', 'Last-Modified'] as $header) {
935 $this->headers->remove($header);
936 }
937 return $this;
938 }
939 /**
940 * Returns true if the response includes a Vary header.
941 *
942 * @final
943 */
944 public function hasVary() : bool
945 {
946 return null !== $this->headers->get('Vary');
947 }
948 /**
949 * Returns an array of header names given in the Vary header.
950 *
951 * @final
952 */
953 public function getVary() : array
954 {
955 if (!($vary = $this->headers->all('Vary'))) {
956 return [];
957 }
958 $ret = [];
959 foreach ($vary as $item) {
960 $ret[] = preg_split('/[\\s,]+/', $item);
961 }
962 return array_merge([], ...$ret);
963 }
964 /**
965 * Sets the Vary header.
966 *
967 * @param string|array $headers
968 * @param bool $replace Whether to replace the actual value or not (true by default)
969 *
970 * @return $this
971 *
972 * @final
973 */
974 public function setVary($headers, bool $replace = \true) : object
975 {
976 $this->headers->set('Vary', $headers, $replace);
977 return $this;
978 }
979 /**
980 * Determines if the Response validators (ETag, Last-Modified) match
981 * a conditional value specified in the Request.
982 *
983 * If the Response is not modified, it sets the status code to 304 and
984 * removes the actual content by calling the setNotModified() method.
985 *
986 * @final
987 */
988 public function isNotModified(Request $request) : bool
989 {
990 if (!$request->isMethodCacheable()) {
991 return \false;
992 }
993 $notModified = \false;
994 $lastModified = $this->headers->get('Last-Modified');
995 $modifiedSince = $request->headers->get('If-Modified-Since');
996 if (($ifNoneMatchEtags = $request->getETags()) && null !== ($etag = $this->getEtag())) {
997 if (0 == strncmp($etag, 'W/', 2)) {
998 $etag = substr($etag, 2);
999 }
1000 // Use weak comparison as per https://tools.ietf.org/html/rfc7232#section-3.2.
1001 foreach ($ifNoneMatchEtags as $ifNoneMatchEtag) {
1002 if (0 == strncmp($ifNoneMatchEtag, 'W/', 2)) {
1003 $ifNoneMatchEtag = substr($ifNoneMatchEtag, 2);
1004 }
1005 if ($ifNoneMatchEtag === $etag || '*' === $ifNoneMatchEtag) {
1006 $notModified = \true;
1007 break;
1008 }
1009 }
1010 } elseif ($modifiedSince && $lastModified) {
1011 $notModified = strtotime($modifiedSince) >= strtotime($lastModified);
1012 }
1013 if ($notModified) {
1014 $this->setNotModified();
1015 }
1016 return $notModified;
1017 }
1018 /**
1019 * Is response invalid?
1020 *
1021 * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
1022 *
1023 * @final
1024 */
1025 public function isInvalid() : bool
1026 {
1027 return $this->statusCode < 100 || $this->statusCode >= 600;
1028 }
1029 /**
1030 * Is response informative?
1031 *
1032 * @final
1033 */
1034 public function isInformational() : bool
1035 {
1036 return $this->statusCode >= 100 && $this->statusCode < 200;
1037 }
1038 /**
1039 * Is response successful?
1040 *
1041 * @final
1042 */
1043 public function isSuccessful() : bool
1044 {
1045 return $this->statusCode >= 200 && $this->statusCode < 300;
1046 }
1047 /**
1048 * Is the response a redirect?
1049 *
1050 * @final
1051 */
1052 public function isRedirection() : bool
1053 {
1054 return $this->statusCode >= 300 && $this->statusCode < 400;
1055 }
1056 /**
1057 * Is there a client error?
1058 *
1059 * @final
1060 */
1061 public function isClientError() : bool
1062 {
1063 return $this->statusCode >= 400 && $this->statusCode < 500;
1064 }
1065 /**
1066 * Was there a server side error?
1067 *
1068 * @final
1069 */
1070 public function isServerError() : bool
1071 {
1072 return $this->statusCode >= 500 && $this->statusCode < 600;
1073 }
1074 /**
1075 * Is the response OK?
1076 *
1077 * @final
1078 */
1079 public function isOk() : bool
1080 {
1081 return 200 === $this->statusCode;
1082 }
1083 /**
1084 * Is the response forbidden?
1085 *
1086 * @final
1087 */
1088 public function isForbidden() : bool
1089 {
1090 return 403 === $this->statusCode;
1091 }
1092 /**
1093 * Is the response a not found error?
1094 *
1095 * @final
1096 */
1097 public function isNotFound() : bool
1098 {
1099 return 404 === $this->statusCode;
1100 }
1101 /**
1102 * Is the response a redirect of some form?
1103 *
1104 * @final
1105 */
1106 public function isRedirect(?string $location = null) : bool
1107 {
1108 return \in_array($this->statusCode, [201, 301, 302, 303, 307, 308]) && (null === $location ?: $location == $this->headers->get('Location'));
1109 }
1110 /**
1111 * Is the response empty?
1112 *
1113 * @final
1114 */
1115 public function isEmpty() : bool
1116 {
1117 return \in_array($this->statusCode, [204, 304]);
1118 }
1119 /**
1120 * Cleans or flushes output buffers up to target level.
1121 *
1122 * Resulting level can be greater than target level if a non-removable buffer has been encountered.
1123 *
1124 * @final
1125 */
1126 public static function closeOutputBuffers(int $targetLevel, bool $flush) : void
1127 {
1128 $status = ob_get_status(\true);
1129 $level = \count($status);
1130 $flags = \PHP_OUTPUT_HANDLER_REMOVABLE | ($flush ? \PHP_OUTPUT_HANDLER_FLUSHABLE : \PHP_OUTPUT_HANDLER_CLEANABLE);
1131 while ($level-- > $targetLevel && ($s = $status[$level]) && (!isset($s['del']) ? !isset($s['flags']) || ($s['flags'] & $flags) === $flags : $s['del'])) {
1132 if ($flush) {
1133 ob_end_flush();
1134 } else {
1135 ob_end_clean();
1136 }
1137 }
1138 }
1139 /**
1140 * Marks a response as safe according to RFC8674.
1141 *
1142 * @see https://tools.ietf.org/html/rfc8674
1143 */
1144 public function setContentSafe(bool $safe = \true) : void
1145 {
1146 if ($safe) {
1147 $this->headers->set('Preference-Applied', 'safe');
1148 } elseif ('safe' === $this->headers->get('Preference-Applied')) {
1149 $this->headers->remove('Preference-Applied');
1150 }
1151 $this->setVary('Prefer', \false);
1152 }
1153 /**
1154 * Checks if we need to remove Cache-Control for SSL encrypted downloads when using IE < 9.
1155 *
1156 * @see http://support.microsoft.com/kb/323308
1157 *
1158 * @final
1159 */
1160 protected function ensureIEOverSSLCompatibility(Request $request) : void
1161 {
1162 if (\false !== stripos($this->headers->get('Content-Disposition') ?? '', 'attachment') && 1 == preg_match('/MSIE (.*?);/i', $request->server->get('HTTP_USER_AGENT') ?? '', $match) && \true === $request->isSecure()) {
1163 if ((int) preg_replace('/(MSIE )(.*?);/', '$2', $match[0]) < 9) {
1164 $this->headers->remove('Cache-Control');
1165 }
1166 }
1167 }
1168 }
1169