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