Completed
Push — composer-installed ( 5832b4 )
by Ilia
08:49
created

Response::prepare()   F

Complexity

Conditions 16
Paths 436

Size

Total Lines 56

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 16
nc 436
nop 1
dl 0
loc 56
rs 2.1833
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
        307 => 'Temporary Redirect',
146
        308 => 'Permanent Redirect',    // RFC7238
147
        400 => 'Bad Request',
148
        401 => 'Unauthorized',
149
        402 => 'Payment Required',
150
        403 => 'Forbidden',
151
        404 => 'Not Found',
152
        405 => 'Method Not Allowed',
153
        406 => 'Not Acceptable',
154
        407 => 'Proxy Authentication Required',
155
        408 => 'Request Timeout',
156
        409 => 'Conflict',
157
        410 => 'Gone',
158
        411 => 'Length Required',
159
        412 => 'Precondition Failed',
160
        413 => 'Payload Too Large',
161
        414 => 'URI Too Long',
162
        415 => 'Unsupported Media Type',
163
        416 => 'Range Not Satisfiable',
164
        417 => 'Expectation Failed',
165
        418 => 'I\'m a teapot',                                               // RFC2324
166
        421 => 'Misdirected Request',                                         // RFC7540
167
        422 => 'Unprocessable Entity',                                        // RFC4918
168
        423 => 'Locked',                                                      // RFC4918
169
        424 => 'Failed Dependency',                                           // RFC4918
170
        425 => 'Reserved for WebDAV advanced collections expired proposal',   // RFC2817
171
        426 => 'Upgrade Required',                                            // RFC2817
172
        428 => 'Precondition Required',                                       // RFC6585
173
        429 => 'Too Many Requests',                                           // RFC6585
174
        431 => 'Request Header Fields Too Large',                             // RFC6585
175
        451 => 'Unavailable For Legal Reasons',                               // RFC7725
176
        500 => 'Internal Server Error',
177
        501 => 'Not Implemented',
178
        502 => 'Bad Gateway',
179
        503 => 'Service Unavailable',
180
        504 => 'Gateway Timeout',
181
        505 => 'HTTP Version Not Supported',
182
        506 => 'Variant Also Negotiates',                                     // RFC2295
183
        507 => 'Insufficient Storage',                                        // RFC4918
184
        508 => 'Loop Detected',                                               // RFC5842
185
        510 => 'Not Extended',                                                // RFC2774
186
        511 => 'Network Authentication Required',                             // RFC6585
187
    );
188
189
    /**
190
     * Constructor.
191
     *
192
     * @param mixed $content The response content, see setContent()
193
     * @param int   $status  The response status code
194
     * @param array $headers An array of response headers
195
     *
196
     * @throws \InvalidArgumentException When the HTTP status code is not valid
197
     */
198
    public function __construct($content = '', $status = 200, $headers = array())
199
    {
200
        $this->headers = new ResponseHeaderBag($headers);
201
        $this->setContent($content);
202
        $this->setStatusCode($status);
203
        $this->setProtocolVersion('1.0');
204
205
        /* RFC2616 - 14.18 says all Responses need to have a Date */
206
        if (!$this->headers->has('Date')) {
207
            $this->setDate(\DateTime::createFromFormat('U', time()));
0 ignored issues
show
Security Bug introduced by
It seems like \DateTime::createFromFormat('U', time()) 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...
208
        }
209
    }
210
211
    /**
212
     * Factory method for chainability.
213
     *
214
     * Example:
215
     *
216
     *     return Response::create($body, 200)
217
     *         ->setSharedMaxAge(300);
218
     *
219
     * @param mixed $content The response content, see setContent()
220
     * @param int   $status  The response status code
221
     * @param array $headers An array of response headers
222
     *
223
     * @return static
224
     */
225
    public static function create($content = '', $status = 200, $headers = array())
226
    {
227
        return new static($content, $status, $headers);
228
    }
229
230
    /**
231
     * Returns the Response as an HTTP string.
232
     *
233
     * The string representation of the Response is the same as the
234
     * one that will be sent to the client only if the prepare() method
235
     * has been called before.
236
     *
237
     * @return string The Response as an HTTP string
238
     *
239
     * @see prepare()
240
     */
241
    public function __toString()
242
    {
243
        return
244
            sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)."\r\n".
245
            $this->headers."\r\n".
246
            $this->getContent();
247
    }
248
249
    /**
250
     * Clones the current Response instance.
251
     */
252
    public function __clone()
253
    {
254
        $this->headers = clone $this->headers;
255
    }
256
257
    /**
258
     * Prepares the Response before it is sent to the client.
259
     *
260
     * This method tweaks the Response to ensure that it is
261
     * compliant with RFC 2616. Most of the changes are based on
262
     * the Request that is "associated" with this Response.
263
     *
264
     * @param Request $request A Request instance
265
     *
266
     * @return $this
267
     */
268
    public function prepare(Request $request)
269
    {
270
        $headers = $this->headers;
271
272
        if ($this->isInformational() || $this->isEmpty()) {
273
            $this->setContent(null);
274
            $headers->remove('Content-Type');
275
            $headers->remove('Content-Length');
276
        } else {
277
            // Content-type based on the Request
278
            if (!$headers->has('Content-Type')) {
279
                $format = $request->getRequestFormat();
280
                if (null !== $format && $mimeType = $request->getMimeType($format)) {
281
                    $headers->set('Content-Type', $mimeType);
282
                }
283
            }
284
285
            // Fix Content-Type
286
            $charset = $this->charset ?: 'UTF-8';
287
            if (!$headers->has('Content-Type')) {
288
                $headers->set('Content-Type', 'text/html; charset='.$charset);
289
            } elseif (0 === stripos($headers->get('Content-Type'), 'text/') && false === stripos($headers->get('Content-Type'), 'charset')) {
290
                // add the charset
291
                $headers->set('Content-Type', $headers->get('Content-Type').'; charset='.$charset);
292
            }
293
294
            // Fix Content-Length
295
            if ($headers->has('Transfer-Encoding')) {
296
                $headers->remove('Content-Length');
297
            }
298
299
            if ($request->isMethod('HEAD')) {
300
                // cf. RFC2616 14.13
301
                $length = $headers->get('Content-Length');
302
                $this->setContent(null);
303
                if ($length) {
304
                    $headers->set('Content-Length', $length);
305
                }
306
            }
307
        }
308
309
        // Fix protocol
310
        if ('HTTP/1.0' != $request->server->get('SERVER_PROTOCOL')) {
311
            $this->setProtocolVersion('1.1');
312
        }
313
314
        // Check if we need to send extra expire info headers
315
        if ('1.0' == $this->getProtocolVersion() && 'no-cache' == $this->headers->get('Cache-Control')) {
316
            $this->headers->set('pragma', 'no-cache');
317
            $this->headers->set('expires', -1);
318
        }
319
320
        $this->ensureIEOverSSLCompatibility($request);
321
322
        return $this;
323
    }
324
325
    /**
326
     * Sends HTTP headers.
327
     *
328
     * @return $this
329
     */
330
    public function sendHeaders()
331
    {
332
        // headers have already been sent by the developer
333
        if (headers_sent()) {
334
            return $this;
335
        }
336
337
        /* RFC2616 - 14.18 says all Responses need to have a Date */
338
        if (!$this->headers->has('Date')) {
339
            $this->setDate(\DateTime::createFromFormat('U', time()));
0 ignored issues
show
Security Bug introduced by
It seems like \DateTime::createFromFormat('U', time()) 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...
340
        }
341
342
        // headers
343
        foreach ($this->headers->allPreserveCase() as $name => $values) {
344
            foreach ($values as $value) {
345
                header($name.': '.$value, false, $this->statusCode);
346
            }
347
        }
348
349
        // status
350
        header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode);
351
352
        // cookies
353
        foreach ($this->headers->getCookies() as $cookie) {
354
            setcookie($cookie->getName(), $cookie->getValue(), $cookie->getExpiresTime(), $cookie->getPath(), $cookie->getDomain(), $cookie->isSecure(), $cookie->isHttpOnly());
355
        }
356
357
        return $this;
358
    }
359
360
    /**
361
     * Sends content for the current web response.
362
     *
363
     * @return $this
364
     */
365
    public function sendContent()
366
    {
367
        echo $this->content;
368
369
        return $this;
370
    }
371
372
    /**
373
     * Sends HTTP headers and content.
374
     *
375
     * @return $this
376
     */
377
    public function send()
378
    {
379
        $this->sendHeaders();
380
        $this->sendContent();
381
382
        if (function_exists('fastcgi_finish_request')) {
383
            fastcgi_finish_request();
384
        } elseif ('cli' !== PHP_SAPI) {
385
            static::closeOutputBuffers(0, true);
386
        }
387
388
        return $this;
389
    }
390
391
    /**
392
     * Sets the response content.
393
     *
394
     * Valid types are strings, numbers, null, and objects that implement a __toString() method.
395
     *
396
     * @param mixed $content Content that can be cast to string
397
     *
398
     * @return $this
399
     *
400
     * @throws \UnexpectedValueException
401
     */
402
    public function setContent($content)
403
    {
404
        if (null !== $content && !is_string($content) && !is_numeric($content) && !is_callable(array($content, '__toString'))) {
405
            throw new \UnexpectedValueException(sprintf('The Response content must be a string or object implementing __toString(), "%s" given.', gettype($content)));
406
        }
407
408
        $this->content = (string) $content;
409
410
        return $this;
411
    }
412
413
    /**
414
     * Gets the current response content.
415
     *
416
     * @return string Content
417
     */
418
    public function getContent()
419
    {
420
        return $this->content;
421
    }
422
423
    /**
424
     * Sets the HTTP protocol version (1.0 or 1.1).
425
     *
426
     * @param string $version The HTTP protocol version
427
     *
428
     * @return $this
429
     */
430
    public function setProtocolVersion($version)
431
    {
432
        $this->version = $version;
433
434
        return $this;
435
    }
436
437
    /**
438
     * Gets the HTTP protocol version.
439
     *
440
     * @return string The HTTP protocol version
441
     */
442
    public function getProtocolVersion()
443
    {
444
        return $this->version;
445
    }
446
447
    /**
448
     * Sets the response status code.
449
     *
450
     * @param int   $code HTTP status code
451
     * @param mixed $text HTTP status text
452
     *
453
     * If the status text is null it will be automatically populated for the known
454
     * status codes and left empty otherwise.
455
     *
456
     * @return $this
457
     *
458
     * @throws \InvalidArgumentException When the HTTP status code is not valid
459
     */
460
    public function setStatusCode($code, $text = null)
461
    {
462
        $this->statusCode = $code = (int) $code;
463
        if ($this->isInvalid()) {
464
            throw new \InvalidArgumentException(sprintf('The HTTP status code "%s" is not valid.', $code));
465
        }
466
467
        if (null === $text) {
468
            $this->statusText = isset(self::$statusTexts[$code]) ? self::$statusTexts[$code] : 'unknown status';
469
470
            return $this;
471
        }
472
473
        if (false === $text) {
474
            $this->statusText = '';
475
476
            return $this;
477
        }
478
479
        $this->statusText = $text;
480
481
        return $this;
482
    }
483
484
    /**
485
     * Retrieves the status code for the current web response.
486
     *
487
     * @return int Status code
488
     */
489
    public function getStatusCode()
490
    {
491
        return $this->statusCode;
492
    }
493
494
    /**
495
     * Sets the response charset.
496
     *
497
     * @param string $charset Character set
498
     *
499
     * @return $this
500
     */
501
    public function setCharset($charset)
502
    {
503
        $this->charset = $charset;
504
505
        return $this;
506
    }
507
508
    /**
509
     * Retrieves the response charset.
510
     *
511
     * @return string Character set
512
     */
513
    public function getCharset()
514
    {
515
        return $this->charset;
516
    }
517
518
    /**
519
     * Returns true if the response is worth caching under any circumstance.
520
     *
521
     * Responses marked "private" with an explicit Cache-Control directive are
522
     * considered uncacheable.
523
     *
524
     * Responses with neither a freshness lifetime (Expires, max-age) nor cache
525
     * validator (Last-Modified, ETag) are considered uncacheable.
526
     *
527
     * @return bool true if the response is worth caching, false otherwise
528
     */
529
    public function isCacheable()
530
    {
531
        if (!in_array($this->statusCode, array(200, 203, 300, 301, 302, 404, 410))) {
532
            return false;
533
        }
534
535
        if ($this->headers->hasCacheControlDirective('no-store') || $this->headers->getCacheControlDirective('private')) {
536
            return false;
537
        }
538
539
        return $this->isValidateable() || $this->isFresh();
540
    }
541
542
    /**
543
     * Returns true if the response is "fresh".
544
     *
545
     * Fresh responses may be served from cache without any interaction with the
546
     * origin. A response is considered fresh when it includes a Cache-Control/max-age
547
     * indicator or Expires header and the calculated age is less than the freshness lifetime.
548
     *
549
     * @return bool true if the response is fresh, false otherwise
550
     */
551
    public function isFresh()
552
    {
553
        return $this->getTtl() > 0;
554
    }
555
556
    /**
557
     * Returns true if the response includes headers that can be used to validate
558
     * the response with the origin server using a conditional GET request.
559
     *
560
     * @return bool true if the response is validateable, false otherwise
561
     */
562
    public function isValidateable()
563
    {
564
        return $this->headers->has('Last-Modified') || $this->headers->has('ETag');
565
    }
566
567
    /**
568
     * Marks the response as "private".
569
     *
570
     * It makes the response ineligible for serving other clients.
571
     *
572
     * @return $this
573
     */
574
    public function setPrivate()
575
    {
576
        $this->headers->removeCacheControlDirective('public');
577
        $this->headers->addCacheControlDirective('private');
578
579
        return $this;
580
    }
581
582
    /**
583
     * Marks the response as "public".
584
     *
585
     * It makes the response eligible for serving other clients.
586
     *
587
     * @return $this
588
     */
589
    public function setPublic()
590
    {
591
        $this->headers->addCacheControlDirective('public');
592
        $this->headers->removeCacheControlDirective('private');
593
594
        return $this;
595
    }
596
597
    /**
598
     * Returns true if the response must be revalidated by caches.
599
     *
600
     * This method indicates that the response must not be served stale by a
601
     * cache in any circumstance without first revalidating with the origin.
602
     * When present, the TTL of the response should not be overridden to be
603
     * greater than the value provided by the origin.
604
     *
605
     * @return bool true if the response must be revalidated by a cache, false otherwise
606
     */
607
    public function mustRevalidate()
608
    {
609
        return $this->headers->hasCacheControlDirective('must-revalidate') || $this->headers->hasCacheControlDirective('proxy-revalidate');
610
    }
611
612
    /**
613
     * Returns the Date header as a DateTime instance.
614
     *
615
     * @return \DateTime A \DateTime instance
616
     *
617
     * @throws \RuntimeException When the header is not parseable
618
     */
619
    public function getDate()
620
    {
621
        /*
622
            RFC2616 - 14.18 says all Responses need to have a Date.
623
            Make sure we provide one even if it the header
624
            has been removed in the meantime.
625
         */
626
        if (!$this->headers->has('Date')) {
627
            $this->setDate(\DateTime::createFromFormat('U', time()));
0 ignored issues
show
Security Bug introduced by
It seems like \DateTime::createFromFormat('U', time()) 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...
628
        }
629
630
        return $this->headers->getDate('Date');
631
    }
632
633
    /**
634
     * Sets the Date header.
635
     *
636
     * @param \DateTime $date A \DateTime instance
637
     *
638
     * @return $this
639
     */
640
    public function setDate(\DateTime $date)
641
    {
642
        $date->setTimezone(new \DateTimeZone('UTC'));
643
        $this->headers->set('Date', $date->format('D, d M Y H:i:s').' GMT');
644
645
        return $this;
646
    }
647
648
    /**
649
     * Returns the age of the response.
650
     *
651
     * @return int The age of the response in seconds
652
     */
653
    public function getAge()
654
    {
655
        if (null !== $age = $this->headers->get('Age')) {
656
            return (int) $age;
657
        }
658
659
        return max(time() - $this->getDate()->format('U'), 0);
660
    }
661
662
    /**
663
     * Marks the response stale by setting the Age header to be equal to the maximum age of the response.
664
     *
665
     * @return $this
666
     */
667
    public function expire()
668
    {
669
        if ($this->isFresh()) {
670
            $this->headers->set('Age', $this->getMaxAge());
671
        }
672
673
        return $this;
674
    }
675
676
    /**
677
     * Returns the value of the Expires header as a DateTime instance.
678
     *
679
     * @return \DateTime|null A DateTime instance or null if the header does not exist
680
     */
681
    public function getExpires()
682
    {
683
        try {
684
            return $this->headers->getDate('Expires');
685
        } catch (\RuntimeException $e) {
686
            // according to RFC 2616 invalid date formats (e.g. "0" and "-1") must be treated as in the past
687
            return \DateTime::createFromFormat(DATE_RFC2822, 'Sat, 01 Jan 00 00:00:00 +0000');
688
        }
689
    }
690
691
    /**
692
     * Sets the Expires HTTP header with a DateTime instance.
693
     *
694
     * Passing null as value will remove the header.
695
     *
696
     * @param \DateTime|null $date A \DateTime instance or null to remove the header
697
     *
698
     * @return $this
699
     */
700 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...
701
    {
702
        if (null === $date) {
703
            $this->headers->remove('Expires');
704
        } else {
705
            $date = clone $date;
706
            $date->setTimezone(new \DateTimeZone('UTC'));
707
            $this->headers->set('Expires', $date->format('D, d M Y H:i:s').' GMT');
708
        }
709
710
        return $this;
711
    }
712
713
    /**
714
     * Returns the number of seconds after the time specified in the response's Date
715
     * header when the response should no longer be considered fresh.
716
     *
717
     * First, it checks for a s-maxage directive, then a max-age directive, and then it falls
718
     * back on an expires header. It returns null when no maximum age can be established.
719
     *
720
     * @return int|null Number of seconds
721
     */
722
    public function getMaxAge()
723
    {
724
        if ($this->headers->hasCacheControlDirective('s-maxage')) {
725
            return (int) $this->headers->getCacheControlDirective('s-maxage');
726
        }
727
728
        if ($this->headers->hasCacheControlDirective('max-age')) {
729
            return (int) $this->headers->getCacheControlDirective('max-age');
730
        }
731
732
        if (null !== $this->getExpires()) {
733
            return $this->getExpires()->format('U') - $this->getDate()->format('U');
734
        }
735
    }
736
737
    /**
738
     * Sets the number of seconds after which the response should no longer be considered fresh.
739
     *
740
     * This methods sets the Cache-Control max-age directive.
741
     *
742
     * @param int $value Number of seconds
743
     *
744
     * @return $this
745
     */
746
    public function setMaxAge($value)
747
    {
748
        $this->headers->addCacheControlDirective('max-age', $value);
749
750
        return $this;
751
    }
752
753
    /**
754
     * Sets the number of seconds after which the response should no longer be considered fresh by shared caches.
755
     *
756
     * This methods sets the Cache-Control s-maxage directive.
757
     *
758
     * @param int $value Number of seconds
759
     *
760
     * @return $this
761
     */
762
    public function setSharedMaxAge($value)
763
    {
764
        $this->setPublic();
765
        $this->headers->addCacheControlDirective('s-maxage', $value);
766
767
        return $this;
768
    }
769
770
    /**
771
     * Returns the response's time-to-live in seconds.
772
     *
773
     * It returns null when no freshness information is present in the response.
774
     *
775
     * When the responses TTL is <= 0, the response may not be served from cache without first
776
     * revalidating with the origin.
777
     *
778
     * @return int|null The TTL in seconds
779
     */
780
    public function getTtl()
781
    {
782
        if (null !== $maxAge = $this->getMaxAge()) {
783
            return $maxAge - $this->getAge();
784
        }
785
    }
786
787
    /**
788
     * Sets the response's time-to-live for shared caches.
789
     *
790
     * This method adjusts the Cache-Control/s-maxage directive.
791
     *
792
     * @param int $seconds Number of seconds
793
     *
794
     * @return $this
795
     */
796
    public function setTtl($seconds)
797
    {
798
        $this->setSharedMaxAge($this->getAge() + $seconds);
799
800
        return $this;
801
    }
802
803
    /**
804
     * Sets the response's time-to-live for private/client caches.
805
     *
806
     * This method adjusts the Cache-Control/max-age directive.
807
     *
808
     * @param int $seconds Number of seconds
809
     *
810
     * @return $this
811
     */
812
    public function setClientTtl($seconds)
813
    {
814
        $this->setMaxAge($this->getAge() + $seconds);
815
816
        return $this;
817
    }
818
819
    /**
820
     * Returns the Last-Modified HTTP header as a DateTime instance.
821
     *
822
     * @return \DateTime|null A DateTime instance or null if the header does not exist
823
     *
824
     * @throws \RuntimeException When the HTTP header is not parseable
825
     */
826
    public function getLastModified()
827
    {
828
        return $this->headers->getDate('Last-Modified');
829
    }
830
831
    /**
832
     * Sets the Last-Modified HTTP header with a DateTime instance.
833
     *
834
     * Passing null as value will remove the header.
835
     *
836
     * @param \DateTime|null $date A \DateTime instance or null to remove the header
837
     *
838
     * @return $this
839
     */
840 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...
841
    {
842
        if (null === $date) {
843
            $this->headers->remove('Last-Modified');
844
        } else {
845
            $date = clone $date;
846
            $date->setTimezone(new \DateTimeZone('UTC'));
847
            $this->headers->set('Last-Modified', $date->format('D, d M Y H:i:s').' GMT');
848
        }
849
850
        return $this;
851
    }
852
853
    /**
854
     * Returns the literal value of the ETag HTTP header.
855
     *
856
     * @return string|null The ETag HTTP header or null if it does not exist
857
     */
858
    public function getEtag()
859
    {
860
        return $this->headers->get('ETag');
861
    }
862
863
    /**
864
     * Sets the ETag value.
865
     *
866
     * @param string|null $etag The ETag unique identifier or null to remove the header
867
     * @param bool        $weak Whether you want a weak ETag or not
868
     *
869
     * @return $this
870
     */
871
    public function setEtag($etag = null, $weak = false)
872
    {
873
        if (null === $etag) {
874
            $this->headers->remove('Etag');
875
        } else {
876
            if (0 !== strpos($etag, '"')) {
877
                $etag = '"'.$etag.'"';
878
            }
879
880
            $this->headers->set('ETag', (true === $weak ? 'W/' : '').$etag);
881
        }
882
883
        return $this;
884
    }
885
886
    /**
887
     * Sets the response's cache headers (validation and/or expiration).
888
     *
889
     * Available options are: etag, last_modified, max_age, s_maxage, private, and public.
890
     *
891
     * @param array $options An array of cache options
892
     *
893
     * @return $this
894
     *
895
     * @throws \InvalidArgumentException
896
     */
897
    public function setCache(array $options)
898
    {
899
        if ($diff = array_diff(array_keys($options), array('etag', 'last_modified', 'max_age', 's_maxage', 'private', 'public'))) {
900
            throw new \InvalidArgumentException(sprintf('Response does not support the following options: "%s".', implode('", "', array_values($diff))));
901
        }
902
903
        if (isset($options['etag'])) {
904
            $this->setEtag($options['etag']);
905
        }
906
907
        if (isset($options['last_modified'])) {
908
            $this->setLastModified($options['last_modified']);
909
        }
910
911
        if (isset($options['max_age'])) {
912
            $this->setMaxAge($options['max_age']);
913
        }
914
915
        if (isset($options['s_maxage'])) {
916
            $this->setSharedMaxAge($options['s_maxage']);
917
        }
918
919
        if (isset($options['public'])) {
920
            if ($options['public']) {
921
                $this->setPublic();
922
            } else {
923
                $this->setPrivate();
924
            }
925
        }
926
927
        if (isset($options['private'])) {
928
            if ($options['private']) {
929
                $this->setPrivate();
930
            } else {
931
                $this->setPublic();
932
            }
933
        }
934
935
        return $this;
936
    }
937
938
    /**
939
     * Modifies the response so that it conforms to the rules defined for a 304 status code.
940
     *
941
     * This sets the status, removes the body, and discards any headers
942
     * that MUST NOT be included in 304 responses.
943
     *
944
     * @return $this
945
     *
946
     * @see http://tools.ietf.org/html/rfc2616#section-10.3.5
947
     */
948
    public function setNotModified()
949
    {
950
        $this->setStatusCode(304);
951
        $this->setContent(null);
952
953
        // remove headers that MUST NOT be included with 304 Not Modified responses
954
        foreach (array('Allow', 'Content-Encoding', 'Content-Language', 'Content-Length', 'Content-MD5', 'Content-Type', 'Last-Modified') as $header) {
955
            $this->headers->remove($header);
956
        }
957
958
        return $this;
959
    }
960
961
    /**
962
     * Returns true if the response includes a Vary header.
963
     *
964
     * @return bool true if the response includes a Vary header, false otherwise
965
     */
966
    public function hasVary()
967
    {
968
        return null !== $this->headers->get('Vary');
969
    }
970
971
    /**
972
     * Returns an array of header names given in the Vary header.
973
     *
974
     * @return array An array of Vary names
975
     */
976
    public function getVary()
977
    {
978
        if (!$vary = $this->headers->get('Vary', null, false)) {
979
            return array();
980
        }
981
982
        $ret = array();
983
        foreach ($vary as $item) {
984
            $ret = array_merge($ret, preg_split('/[\s,]+/', $item));
985
        }
986
987
        return $ret;
988
    }
989
990
    /**
991
     * Sets the Vary header.
992
     *
993
     * @param string|array $headers
994
     * @param bool         $replace Whether to replace the actual value or not (true by default)
995
     *
996
     * @return $this
997
     */
998
    public function setVary($headers, $replace = true)
999
    {
1000
        $this->headers->set('Vary', $headers, $replace);
1001
1002
        return $this;
1003
    }
1004
1005
    /**
1006
     * Determines if the Response validators (ETag, Last-Modified) match
1007
     * a conditional value specified in the Request.
1008
     *
1009
     * If the Response is not modified, it sets the status code to 304 and
1010
     * removes the actual content by calling the setNotModified() method.
1011
     *
1012
     * @param Request $request A Request instance
1013
     *
1014
     * @return bool true if the Response validators match the Request, false otherwise
1015
     */
1016
    public function isNotModified(Request $request)
1017
    {
1018
        if (!$request->isMethodCacheable()) {
1019
            return false;
1020
        }
1021
1022
        $notModified = false;
1023
        $lastModified = $this->headers->get('Last-Modified');
1024
        $modifiedSince = $request->headers->get('If-Modified-Since');
1025
1026
        if ($etags = $request->getETags()) {
1027
            $notModified = in_array($this->getEtag(), $etags) || in_array('*', $etags);
1028
        }
1029
1030
        if ($modifiedSince && $lastModified) {
1031
            $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...
1032
        }
1033
1034
        if ($notModified) {
1035
            $this->setNotModified();
1036
        }
1037
1038
        return $notModified;
1039
    }
1040
1041
    /**
1042
     * Is response invalid?
1043
     *
1044
     * @return bool
1045
     *
1046
     * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
1047
     */
1048
    public function isInvalid()
1049
    {
1050
        return $this->statusCode < 100 || $this->statusCode >= 600;
1051
    }
1052
1053
    /**
1054
     * Is response informative?
1055
     *
1056
     * @return bool
1057
     */
1058
    public function isInformational()
1059
    {
1060
        return $this->statusCode >= 100 && $this->statusCode < 200;
1061
    }
1062
1063
    /**
1064
     * Is response successful?
1065
     *
1066
     * @return bool
1067
     */
1068
    public function isSuccessful()
1069
    {
1070
        return $this->statusCode >= 200 && $this->statusCode < 300;
1071
    }
1072
1073
    /**
1074
     * Is the response a redirect?
1075
     *
1076
     * @return bool
1077
     */
1078
    public function isRedirection()
1079
    {
1080
        return $this->statusCode >= 300 && $this->statusCode < 400;
1081
    }
1082
1083
    /**
1084
     * Is there a client error?
1085
     *
1086
     * @return bool
1087
     */
1088
    public function isClientError()
1089
    {
1090
        return $this->statusCode >= 400 && $this->statusCode < 500;
1091
    }
1092
1093
    /**
1094
     * Was there a server side error?
1095
     *
1096
     * @return bool
1097
     */
1098
    public function isServerError()
1099
    {
1100
        return $this->statusCode >= 500 && $this->statusCode < 600;
1101
    }
1102
1103
    /**
1104
     * Is the response OK?
1105
     *
1106
     * @return bool
1107
     */
1108
    public function isOk()
1109
    {
1110
        return 200 === $this->statusCode;
1111
    }
1112
1113
    /**
1114
     * Is the response forbidden?
1115
     *
1116
     * @return bool
1117
     */
1118
    public function isForbidden()
1119
    {
1120
        return 403 === $this->statusCode;
1121
    }
1122
1123
    /**
1124
     * Is the response a not found error?
1125
     *
1126
     * @return bool
1127
     */
1128
    public function isNotFound()
1129
    {
1130
        return 404 === $this->statusCode;
1131
    }
1132
1133
    /**
1134
     * Is the response a redirect of some form?
1135
     *
1136
     * @param string $location
1137
     *
1138
     * @return bool
1139
     */
1140
    public function isRedirect($location = null)
1141
    {
1142
        return in_array($this->statusCode, array(201, 301, 302, 303, 307, 308)) && (null === $location ?: $location == $this->headers->get('Location'));
1143
    }
1144
1145
    /**
1146
     * Is the response empty?
1147
     *
1148
     * @return bool
1149
     */
1150
    public function isEmpty()
1151
    {
1152
        return in_array($this->statusCode, array(204, 304));
1153
    }
1154
1155
    /**
1156
     * Cleans or flushes output buffers up to target level.
1157
     *
1158
     * Resulting level can be greater than target level if a non-removable buffer has been encountered.
1159
     *
1160
     * @param int  $targetLevel The target output buffering level
1161
     * @param bool $flush       Whether to flush or clean the buffers
1162
     */
1163
    public static function closeOutputBuffers($targetLevel, $flush)
1164
    {
1165
        $status = ob_get_status(true);
1166
        $level = count($status);
1167
        $flags = defined('PHP_OUTPUT_HANDLER_REMOVABLE') ? PHP_OUTPUT_HANDLER_REMOVABLE | ($flush ? PHP_OUTPUT_HANDLER_FLUSHABLE : PHP_OUTPUT_HANDLER_CLEANABLE) : -1;
1168
1169
        while ($level-- > $targetLevel && ($s = $status[$level]) && (!isset($s['del']) ? !isset($s['flags']) || $flags === ($s['flags'] & $flags) : $s['del'])) {
1170
            if ($flush) {
1171
                ob_end_flush();
1172
            } else {
1173
                ob_end_clean();
1174
            }
1175
        }
1176
    }
1177
1178
    /**
1179
     * Checks if we need to remove Cache-Control for SSL encrypted downloads when using IE < 9.
1180
     *
1181
     * @see http://support.microsoft.com/kb/323308
1182
     */
1183
    protected function ensureIEOverSSLCompatibility(Request $request)
1184
    {
1185
        if (false !== stripos($this->headers->get('Content-Disposition'), 'attachment') && preg_match('/MSIE (.*?);/i', $request->server->get('HTTP_USER_AGENT'), $match) == 1 && true === $request->isSecure()) {
1186
            if ((int) preg_replace('/(MSIE )(.*?);/', '$2', $match[0]) < 9) {
1187
                $this->headers->remove('Cache-Control');
1188
            }
1189
        }
1190
    }
1191
}
1192