Completed
Push — master ( 193986...cdf399 )
by Andrey
11:47
created

Response::setSharedMaxAge()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Symfony\Component\HttpFoundation;
13
14
/**
15
 * Response represents an HTTP response.
16
 *
17
 * @author Fabien Potencier <[email protected]>
18
 */
19
class Response
20
{
21
    const HTTP_CONTINUE = 100;
22
    const HTTP_SWITCHING_PROTOCOLS = 101;
23
    const HTTP_PROCESSING = 102;            // RFC2518
24
    const HTTP_OK = 200;
25
    const HTTP_CREATED = 201;
26
    const HTTP_ACCEPTED = 202;
27
    const HTTP_NON_AUTHORITATIVE_INFORMATION = 203;
28
    const HTTP_NO_CONTENT = 204;
29
    const HTTP_RESET_CONTENT = 205;
30
    const HTTP_PARTIAL_CONTENT = 206;
31
    const HTTP_MULTI_STATUS = 207;          // RFC4918
32
    const HTTP_ALREADY_REPORTED = 208;      // RFC5842
33
    const HTTP_IM_USED = 226;               // RFC3229
34
    const HTTP_MULTIPLE_CHOICES = 300;
35
    const HTTP_MOVED_PERMANENTLY = 301;
36
    const HTTP_FOUND = 302;
37
    const HTTP_SEE_OTHER = 303;
38
    const HTTP_NOT_MODIFIED = 304;
39
    const HTTP_USE_PROXY = 305;
40
    const HTTP_RESERVED = 306;
41
    const HTTP_TEMPORARY_REDIRECT = 307;
42
    const HTTP_PERMANENTLY_REDIRECT = 308;  // RFC7238
43
    const HTTP_BAD_REQUEST = 400;
44
    const HTTP_UNAUTHORIZED = 401;
45
    const HTTP_PAYMENT_REQUIRED = 402;
46
    const HTTP_FORBIDDEN = 403;
47
    const HTTP_NOT_FOUND = 404;
48
    const HTTP_METHOD_NOT_ALLOWED = 405;
49
    const HTTP_NOT_ACCEPTABLE = 406;
50
    const HTTP_PROXY_AUTHENTICATION_REQUIRED = 407;
51
    const HTTP_REQUEST_TIMEOUT = 408;
52
    const HTTP_CONFLICT = 409;
53
    const HTTP_GONE = 410;
54
    const HTTP_LENGTH_REQUIRED = 411;
55
    const HTTP_PRECONDITION_FAILED = 412;
56
    const HTTP_REQUEST_ENTITY_TOO_LARGE = 413;
57
    const HTTP_REQUEST_URI_TOO_LONG = 414;
58
    const HTTP_UNSUPPORTED_MEDIA_TYPE = 415;
59
    const HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
60
    const HTTP_EXPECTATION_FAILED = 417;
61
    const HTTP_I_AM_A_TEAPOT = 418;                                               // RFC2324
62
    const HTTP_MISDIRECTED_REQUEST = 421;                                         // RFC7540
63
    const HTTP_UNPROCESSABLE_ENTITY = 422;                                        // RFC4918
64
    const HTTP_LOCKED = 423;                                                      // RFC4918
65
    const HTTP_FAILED_DEPENDENCY = 424;                                           // RFC4918
66
    const HTTP_RESERVED_FOR_WEBDAV_ADVANCED_COLLECTIONS_EXPIRED_PROPOSAL = 425;   // RFC2817
67
    const HTTP_UPGRADE_REQUIRED = 426;                                            // RFC2817
68
    const HTTP_PRECONDITION_REQUIRED = 428;                                       // RFC6585
69
    const HTTP_TOO_MANY_REQUESTS = 429;                                           // RFC6585
70
    const HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE = 431;                             // RFC6585
71
    const HTTP_UNAVAILABLE_FOR_LEGAL_REASONS = 451;
72
    const HTTP_INTERNAL_SERVER_ERROR = 500;
73
    const HTTP_NOT_IMPLEMENTED = 501;
74
    const HTTP_BAD_GATEWAY = 502;
75
    const HTTP_SERVICE_UNAVAILABLE = 503;
76
    const HTTP_GATEWAY_TIMEOUT = 504;
77
    const HTTP_VERSION_NOT_SUPPORTED = 505;
78
    const HTTP_VARIANT_ALSO_NEGOTIATES_EXPERIMENTAL = 506;                        // RFC2295
79
    const HTTP_INSUFFICIENT_STORAGE = 507;                                        // RFC4918
80
    const HTTP_LOOP_DETECTED = 508;                                               // RFC5842
81
    const HTTP_NOT_EXTENDED = 510;                                                // RFC2774
82
    const HTTP_NETWORK_AUTHENTICATION_REQUIRED = 511;                             // RFC6585
83
84
    /**
85
     * @var \Symfony\Component\HttpFoundation\ResponseHeaderBag
86
     */
87
    public $headers;
88
89
    /**
90
     * @var string
91
     */
92
    protected $content;
93
94
    /**
95
     * @var string
96
     */
97
    protected $version;
98
99
    /**
100
     * @var int
101
     */
102
    protected $statusCode;
103
104
    /**
105
     * @var string
106
     */
107
    protected $statusText;
108
109
    /**
110
     * @var string
111
     */
112
    protected $charset;
113
114
    /**
115
     * Status codes translation table.
116
     *
117
     * The list of codes is complete according to the
118
     * {@link http://www.iana.org/assignments/http-status-codes/ Hypertext Transfer Protocol (HTTP) Status Code Registry}
119
     * (last updated 2016-03-01).
120
     *
121
     * Unless otherwise noted, the status code is defined in RFC2616.
122
     *
123
     * @var array
124
     */
125
    public static $statusTexts = array(
126
        100 => 'Continue',
127
        101 => 'Switching Protocols',
128
        102 => 'Processing',            // RFC2518
129
        200 => 'OK',
130
        201 => 'Created',
131
        202 => 'Accepted',
132
        203 => 'Non-Authoritative Information',
133
        204 => 'No Content',
134
        205 => 'Reset Content',
135
        206 => 'Partial Content',
136
        207 => 'Multi-Status',          // RFC4918
137
        208 => 'Already Reported',      // RFC5842
138
        226 => 'IM Used',               // RFC3229
139
        300 => 'Multiple Choices',
140
        301 => 'Moved Permanently',
141
        302 => 'Found',
142
        303 => 'See Other',
143
        304 => 'Not Modified',
144
        305 => 'Use Proxy',
145
        306 => 'Reserved',
146
        307 => 'Temporary Redirect',
147
        308 => 'Permanent Redirect',    // RFC7238
148
        400 => 'Bad Request',
149
        401 => 'Unauthorized',
150
        402 => 'Payment Required',
151
        403 => 'Forbidden',
152
        404 => 'Not Found',
153
        405 => 'Method Not Allowed',
154
        406 => 'Not Acceptable',
155
        407 => 'Proxy Authentication Required',
156
        408 => 'Request Timeout',
157
        409 => 'Conflict',
158
        410 => 'Gone',
159
        411 => 'Length Required',
160
        412 => 'Precondition Failed',
161
        413 => 'Payload Too Large',
162
        414 => 'URI Too Long',
163
        415 => 'Unsupported Media Type',
164
        416 => 'Range Not Satisfiable',
165
        417 => 'Expectation Failed',
166
        418 => 'I\'m a teapot',                                               // RFC2324
167
        421 => 'Misdirected Request',                                         // RFC7540
168
        422 => 'Unprocessable Entity',                                        // RFC4918
169
        423 => 'Locked',                                                      // RFC4918
170
        424 => 'Failed Dependency',                                           // RFC4918
171
        425 => 'Reserved for WebDAV advanced collections expired proposal',   // RFC2817
172
        426 => 'Upgrade Required',                                            // RFC2817
173
        428 => 'Precondition Required',                                       // RFC6585
174
        429 => 'Too Many Requests',                                           // RFC6585
175
        431 => 'Request Header Fields Too Large',                             // RFC6585
176
        451 => 'Unavailable For Legal Reasons',                               // RFC7725
177
        500 => 'Internal Server Error',
178
        501 => 'Not Implemented',
179
        502 => 'Bad Gateway',
180
        503 => 'Service Unavailable',
181
        504 => 'Gateway Timeout',
182
        505 => 'HTTP Version Not Supported',
183
        506 => 'Variant Also Negotiates',                                     // RFC2295
184
        507 => 'Insufficient Storage',                                        // RFC4918
185
        508 => 'Loop Detected',                                               // RFC5842
186
        510 => 'Not Extended',                                                // RFC2774
187
        511 => 'Network Authentication Required',                             // RFC6585
188
    );
189
190
    /**
191
     * Constructor.
192
     *
193
     * @param mixed $content The response content, see setContent()
194
     * @param int   $status  The response status code
195
     * @param array $headers An array of response headers
196
     *
197
     * @throws \InvalidArgumentException When the HTTP status code is not valid
198
     */
199
    public function __construct($content = '', $status = 200, $headers = array())
200
    {
201
        $this->headers = new ResponseHeaderBag($headers);
202
        $this->setContent($content);
203
        $this->setStatusCode($status);
204
        $this->setProtocolVersion('1.0');
205
        if (!$this->headers->has('Date')) {
206
            $this->setDate(\DateTime::createFromFormat('U', time(), new \DateTimeZone('UTC')));
0 ignored issues
show
Security Bug introduced by
It seems like \DateTime::createFromFor...w \DateTimeZone('UTC')) targeting DateTime::createFromFormat() can also be of type false; however, Symfony\Component\HttpFo...ion\Response::setDate() does only seem to accept object<DateTime>, did you maybe forget to handle an error condition?
Loading history...
207
        }
208
    }
209
210
    /**
211
     * Factory method for chainability.
212
     *
213
     * Example:
214
     *
215
     *     return Response::create($body, 200)
216
     *         ->setSharedMaxAge(300);
217
     *
218
     * @param mixed $content The response content, see setContent()
219
     * @param int   $status  The response status code
220
     * @param array $headers An array of response headers
221
     *
222
     * @return static
223
     */
224
    public static function create($content = '', $status = 200, $headers = array())
225
    {
226
        return new static($content, $status, $headers);
227
    }
228
229
    /**
230
     * Returns the Response as an HTTP string.
231
     *
232
     * The string representation of the Response is the same as the
233
     * one that will be sent to the client only if the prepare() method
234
     * has been called before.
235
     *
236
     * @return string The Response as an HTTP string
237
     *
238
     * @see prepare()
239
     */
240
    public function __toString()
241
    {
242
        return
243
            sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)."\r\n".
244
            $this->headers."\r\n".
245
            $this->getContent();
246
    }
247
248
    /**
249
     * Clones the current Response instance.
250
     */
251
    public function __clone()
252
    {
253
        $this->headers = clone $this->headers;
254
    }
255
256
    /**
257
     * Prepares the Response before it is sent to the client.
258
     *
259
     * This method tweaks the Response to ensure that it is
260
     * compliant with RFC 2616. Most of the changes are based on
261
     * the Request that is "associated" with this Response.
262
     *
263
     * @param Request $request A Request instance
264
     *
265
     * @return $this
266
     */
267
    public function prepare(Request $request)
268
    {
269
        $headers = $this->headers;
270
271
        if ($this->isInformational() || $this->isEmpty()) {
272
            $this->setContent(null);
273
            $headers->remove('Content-Type');
274
            $headers->remove('Content-Length');
275
        } else {
276
            // Content-type based on the Request
277
            if (!$headers->has('Content-Type')) {
278
                $format = $request->getRequestFormat();
279
                if (null !== $format && $mimeType = $request->getMimeType($format)) {
280
                    $headers->set('Content-Type', $mimeType);
281
                }
282
            }
283
284
            // Fix Content-Type
285
            $charset = $this->charset ?: 'UTF-8';
286
            if (!$headers->has('Content-Type')) {
287
                $headers->set('Content-Type', 'text/html; charset='.$charset);
288
            } elseif (0 === stripos($headers->get('Content-Type'), 'text/') && false === stripos($headers->get('Content-Type'), 'charset')) {
289
                // add the charset
290
                $headers->set('Content-Type', $headers->get('Content-Type').'; charset='.$charset);
291
            }
292
293
            // Fix Content-Length
294
            if ($headers->has('Transfer-Encoding')) {
295
                $headers->remove('Content-Length');
296
            }
297
298
            if ($request->isMethod('HEAD')) {
299
                // cf. RFC2616 14.13
300
                $length = $headers->get('Content-Length');
301
                $this->setContent(null);
302
                if ($length) {
303
                    $headers->set('Content-Length', $length);
304
                }
305
            }
306
        }
307
308
        // Fix protocol
309
        if ('HTTP/1.0' != $request->server->get('SERVER_PROTOCOL')) {
310
            $this->setProtocolVersion('1.1');
311
        }
312
313
        // Check if we need to send extra expire info headers
314
        if ('1.0' == $this->getProtocolVersion() && 'no-cache' == $this->headers->get('Cache-Control')) {
315
            $this->headers->set('pragma', 'no-cache');
316
            $this->headers->set('expires', -1);
317
        }
318
319
        $this->ensureIEOverSSLCompatibility($request);
320
321
        return $this;
322
    }
323
324
    /**
325
     * Sends HTTP headers.
326
     *
327
     * @return $this
328
     */
329
    public function sendHeaders()
330
    {
331
        // headers have already been sent by the developer
332
        if (headers_sent()) {
333
            return $this;
334
        }
335
336
        // headers
337
        foreach ($this->headers->allPreserveCase() as $name => $values) {
338
            foreach ($values as $value) {
339
                header($name.': '.$value, false);
340
            }
341
        }
342
343
        // status
344
        header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode);
345
346
        // cookies
347 View Code Duplication
        foreach ($this->headers->getCookies() as $cookie) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
348
            setcookie($cookie->getName(), $cookie->getValue(), $cookie->getExpiresTime(), $cookie->getPath(), $cookie->getDomain(), $cookie->isSecure(), $cookie->isHttpOnly());
349
        }
350
351
        return $this;
352
    }
353
354
    /**
355
     * Sends content for the current web response.
356
     *
357
     * @return $this
358
     */
359
    public function sendContent()
360
    {
361
        echo $this->content;
362
363
        return $this;
364
    }
365
366
    /**
367
     * Sends HTTP headers and content.
368
     *
369
     * @return $this
370
     */
371
    public function send()
372
    {
373
        $this->sendHeaders();
374
        $this->sendContent();
375
376
        if (function_exists('fastcgi_finish_request')) {
377
            fastcgi_finish_request();
378
        } elseif ('cli' !== PHP_SAPI) {
379
            static::closeOutputBuffers(0, true);
380
        }
381
382
        return $this;
383
    }
384
385
    /**
386
     * Sets the response content.
387
     *
388
     * Valid types are strings, numbers, null, and objects that implement a __toString() method.
389
     *
390
     * @param mixed $content Content that can be cast to string
391
     *
392
     * @return $this
393
     *
394
     * @throws \UnexpectedValueException
395
     */
396
    public function setContent($content)
397
    {
398
        if (null !== $content && !is_string($content) && !is_numeric($content) && !is_callable(array($content, '__toString'))) {
399
            throw new \UnexpectedValueException(sprintf('The Response content must be a string or object implementing __toString(), "%s" given.', gettype($content)));
400
        }
401
402
        $this->content = (string) $content;
403
404
        return $this;
405
    }
406
407
    /**
408
     * Gets the current response content.
409
     *
410
     * @return string Content
411
     */
412
    public function getContent()
413
    {
414
        return $this->content;
415
    }
416
417
    /**
418
     * Sets the HTTP protocol version (1.0 or 1.1).
419
     *
420
     * @param string $version The HTTP protocol version
421
     *
422
     * @return $this
423
     */
424
    public function setProtocolVersion($version)
425
    {
426
        $this->version = $version;
427
428
        return $this;
429
    }
430
431
    /**
432
     * Gets the HTTP protocol version.
433
     *
434
     * @return string The HTTP protocol version
435
     */
436
    public function getProtocolVersion()
437
    {
438
        return $this->version;
439
    }
440
441
    /**
442
     * Sets the response status code.
443
     *
444
     * @param int   $code HTTP status code
445
     * @param mixed $text HTTP status text
446
     *
447
     * If the status text is null it will be automatically populated for the known
448
     * status codes and left empty otherwise.
449
     *
450
     * @return $this
451
     *
452
     * @throws \InvalidArgumentException When the HTTP status code is not valid
453
     */
454
    public function setStatusCode($code, $text = null)
455
    {
456
        $this->statusCode = $code = (int) $code;
457
        if ($this->isInvalid()) {
458
            throw new \InvalidArgumentException(sprintf('The HTTP status code "%s" is not valid.', $code));
459
        }
460
461
        if (null === $text) {
462
            $this->statusText = isset(self::$statusTexts[$code]) ? self::$statusTexts[$code] : 'unknown status';
463
464
            return $this;
465
        }
466
467
        if (false === $text) {
468
            $this->statusText = '';
469
470
            return $this;
471
        }
472
473
        $this->statusText = $text;
474
475
        return $this;
476
    }
477
478
    /**
479
     * Retrieves the status code for the current web response.
480
     *
481
     * @return int Status code
482
     */
483
    public function getStatusCode()
484
    {
485
        return $this->statusCode;
486
    }
487
488
    /**
489
     * Sets the response charset.
490
     *
491
     * @param string $charset Character set
492
     *
493
     * @return $this
494
     */
495
    public function setCharset($charset)
496
    {
497
        $this->charset = $charset;
498
499
        return $this;
500
    }
501
502
    /**
503
     * Retrieves the response charset.
504
     *
505
     * @return string Character set
506
     */
507
    public function getCharset()
508
    {
509
        return $this->charset;
510
    }
511
512
    /**
513
     * Returns true if the response is worth caching under any circumstance.
514
     *
515
     * Responses marked "private" with an explicit Cache-Control directive are
516
     * considered uncacheable.
517
     *
518
     * Responses with neither a freshness lifetime (Expires, max-age) nor cache
519
     * validator (Last-Modified, ETag) are considered uncacheable.
520
     *
521
     * @return bool true if the response is worth caching, false otherwise
522
     */
523
    public function isCacheable()
524
    {
525
        if (!in_array($this->statusCode, array(200, 203, 300, 301, 302, 404, 410))) {
526
            return false;
527
        }
528
529
        if ($this->headers->hasCacheControlDirective('no-store') || $this->headers->getCacheControlDirective('private')) {
530
            return false;
531
        }
532
533
        return $this->isValidateable() || $this->isFresh();
534
    }
535
536
    /**
537
     * Returns true if the response is "fresh".
538
     *
539
     * Fresh responses may be served from cache without any interaction with the
540
     * origin. A response is considered fresh when it includes a Cache-Control/max-age
541
     * indicator or Expires header and the calculated age is less than the freshness lifetime.
542
     *
543
     * @return bool true if the response is fresh, false otherwise
544
     */
545
    public function isFresh()
546
    {
547
        return $this->getTtl() > 0;
548
    }
549
550
    /**
551
     * Returns true if the response includes headers that can be used to validate
552
     * the response with the origin server using a conditional GET request.
553
     *
554
     * @return bool true if the response is validateable, false otherwise
555
     */
556
    public function isValidateable()
557
    {
558
        return $this->headers->has('Last-Modified') || $this->headers->has('ETag');
559
    }
560
561
    /**
562
     * Marks the response as "private".
563
     *
564
     * It makes the response ineligible for serving other clients.
565
     *
566
     * @return $this
567
     */
568
    public function setPrivate()
569
    {
570
        $this->headers->removeCacheControlDirective('public');
571
        $this->headers->addCacheControlDirective('private');
572
573
        return $this;
574
    }
575
576
    /**
577
     * Marks the response as "public".
578
     *
579
     * It makes the response eligible for serving other clients.
580
     *
581
     * @return $this
582
     */
583
    public function setPublic()
584
    {
585
        $this->headers->addCacheControlDirective('public');
586
        $this->headers->removeCacheControlDirective('private');
587
588
        return $this;
589
    }
590
591
    /**
592
     * Returns true if the response must be revalidated by caches.
593
     *
594
     * This method indicates that the response must not be served stale by a
595
     * cache in any circumstance without first revalidating with the origin.
596
     * When present, the TTL of the response should not be overridden to be
597
     * greater than the value provided by the origin.
598
     *
599
     * @return bool true if the response must be revalidated by a cache, false otherwise
600
     */
601
    public function mustRevalidate()
602
    {
603
        return $this->headers->hasCacheControlDirective('must-revalidate') || $this->headers->hasCacheControlDirective('proxy-revalidate');
604
    }
605
606
    /**
607
     * Returns the Date header as a DateTime instance.
608
     *
609
     * @return \DateTime A \DateTime instance
610
     *
611
     * @throws \RuntimeException When the header is not parseable
612
     */
613
    public function getDate()
614
    {
615
        return $this->headers->getDate('Date', new \DateTime());
616
    }
617
618
    /**
619
     * Sets the Date header.
620
     *
621
     * @param \DateTime $date A \DateTime instance
622
     *
623
     * @return $this
624
     */
625
    public function setDate(\DateTime $date)
626
    {
627
        $date->setTimezone(new \DateTimeZone('UTC'));
628
        $this->headers->set('Date', $date->format('D, d M Y H:i:s').' GMT');
629
630
        return $this;
631
    }
632
633
    /**
634
     * Returns the age of the response.
635
     *
636
     * @return int The age of the response in seconds
637
     */
638
    public function getAge()
639
    {
640
        if (null !== $age = $this->headers->get('Age')) {
641
            return (int) $age;
642
        }
643
644
        return max(time() - $this->getDate()->format('U'), 0);
645
    }
646
647
    /**
648
     * Marks the response stale by setting the Age header to be equal to the maximum age of the response.
649
     *
650
     * @return $this
651
     */
652
    public function expire()
653
    {
654
        if ($this->isFresh()) {
655
            $this->headers->set('Age', $this->getMaxAge());
656
        }
657
658
        return $this;
659
    }
660
661
    /**
662
     * Returns the value of the Expires header as a DateTime instance.
663
     *
664
     * @return \DateTime|null A DateTime instance or null if the header does not exist
665
     */
666
    public function getExpires()
667
    {
668
        try {
669
            return $this->headers->getDate('Expires');
670
        } catch (\RuntimeException $e) {
671
            // according to RFC 2616 invalid date formats (e.g. "0" and "-1") must be treated as in the past
672
            return \DateTime::createFromFormat(DATE_RFC2822, 'Sat, 01 Jan 00 00:00:00 +0000');
673
        }
674
    }
675
676
    /**
677
     * Sets the Expires HTTP header with a DateTime instance.
678
     *
679
     * Passing null as value will remove the header.
680
     *
681
     * @param \DateTime|null $date A \DateTime instance or null to remove the header
682
     *
683
     * @return $this
684
     */
685 View Code Duplication
    public function setExpires(\DateTime $date = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
686
    {
687
        if (null === $date) {
688
            $this->headers->remove('Expires');
689
        } else {
690
            $date = clone $date;
691
            $date->setTimezone(new \DateTimeZone('UTC'));
692
            $this->headers->set('Expires', $date->format('D, d M Y H:i:s').' GMT');
693
        }
694
695
        return $this;
696
    }
697
698
    /**
699
     * Returns the number of seconds after the time specified in the response's Date
700
     * header when the response should no longer be considered fresh.
701
     *
702
     * First, it checks for a s-maxage directive, then a max-age directive, and then it falls
703
     * back on an expires header. It returns null when no maximum age can be established.
704
     *
705
     * @return int|null Number of seconds
706
     */
707
    public function getMaxAge()
708
    {
709
        if ($this->headers->hasCacheControlDirective('s-maxage')) {
710
            return (int) $this->headers->getCacheControlDirective('s-maxage');
711
        }
712
713
        if ($this->headers->hasCacheControlDirective('max-age')) {
714
            return (int) $this->headers->getCacheControlDirective('max-age');
715
        }
716
717
        if (null !== $this->getExpires()) {
718
            return $this->getExpires()->format('U') - $this->getDate()->format('U');
719
        }
720
    }
721
722
    /**
723
     * Sets the number of seconds after which the response should no longer be considered fresh.
724
     *
725
     * This methods sets the Cache-Control max-age directive.
726
     *
727
     * @param int $value Number of seconds
728
     *
729
     * @return $this
730
     */
731
    public function setMaxAge($value)
732
    {
733
        $this->headers->addCacheControlDirective('max-age', $value);
734
735
        return $this;
736
    }
737
738
    /**
739
     * Sets the number of seconds after which the response should no longer be considered fresh by shared caches.
740
     *
741
     * This methods sets the Cache-Control s-maxage directive.
742
     *
743
     * @param int $value Number of seconds
744
     *
745
     * @return $this
746
     */
747
    public function setSharedMaxAge($value)
748
    {
749
        $this->setPublic();
750
        $this->headers->addCacheControlDirective('s-maxage', $value);
751
752
        return $this;
753
    }
754
755
    /**
756
     * Returns the response's time-to-live in seconds.
757
     *
758
     * It returns null when no freshness information is present in the response.
759
     *
760
     * When the responses TTL is <= 0, the response may not be served from cache without first
761
     * revalidating with the origin.
762
     *
763
     * @return int|null The TTL in seconds
764
     */
765
    public function getTtl()
766
    {
767
        if (null !== $maxAge = $this->getMaxAge()) {
768
            return $maxAge - $this->getAge();
769
        }
770
    }
771
772
    /**
773
     * Sets the response's time-to-live for shared caches.
774
     *
775
     * This method adjusts the Cache-Control/s-maxage directive.
776
     *
777
     * @param int $seconds Number of seconds
778
     *
779
     * @return $this
780
     */
781
    public function setTtl($seconds)
782
    {
783
        $this->setSharedMaxAge($this->getAge() + $seconds);
784
785
        return $this;
786
    }
787
788
    /**
789
     * Sets the response's time-to-live for private/client caches.
790
     *
791
     * This method adjusts the Cache-Control/max-age directive.
792
     *
793
     * @param int $seconds Number of seconds
794
     *
795
     * @return $this
796
     */
797
    public function setClientTtl($seconds)
798
    {
799
        $this->setMaxAge($this->getAge() + $seconds);
800
801
        return $this;
802
    }
803
804
    /**
805
     * Returns the Last-Modified HTTP header as a DateTime instance.
806
     *
807
     * @return \DateTime|null A DateTime instance or null if the header does not exist
808
     *
809
     * @throws \RuntimeException When the HTTP header is not parseable
810
     */
811
    public function getLastModified()
812
    {
813
        return $this->headers->getDate('Last-Modified');
814
    }
815
816
    /**
817
     * Sets the Last-Modified HTTP header with a DateTime instance.
818
     *
819
     * Passing null as value will remove the header.
820
     *
821
     * @param \DateTime|null $date A \DateTime instance or null to remove the header
822
     *
823
     * @return $this
824
     */
825 View Code Duplication
    public function setLastModified(\DateTime $date = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
826
    {
827
        if (null === $date) {
828
            $this->headers->remove('Last-Modified');
829
        } else {
830
            $date = clone $date;
831
            $date->setTimezone(new \DateTimeZone('UTC'));
832
            $this->headers->set('Last-Modified', $date->format('D, d M Y H:i:s').' GMT');
833
        }
834
835
        return $this;
836
    }
837
838
    /**
839
     * Returns the literal value of the ETag HTTP header.
840
     *
841
     * @return string|null The ETag HTTP header or null if it does not exist
842
     */
843
    public function getEtag()
844
    {
845
        return $this->headers->get('ETag');
846
    }
847
848
    /**
849
     * Sets the ETag value.
850
     *
851
     * @param string|null $etag The ETag unique identifier or null to remove the header
852
     * @param bool        $weak Whether you want a weak ETag or not
853
     *
854
     * @return $this
855
     */
856
    public function setEtag($etag = null, $weak = false)
857
    {
858
        if (null === $etag) {
859
            $this->headers->remove('Etag');
860
        } else {
861
            if (0 !== strpos($etag, '"')) {
862
                $etag = '"'.$etag.'"';
863
            }
864
865
            $this->headers->set('ETag', (true === $weak ? 'W/' : '').$etag);
866
        }
867
868
        return $this;
869
    }
870
871
    /**
872
     * Sets the response's cache headers (validation and/or expiration).
873
     *
874
     * Available options are: etag, last_modified, max_age, s_maxage, private, and public.
875
     *
876
     * @param array $options An array of cache options
877
     *
878
     * @return $this
879
     *
880
     * @throws \InvalidArgumentException
881
     */
882
    public function setCache(array $options)
883
    {
884
        if ($diff = array_diff(array_keys($options), array('etag', 'last_modified', 'max_age', 's_maxage', 'private', 'public'))) {
885
            throw new \InvalidArgumentException(sprintf('Response does not support the following options: "%s".', implode('", "', array_values($diff))));
886
        }
887
888
        if (isset($options['etag'])) {
889
            $this->setEtag($options['etag']);
890
        }
891
892
        if (isset($options['last_modified'])) {
893
            $this->setLastModified($options['last_modified']);
894
        }
895
896
        if (isset($options['max_age'])) {
897
            $this->setMaxAge($options['max_age']);
898
        }
899
900
        if (isset($options['s_maxage'])) {
901
            $this->setSharedMaxAge($options['s_maxage']);
902
        }
903
904
        if (isset($options['public'])) {
905
            if ($options['public']) {
906
                $this->setPublic();
907
            } else {
908
                $this->setPrivate();
909
            }
910
        }
911
912
        if (isset($options['private'])) {
913
            if ($options['private']) {
914
                $this->setPrivate();
915
            } else {
916
                $this->setPublic();
917
            }
918
        }
919
920
        return $this;
921
    }
922
923
    /**
924
     * Modifies the response so that it conforms to the rules defined for a 304 status code.
925
     *
926
     * This sets the status, removes the body, and discards any headers
927
     * that MUST NOT be included in 304 responses.
928
     *
929
     * @return $this
930
     *
931
     * @see http://tools.ietf.org/html/rfc2616#section-10.3.5
932
     */
933
    public function setNotModified()
934
    {
935
        $this->setStatusCode(304);
936
        $this->setContent(null);
937
938
        // remove headers that MUST NOT be included with 304 Not Modified responses
939
        foreach (array('Allow', 'Content-Encoding', 'Content-Language', 'Content-Length', 'Content-MD5', 'Content-Type', 'Last-Modified') as $header) {
940
            $this->headers->remove($header);
941
        }
942
943
        return $this;
944
    }
945
946
    /**
947
     * Returns true if the response includes a Vary header.
948
     *
949
     * @return bool true if the response includes a Vary header, false otherwise
950
     */
951
    public function hasVary()
952
    {
953
        return null !== $this->headers->get('Vary');
954
    }
955
956
    /**
957
     * Returns an array of header names given in the Vary header.
958
     *
959
     * @return array An array of Vary names
960
     */
961
    public function getVary()
962
    {
963
        if (!$vary = $this->headers->get('Vary', null, false)) {
964
            return array();
965
        }
966
967
        $ret = array();
968
        foreach ($vary as $item) {
0 ignored issues
show
Bug introduced by
The expression $vary of type string|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
969
            $ret = array_merge($ret, preg_split('/[\s,]+/', $item));
970
        }
971
972
        return $ret;
973
    }
974
975
    /**
976
     * Sets the Vary header.
977
     *
978
     * @param string|array $headers
979
     * @param bool         $replace Whether to replace the actual value or not (true by default)
980
     *
981
     * @return $this
982
     */
983
    public function setVary($headers, $replace = true)
984
    {
985
        $this->headers->set('Vary', $headers, $replace);
986
987
        return $this;
988
    }
989
990
    /**
991
     * Determines if the Response validators (ETag, Last-Modified) match
992
     * a conditional value specified in the Request.
993
     *
994
     * If the Response is not modified, it sets the status code to 304 and
995
     * removes the actual content by calling the setNotModified() method.
996
     *
997
     * @param Request $request A Request instance
998
     *
999
     * @return bool true if the Response validators match the Request, false otherwise
1000
     */
1001
    public function isNotModified(Request $request)
1002
    {
1003
        if (!$request->isMethodCacheable()) {
1004
            return false;
1005
        }
1006
1007
        $notModified = false;
1008
        $lastModified = $this->headers->get('Last-Modified');
1009
        $modifiedSince = $request->headers->get('If-Modified-Since');
1010
1011
        if ($etags = $request->getETags()) {
1012
            $notModified = in_array($this->getEtag(), $etags) || in_array('*', $etags);
1013
        }
1014
1015
        if ($modifiedSince && $lastModified) {
1016
            $notModified = strtotime($modifiedSince) >= strtotime($lastModified) && (!$etags || $notModified);
0 ignored issues
show
Bug Best Practice introduced by
The expression $etags of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1017
        }
1018
1019
        if ($notModified) {
1020
            $this->setNotModified();
1021
        }
1022
1023
        return $notModified;
1024
    }
1025
1026
    /**
1027
     * Is response invalid?
1028
     *
1029
     * @return bool
1030
     *
1031
     * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
1032
     */
1033
    public function isInvalid()
1034
    {
1035
        return $this->statusCode < 100 || $this->statusCode >= 600;
1036
    }
1037
1038
    /**
1039
     * Is response informative?
1040
     *
1041
     * @return bool
1042
     */
1043
    public function isInformational()
1044
    {
1045
        return $this->statusCode >= 100 && $this->statusCode < 200;
1046
    }
1047
1048
    /**
1049
     * Is response successful?
1050
     *
1051
     * @return bool
1052
     */
1053
    public function isSuccessful()
1054
    {
1055
        return $this->statusCode >= 200 && $this->statusCode < 300;
1056
    }
1057
1058
    /**
1059
     * Is the response a redirect?
1060
     *
1061
     * @return bool
1062
     */
1063
    public function isRedirection()
1064
    {
1065
        return $this->statusCode >= 300 && $this->statusCode < 400;
1066
    }
1067
1068
    /**
1069
     * Is there a client error?
1070
     *
1071
     * @return bool
1072
     */
1073
    public function isClientError()
1074
    {
1075
        return $this->statusCode >= 400 && $this->statusCode < 500;
1076
    }
1077
1078
    /**
1079
     * Was there a server side error?
1080
     *
1081
     * @return bool
1082
     */
1083
    public function isServerError()
1084
    {
1085
        return $this->statusCode >= 500 && $this->statusCode < 600;
1086
    }
1087
1088
    /**
1089
     * Is the response OK?
1090
     *
1091
     * @return bool
1092
     */
1093
    public function isOk()
1094
    {
1095
        return 200 === $this->statusCode;
1096
    }
1097
1098
    /**
1099
     * Is the response forbidden?
1100
     *
1101
     * @return bool
1102
     */
1103
    public function isForbidden()
1104
    {
1105
        return 403 === $this->statusCode;
1106
    }
1107
1108
    /**
1109
     * Is the response a not found error?
1110
     *
1111
     * @return bool
1112
     */
1113
    public function isNotFound()
1114
    {
1115
        return 404 === $this->statusCode;
1116
    }
1117
1118
    /**
1119
     * Is the response a redirect of some form?
1120
     *
1121
     * @param string $location
1122
     *
1123
     * @return bool
1124
     */
1125
    public function isRedirect($location = null)
1126
    {
1127
        return in_array($this->statusCode, array(201, 301, 302, 303, 307, 308)) && (null === $location ?: $location == $this->headers->get('Location'));
1128
    }
1129
1130
    /**
1131
     * Is the response empty?
1132
     *
1133
     * @return bool
1134
     */
1135
    public function isEmpty()
1136
    {
1137
        return in_array($this->statusCode, array(204, 304));
1138
    }
1139
1140
    /**
1141
     * Cleans or flushes output buffers up to target level.
1142
     *
1143
     * Resulting level can be greater than target level if a non-removable buffer has been encountered.
1144
     *
1145
     * @param int  $targetLevel The target output buffering level
1146
     * @param bool $flush       Whether to flush or clean the buffers
1147
     */
1148
    public static function closeOutputBuffers($targetLevel, $flush)
1149
    {
1150
        $status = ob_get_status(true);
1151
        $level = count($status);
1152
        $flags = defined('PHP_OUTPUT_HANDLER_REMOVABLE') ? PHP_OUTPUT_HANDLER_REMOVABLE | ($flush ? PHP_OUTPUT_HANDLER_FLUSHABLE : PHP_OUTPUT_HANDLER_CLEANABLE) : -1;
1153
1154
        while ($level-- > $targetLevel && ($s = $status[$level]) && (!isset($s['del']) ? !isset($s['flags']) || $flags === ($s['flags'] & $flags) : $s['del'])) {
1155
            if ($flush) {
1156
                ob_end_flush();
1157
            } else {
1158
                ob_end_clean();
1159
            }
1160
        }
1161
    }
1162
1163
    /**
1164
     * Checks if we need to remove Cache-Control for SSL encrypted downloads when using IE < 9.
1165
     *
1166
     * @see http://support.microsoft.com/kb/323308
1167
     */
1168
    protected function ensureIEOverSSLCompatibility(Request $request)
1169
    {
1170
        if (false !== stripos($this->headers->get('Content-Disposition'), 'attachment') && preg_match('/MSIE (.*?);/i', $request->server->get('HTTP_USER_AGENT'), $match) == 1 && true === $request->isSecure()) {
1171
            if ((int) preg_replace('/(MSIE )(.*?);/', '$2', $match[0]) < 9) {
1172
                $this->headers->remove('Cache-Control');
1173
            }
1174
        }
1175
    }
1176
}
1177