Passed
Pull Request — master (#2)
by
unknown
15:51
created

Response::prepare()   D

Complexity

Conditions 16
Paths 220

Size

Total Lines 55
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 16
eloc 29
c 0
b 0
f 0
nc 220
nop 1
dl 0
loc 55
rs 4.4833

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
        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')));
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
        foreach ($this->headers->getCookies() as $cookie) {
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')) {
0 ignored issues
show
introduced by
The condition null !== $age = $this->headers->get('Age') is always true.
Loading history...
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
    public function setExpires(\DateTime $date = null)
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
    public function setLastModified(\DateTime $date = null)
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) {
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);
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