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