Projekt

Obecné

Profil

Stáhnout (34.6 KB) Statistiky
| Větev: | Revize:
1
<?php
2

    
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <fabien@symfony.com>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11

    
12
namespace Symfony\Component\HttpFoundation;
13

    
14
/**
15
 * Response represents an HTTP response.
16
 *
17
 * @author Fabien Potencier <fabien@symfony.com>
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 (Experimental)',                      // 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

    
206
    /**
207
     * Factory method for chainability.
208
     *
209
     * Example:
210
     *
211
     *     return Response::create($body, 200)
212
     *         ->setSharedMaxAge(300);
213
     *
214
     * @param mixed $content The response content, see setContent()
215
     * @param int   $status  The response status code
216
     * @param array $headers An array of response headers
217
     *
218
     * @return Response
219
     */
220
    public static function create($content = '', $status = 200, $headers = array())
221
    {
222
        return new static($content, $status, $headers);
223
    }
224

    
225
    /**
226
     * Returns the Response as an HTTP string.
227
     *
228
     * The string representation of the Response is the same as the
229
     * one that will be sent to the client only if the prepare() method
230
     * has been called before.
231
     *
232
     * @return string The Response as an HTTP string
233
     *
234
     * @see prepare()
235
     */
236
    public function __toString()
237
    {
238
        return
239
            sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)."\r\n".
240
            $this->headers."\r\n".
241
            $this->getContent();
242
    }
243

    
244
    /**
245
     * Clones the current Response instance.
246
     */
247
    public function __clone()
248
    {
249
        $this->headers = clone $this->headers;
250
    }
251

    
252
    /**
253
     * Prepares the Response before it is sent to the client.
254
     *
255
     * This method tweaks the Response to ensure that it is
256
     * compliant with RFC 2616. Most of the changes are based on
257
     * the Request that is "associated" with this Response.
258
     *
259
     * @param Request $request A Request instance
260
     *
261
     * @return Response The current response
262
     */
263
    public function prepare(Request $request)
264
    {
265
        $headers = $this->headers;
266

    
267
        if ($this->isInformational() || $this->isEmpty()) {
268
            $this->setContent(null);
269
            $headers->remove('Content-Type');
270
            $headers->remove('Content-Length');
271
        } else {
272
            // Content-type based on the Request
273
            if (!$headers->has('Content-Type')) {
274
                $format = $request->getRequestFormat();
275
                if (null !== $format && $mimeType = $request->getMimeType($format)) {
276
                    $headers->set('Content-Type', $mimeType);
277
                }
278
            }
279

    
280
            // Fix Content-Type
281
            $charset = $this->charset ?: 'UTF-8';
282
            if (!$headers->has('Content-Type')) {
283
                $headers->set('Content-Type', 'text/html; charset='.$charset);
284
            } elseif (0 === stripos($headers->get('Content-Type'), 'text/') && false === stripos($headers->get('Content-Type'), 'charset')) {
285
                // add the charset
286
                $headers->set('Content-Type', $headers->get('Content-Type').'; charset='.$charset);
287
            }
288

    
289
            // Fix Content-Length
290
            if ($headers->has('Transfer-Encoding')) {
291
                $headers->remove('Content-Length');
292
            }
293

    
294
            if ($request->isMethod('HEAD')) {
295
                // cf. RFC2616 14.13
296
                $length = $headers->get('Content-Length');
297
                $this->setContent(null);
298
                if ($length) {
299
                    $headers->set('Content-Length', $length);
300
                }
301
            }
302
        }
303

    
304
        // Fix protocol
305
        if ('HTTP/1.0' != $request->server->get('SERVER_PROTOCOL')) {
306
            $this->setProtocolVersion('1.1');
307
        }
308

    
309
        // Check if we need to send extra expire info headers
310
        if ('1.0' == $this->getProtocolVersion() && 'no-cache' == $this->headers->get('Cache-Control')) {
311
            $this->headers->set('pragma', 'no-cache');
312
            $this->headers->set('expires', -1);
313
        }
314

    
315
        $this->ensureIEOverSSLCompatibility($request);
316

    
317
        return $this;
318
    }
319

    
320
    /**
321
     * Sends HTTP headers.
322
     *
323
     * @return Response
324
     */
325
    public function sendHeaders()
326
    {
327
        // headers have already been sent by the developer
328
        if (headers_sent()) {
329
            return $this;
330
        }
331

    
332
        if (!$this->headers->has('Date')) {
333
            $this->setDate(\DateTime::createFromFormat('U', time()));
334
        }
335

    
336
        // headers
337
        foreach ($this->headers->allPreserveCase() as $name => $values) {
338
            foreach ($values as $value) {
339
                header($name.': '.$value, false, $this->statusCode);
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 Response
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 Response
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 Response
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 Response
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 Response
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 Response
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 Response
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 Response
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
        if (!$this->headers->has('Date')) {
616
            $this->setDate(\DateTime::createFromFormat('U', time()));
617
        }
618

    
619
        return $this->headers->getDate('Date');
620
    }
621

    
622
    /**
623
     * Sets the Date header.
624
     *
625
     * @param \DateTime $date A \DateTime instance
626
     *
627
     * @return Response
628
     */
629
    public function setDate(\DateTime $date)
630
    {
631
        $date->setTimezone(new \DateTimeZone('UTC'));
632
        $this->headers->set('Date', $date->format('D, d M Y H:i:s').' GMT');
633

    
634
        return $this;
635
    }
636

    
637
    /**
638
     * Returns the age of the response.
639
     *
640
     * @return int The age of the response in seconds
641
     */
642
    public function getAge()
643
    {
644
        if (null !== $age = $this->headers->get('Age')) {
645
            return (int) $age;
646
        }
647

    
648
        return max(time() - $this->getDate()->format('U'), 0);
649
    }
650

    
651
    /**
652
     * Marks the response stale by setting the Age header to be equal to the maximum age of the response.
653
     *
654
     * @return Response
655
     */
656
    public function expire()
657
    {
658
        if ($this->isFresh()) {
659
            $this->headers->set('Age', $this->getMaxAge());
660
        }
661

    
662
        return $this;
663
    }
664

    
665
    /**
666
     * Returns the value of the Expires header as a DateTime instance.
667
     *
668
     * @return \DateTime|null A DateTime instance or null if the header does not exist
669
     */
670
    public function getExpires()
671
    {
672
        try {
673
            return $this->headers->getDate('Expires');
674
        } catch (\RuntimeException $e) {
675
            // according to RFC 2616 invalid date formats (e.g. "0" and "-1") must be treated as in the past
676
            return \DateTime::createFromFormat(DATE_RFC2822, 'Sat, 01 Jan 00 00:00:00 +0000');
677
        }
678
    }
679

    
680
    /**
681
     * Sets the Expires HTTP header with a DateTime instance.
682
     *
683
     * Passing null as value will remove the header.
684
     *
685
     * @param \DateTime|null $date A \DateTime instance or null to remove the header
686
     *
687
     * @return Response
688
     */
689
    public function setExpires(\DateTime $date = null)
690
    {
691
        if (null === $date) {
692
            $this->headers->remove('Expires');
693
        } else {
694
            $date = clone $date;
695
            $date->setTimezone(new \DateTimeZone('UTC'));
696
            $this->headers->set('Expires', $date->format('D, d M Y H:i:s').' GMT');
697
        }
698

    
699
        return $this;
700
    }
701

    
702
    /**
703
     * Returns the number of seconds after the time specified in the response's Date
704
     * header when the response should no longer be considered fresh.
705
     *
706
     * First, it checks for a s-maxage directive, then a max-age directive, and then it falls
707
     * back on an expires header. It returns null when no maximum age can be established.
708
     *
709
     * @return int|null Number of seconds
710
     */
711
    public function getMaxAge()
712
    {
713
        if ($this->headers->hasCacheControlDirective('s-maxage')) {
714
            return (int) $this->headers->getCacheControlDirective('s-maxage');
715
        }
716

    
717
        if ($this->headers->hasCacheControlDirective('max-age')) {
718
            return (int) $this->headers->getCacheControlDirective('max-age');
719
        }
720

    
721
        if (null !== $this->getExpires()) {
722
            return $this->getExpires()->format('U') - $this->getDate()->format('U');
723
        }
724
    }
725

    
726
    /**
727
     * Sets the number of seconds after which the response should no longer be considered fresh.
728
     *
729
     * This methods sets the Cache-Control max-age directive.
730
     *
731
     * @param int $value Number of seconds
732
     *
733
     * @return Response
734
     */
735
    public function setMaxAge($value)
736
    {
737
        $this->headers->addCacheControlDirective('max-age', $value);
738

    
739
        return $this;
740
    }
741

    
742
    /**
743
     * Sets the number of seconds after which the response should no longer be considered fresh by shared caches.
744
     *
745
     * This methods sets the Cache-Control s-maxage directive.
746
     *
747
     * @param int $value Number of seconds
748
     *
749
     * @return Response
750
     */
751
    public function setSharedMaxAge($value)
752
    {
753
        $this->setPublic();
754
        $this->headers->addCacheControlDirective('s-maxage', $value);
755

    
756
        return $this;
757
    }
758

    
759
    /**
760
     * Returns the response's time-to-live in seconds.
761
     *
762
     * It returns null when no freshness information is present in the response.
763
     *
764
     * When the responses TTL is <= 0, the response may not be served from cache without first
765
     * revalidating with the origin.
766
     *
767
     * @return int|null The TTL in seconds
768
     */
769
    public function getTtl()
770
    {
771
        if (null !== $maxAge = $this->getMaxAge()) {
772
            return $maxAge - $this->getAge();
773
        }
774
    }
775

    
776
    /**
777
     * Sets the response's time-to-live for shared caches.
778
     *
779
     * This method adjusts the Cache-Control/s-maxage directive.
780
     *
781
     * @param int $seconds Number of seconds
782
     *
783
     * @return Response
784
     */
785
    public function setTtl($seconds)
786
    {
787
        $this->setSharedMaxAge($this->getAge() + $seconds);
788

    
789
        return $this;
790
    }
791

    
792
    /**
793
     * Sets the response's time-to-live for private/client caches.
794
     *
795
     * This method adjusts the Cache-Control/max-age directive.
796
     *
797
     * @param int $seconds Number of seconds
798
     *
799
     * @return Response
800
     */
801
    public function setClientTtl($seconds)
802
    {
803
        $this->setMaxAge($this->getAge() + $seconds);
804

    
805
        return $this;
806
    }
807

    
808
    /**
809
     * Returns the Last-Modified HTTP header as a DateTime instance.
810
     *
811
     * @return \DateTime|null A DateTime instance or null if the header does not exist
812
     *
813
     * @throws \RuntimeException When the HTTP header is not parseable
814
     */
815
    public function getLastModified()
816
    {
817
        return $this->headers->getDate('Last-Modified');
818
    }
819

    
820
    /**
821
     * Sets the Last-Modified HTTP header with a DateTime instance.
822
     *
823
     * Passing null as value will remove the header.
824
     *
825
     * @param \DateTime|null $date A \DateTime instance or null to remove the header
826
     *
827
     * @return Response
828
     */
829
    public function setLastModified(\DateTime $date = null)
830
    {
831
        if (null === $date) {
832
            $this->headers->remove('Last-Modified');
833
        } else {
834
            $date = clone $date;
835
            $date->setTimezone(new \DateTimeZone('UTC'));
836
            $this->headers->set('Last-Modified', $date->format('D, d M Y H:i:s').' GMT');
837
        }
838

    
839
        return $this;
840
    }
841

    
842
    /**
843
     * Returns the literal value of the ETag HTTP header.
844
     *
845
     * @return string|null The ETag HTTP header or null if it does not exist
846
     */
847
    public function getEtag()
848
    {
849
        return $this->headers->get('ETag');
850
    }
851

    
852
    /**
853
     * Sets the ETag value.
854
     *
855
     * @param string|null $etag The ETag unique identifier or null to remove the header
856
     * @param bool        $weak Whether you want a weak ETag or not
857
     *
858
     * @return Response
859
     */
860
    public function setEtag($etag = null, $weak = false)
861
    {
862
        if (null === $etag) {
863
            $this->headers->remove('Etag');
864
        } else {
865
            if (0 !== strpos($etag, '"')) {
866
                $etag = '"'.$etag.'"';
867
            }
868

    
869
            $this->headers->set('ETag', (true === $weak ? 'W/' : '').$etag);
870
        }
871

    
872
        return $this;
873
    }
874

    
875
    /**
876
     * Sets the response's cache headers (validation and/or expiration).
877
     *
878
     * Available options are: etag, last_modified, max_age, s_maxage, private, and public.
879
     *
880
     * @param array $options An array of cache options
881
     *
882
     * @return Response
883
     *
884
     * @throws \InvalidArgumentException
885
     */
886
    public function setCache(array $options)
887
    {
888
        if ($diff = array_diff(array_keys($options), array('etag', 'last_modified', 'max_age', 's_maxage', 'private', 'public'))) {
889
            throw new \InvalidArgumentException(sprintf('Response does not support the following options: "%s".', implode('", "', array_values($diff))));
890
        }
891

    
892
        if (isset($options['etag'])) {
893
            $this->setEtag($options['etag']);
894
        }
895

    
896
        if (isset($options['last_modified'])) {
897
            $this->setLastModified($options['last_modified']);
898
        }
899

    
900
        if (isset($options['max_age'])) {
901
            $this->setMaxAge($options['max_age']);
902
        }
903

    
904
        if (isset($options['s_maxage'])) {
905
            $this->setSharedMaxAge($options['s_maxage']);
906
        }
907

    
908
        if (isset($options['public'])) {
909
            if ($options['public']) {
910
                $this->setPublic();
911
            } else {
912
                $this->setPrivate();
913
            }
914
        }
915

    
916
        if (isset($options['private'])) {
917
            if ($options['private']) {
918
                $this->setPrivate();
919
            } else {
920
                $this->setPublic();
921
            }
922
        }
923

    
924
        return $this;
925
    }
926

    
927
    /**
928
     * Modifies the response so that it conforms to the rules defined for a 304 status code.
929
     *
930
     * This sets the status, removes the body, and discards any headers
931
     * that MUST NOT be included in 304 responses.
932
     *
933
     * @return Response
934
     *
935
     * @see http://tools.ietf.org/html/rfc2616#section-10.3.5
936
     */
937
    public function setNotModified()
938
    {
939
        $this->setStatusCode(304);
940
        $this->setContent(null);
941

    
942
        // remove headers that MUST NOT be included with 304 Not Modified responses
943
        foreach (array('Allow', 'Content-Encoding', 'Content-Language', 'Content-Length', 'Content-MD5', 'Content-Type', 'Last-Modified') as $header) {
944
            $this->headers->remove($header);
945
        }
946

    
947
        return $this;
948
    }
949

    
950
    /**
951
     * Returns true if the response includes a Vary header.
952
     *
953
     * @return bool true if the response includes a Vary header, false otherwise
954
     */
955
    public function hasVary()
956
    {
957
        return null !== $this->headers->get('Vary');
958
    }
959

    
960
    /**
961
     * Returns an array of header names given in the Vary header.
962
     *
963
     * @return array An array of Vary names
964
     */
965
    public function getVary()
966
    {
967
        if (!$vary = $this->headers->get('Vary', null, false)) {
968
            return array();
969
        }
970

    
971
        $ret = array();
972
        foreach ($vary as $item) {
973
            $ret = array_merge($ret, preg_split('/[\s,]+/', $item));
974
        }
975

    
976
        return $ret;
977
    }
978

    
979
    /**
980
     * Sets the Vary header.
981
     *
982
     * @param string|array $headers
983
     * @param bool         $replace Whether to replace the actual value or not (true by default)
984
     *
985
     * @return Response
986
     */
987
    public function setVary($headers, $replace = true)
988
    {
989
        $this->headers->set('Vary', $headers, $replace);
990

    
991
        return $this;
992
    }
993

    
994
    /**
995
     * Determines if the Response validators (ETag, Last-Modified) match
996
     * a conditional value specified in the Request.
997
     *
998
     * If the Response is not modified, it sets the status code to 304 and
999
     * removes the actual content by calling the setNotModified() method.
1000
     *
1001
     * @param Request $request A Request instance
1002
     *
1003
     * @return bool true if the Response validators match the Request, false otherwise
1004
     */
1005
    public function isNotModified(Request $request)
1006
    {
1007
        if (!$request->isMethodSafe()) {
1008
            return false;
1009
        }
1010

    
1011
        $notModified = false;
1012
        $lastModified = $this->headers->get('Last-Modified');
1013
        $modifiedSince = $request->headers->get('If-Modified-Since');
1014

    
1015
        if ($etags = $request->getETags()) {
1016
            $notModified = in_array($this->getEtag(), $etags) || in_array('*', $etags);
1017
        }
1018

    
1019
        if ($modifiedSince && $lastModified) {
1020
            $notModified = strtotime($modifiedSince) >= strtotime($lastModified) && (!$etags || $notModified);
1021
        }
1022

    
1023
        if ($notModified) {
1024
            $this->setNotModified();
1025
        }
1026

    
1027
        return $notModified;
1028
    }
1029

    
1030
    /**
1031
     * Is response invalid?
1032
     *
1033
     * @return bool
1034
     *
1035
     * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
1036
     */
1037
    public function isInvalid()
1038
    {
1039
        return $this->statusCode < 100 || $this->statusCode >= 600;
1040
    }
1041

    
1042
    /**
1043
     * Is response informative?
1044
     *
1045
     * @return bool
1046
     */
1047
    public function isInformational()
1048
    {
1049
        return $this->statusCode >= 100 && $this->statusCode < 200;
1050
    }
1051

    
1052
    /**
1053
     * Is response successful?
1054
     *
1055
     * @return bool
1056
     */
1057
    public function isSuccessful()
1058
    {
1059
        return $this->statusCode >= 200 && $this->statusCode < 300;
1060
    }
1061

    
1062
    /**
1063
     * Is the response a redirect?
1064
     *
1065
     * @return bool
1066
     */
1067
    public function isRedirection()
1068
    {
1069
        return $this->statusCode >= 300 && $this->statusCode < 400;
1070
    }
1071

    
1072
    /**
1073
     * Is there a client error?
1074
     *
1075
     * @return bool
1076
     */
1077
    public function isClientError()
1078
    {
1079
        return $this->statusCode >= 400 && $this->statusCode < 500;
1080
    }
1081

    
1082
    /**
1083
     * Was there a server side error?
1084
     *
1085
     * @return bool
1086
     */
1087
    public function isServerError()
1088
    {
1089
        return $this->statusCode >= 500 && $this->statusCode < 600;
1090
    }
1091

    
1092
    /**
1093
     * Is the response OK?
1094
     *
1095
     * @return bool
1096
     */
1097
    public function isOk()
1098
    {
1099
        return 200 === $this->statusCode;
1100
    }
1101

    
1102
    /**
1103
     * Is the response forbidden?
1104
     *
1105
     * @return bool
1106
     */
1107
    public function isForbidden()
1108
    {
1109
        return 403 === $this->statusCode;
1110
    }
1111

    
1112
    /**
1113
     * Is the response a not found error?
1114
     *
1115
     * @return bool
1116
     */
1117
    public function isNotFound()
1118
    {
1119
        return 404 === $this->statusCode;
1120
    }
1121

    
1122
    /**
1123
     * Is the response a redirect of some form?
1124
     *
1125
     * @param string $location
1126
     *
1127
     * @return bool
1128
     */
1129
    public function isRedirect($location = null)
1130
    {
1131
        return in_array($this->statusCode, array(201, 301, 302, 303, 307, 308)) && (null === $location ?: $location == $this->headers->get('Location'));
1132
    }
1133

    
1134
    /**
1135
     * Is the response empty?
1136
     *
1137
     * @return bool
1138
     */
1139
    public function isEmpty()
1140
    {
1141
        return in_array($this->statusCode, array(204, 304));
1142
    }
1143

    
1144
    /**
1145
     * Cleans or flushes output buffers up to target level.
1146
     *
1147
     * Resulting level can be greater than target level if a non-removable buffer has been encountered.
1148
     *
1149
     * @param int  $targetLevel The target output buffering level
1150
     * @param bool $flush       Whether to flush or clean the buffers
1151
     */
1152
    public static function closeOutputBuffers($targetLevel, $flush)
1153
    {
1154
        $status = ob_get_status(true);
1155
        $level = count($status);
1156
        // PHP_OUTPUT_HANDLER_* are not defined on HHVM 3.3
1157
        $flags = defined('PHP_OUTPUT_HANDLER_REMOVABLE') ? PHP_OUTPUT_HANDLER_REMOVABLE | ($flush ? PHP_OUTPUT_HANDLER_FLUSHABLE : PHP_OUTPUT_HANDLER_CLEANABLE) : -1;
1158

    
1159
        while ($level-- > $targetLevel && ($s = $status[$level]) && (!isset($s['del']) ? !isset($s['flags']) || $flags === ($s['flags'] & $flags) : $s['del'])) {
1160
            if ($flush) {
1161
                ob_end_flush();
1162
            } else {
1163
                ob_end_clean();
1164
            }
1165
        }
1166
    }
1167

    
1168
    /**
1169
     * Checks if we need to remove Cache-Control for SSL encrypted downloads when using IE < 9.
1170
     *
1171
     * @link http://support.microsoft.com/kb/323308
1172
     */
1173
    protected function ensureIEOverSSLCompatibility(Request $request)
1174
    {
1175
        if (false !== stripos($this->headers->get('Content-Disposition'), 'attachment') && preg_match('/MSIE (.*?);/i', $request->server->get('HTTP_USER_AGENT'), $match) == 1 && true === $request->isSecure()) {
1176
            if ((int) preg_replace('/(MSIE )(.*?);/', '$2', $match[0]) < 9) {
1177
                $this->headers->remove('Cache-Control');
1178
            }
1179
        }
1180
    }
1181
}
(21-21/26)