Chris@0
|
1 <?php
|
Chris@0
|
2
|
Chris@0
|
3 /*
|
Chris@0
|
4 * This file is part of the Symfony package.
|
Chris@0
|
5 *
|
Chris@0
|
6 * (c) Fabien Potencier <fabien@symfony.com>
|
Chris@0
|
7 *
|
Chris@0
|
8 * For the full copyright and license information, please view the LICENSE
|
Chris@0
|
9 * file that was distributed with this source code.
|
Chris@0
|
10 */
|
Chris@0
|
11
|
Chris@0
|
12 namespace Symfony\Component\HttpKernel\HttpCache;
|
Chris@0
|
13
|
Chris@0
|
14 use Symfony\Component\HttpFoundation\Response;
|
Chris@0
|
15
|
Chris@0
|
16 /**
|
Chris@0
|
17 * ResponseCacheStrategy knows how to compute the Response cache HTTP header
|
Chris@0
|
18 * based on the different response cache headers.
|
Chris@0
|
19 *
|
Chris@0
|
20 * This implementation changes the master response TTL to the smallest TTL received
|
Chris@0
|
21 * or force validation if one of the surrogates has validation cache strategy.
|
Chris@0
|
22 *
|
Chris@0
|
23 * @author Fabien Potencier <fabien@symfony.com>
|
Chris@0
|
24 */
|
Chris@0
|
25 class ResponseCacheStrategy implements ResponseCacheStrategyInterface
|
Chris@0
|
26 {
|
Chris@18
|
27 /**
|
Chris@18
|
28 * Cache-Control headers that are sent to the final response if they appear in ANY of the responses.
|
Chris@18
|
29 */
|
Chris@18
|
30 private static $overrideDirectives = ['private', 'no-cache', 'no-store', 'no-transform', 'must-revalidate', 'proxy-revalidate'];
|
Chris@18
|
31
|
Chris@18
|
32 /**
|
Chris@18
|
33 * Cache-Control headers that are sent to the final response if they appear in ALL of the responses.
|
Chris@18
|
34 */
|
Chris@18
|
35 private static $inheritDirectives = ['public', 'immutable'];
|
Chris@18
|
36
|
Chris@0
|
37 private $embeddedResponses = 0;
|
Chris@0
|
38 private $isNotCacheableResponseEmbedded = false;
|
Chris@18
|
39 private $age = 0;
|
Chris@18
|
40 private $flagDirectives = [
|
Chris@18
|
41 'no-cache' => null,
|
Chris@18
|
42 'no-store' => null,
|
Chris@18
|
43 'no-transform' => null,
|
Chris@18
|
44 'must-revalidate' => null,
|
Chris@18
|
45 'proxy-revalidate' => null,
|
Chris@18
|
46 'public' => null,
|
Chris@18
|
47 'private' => null,
|
Chris@18
|
48 'immutable' => null,
|
Chris@18
|
49 ];
|
Chris@18
|
50 private $ageDirectives = [
|
Chris@18
|
51 'max-age' => null,
|
Chris@18
|
52 's-maxage' => null,
|
Chris@18
|
53 'expires' => null,
|
Chris@18
|
54 ];
|
Chris@0
|
55
|
Chris@0
|
56 /**
|
Chris@0
|
57 * {@inheritdoc}
|
Chris@0
|
58 */
|
Chris@0
|
59 public function add(Response $response)
|
Chris@0
|
60 {
|
Chris@18
|
61 ++$this->embeddedResponses;
|
Chris@0
|
62
|
Chris@18
|
63 foreach (self::$overrideDirectives as $directive) {
|
Chris@18
|
64 if ($response->headers->hasCacheControlDirective($directive)) {
|
Chris@18
|
65 $this->flagDirectives[$directive] = true;
|
Chris@0
|
66 }
|
Chris@0
|
67 }
|
Chris@0
|
68
|
Chris@18
|
69 foreach (self::$inheritDirectives as $directive) {
|
Chris@18
|
70 if (false !== $this->flagDirectives[$directive]) {
|
Chris@18
|
71 $this->flagDirectives[$directive] = $response->headers->hasCacheControlDirective($directive);
|
Chris@18
|
72 }
|
Chris@18
|
73 }
|
Chris@18
|
74
|
Chris@18
|
75 $age = $response->getAge();
|
Chris@18
|
76 $this->age = max($this->age, $age);
|
Chris@18
|
77
|
Chris@18
|
78 if ($this->willMakeFinalResponseUncacheable($response)) {
|
Chris@18
|
79 $this->isNotCacheableResponseEmbedded = true;
|
Chris@18
|
80
|
Chris@18
|
81 return;
|
Chris@18
|
82 }
|
Chris@18
|
83
|
Chris@18
|
84 $this->storeRelativeAgeDirective('max-age', $response->headers->getCacheControlDirective('max-age'), $age);
|
Chris@18
|
85 $this->storeRelativeAgeDirective('s-maxage', $response->headers->getCacheControlDirective('s-maxage') ?: $response->headers->getCacheControlDirective('max-age'), $age);
|
Chris@18
|
86
|
Chris@18
|
87 $expires = $response->getExpires();
|
Chris@18
|
88 $expires = null !== $expires ? $expires->format('U') - $response->getDate()->format('U') : null;
|
Chris@18
|
89 $this->storeRelativeAgeDirective('expires', $expires >= 0 ? $expires : null, 0);
|
Chris@0
|
90 }
|
Chris@0
|
91
|
Chris@0
|
92 /**
|
Chris@0
|
93 * {@inheritdoc}
|
Chris@0
|
94 */
|
Chris@0
|
95 public function update(Response $response)
|
Chris@0
|
96 {
|
Chris@0
|
97 // if we have no embedded Response, do nothing
|
Chris@0
|
98 if (0 === $this->embeddedResponses) {
|
Chris@0
|
99 return;
|
Chris@0
|
100 }
|
Chris@0
|
101
|
Chris@18
|
102 // Remove validation related headers of the master response,
|
Chris@18
|
103 // because some of the response content comes from at least
|
Chris@18
|
104 // one embedded response (which likely has a different caching strategy).
|
Chris@18
|
105 $response->setEtag(null);
|
Chris@18
|
106 $response->setLastModified(null);
|
Chris@12
|
107
|
Chris@18
|
108 $this->add($response);
|
Chris@0
|
109
|
Chris@18
|
110 $response->headers->set('Age', $this->age);
|
Chris@18
|
111
|
Chris@18
|
112 if ($this->isNotCacheableResponseEmbedded) {
|
Chris@18
|
113 $response->setExpires($response->getDate());
|
Chris@18
|
114
|
Chris@18
|
115 if ($this->flagDirectives['no-store']) {
|
Chris@18
|
116 $response->headers->set('Cache-Control', 'no-cache, no-store, must-revalidate');
|
Chris@18
|
117 } else {
|
Chris@18
|
118 $response->headers->set('Cache-Control', 'no-cache, must-revalidate');
|
Chris@18
|
119 }
|
Chris@0
|
120
|
Chris@0
|
121 return;
|
Chris@0
|
122 }
|
Chris@0
|
123
|
Chris@18
|
124 $flags = array_filter($this->flagDirectives);
|
Chris@0
|
125
|
Chris@18
|
126 if (isset($flags['must-revalidate'])) {
|
Chris@18
|
127 $flags['no-cache'] = true;
|
Chris@0
|
128 }
|
Chris@18
|
129
|
Chris@18
|
130 $response->headers->set('Cache-Control', implode(', ', array_keys($flags)));
|
Chris@18
|
131
|
Chris@18
|
132 $maxAge = null;
|
Chris@18
|
133 $sMaxage = null;
|
Chris@18
|
134
|
Chris@18
|
135 if (\is_numeric($this->ageDirectives['max-age'])) {
|
Chris@18
|
136 $maxAge = $this->ageDirectives['max-age'] + $this->age;
|
Chris@18
|
137 $response->headers->addCacheControlDirective('max-age', $maxAge);
|
Chris@18
|
138 }
|
Chris@18
|
139
|
Chris@18
|
140 if (\is_numeric($this->ageDirectives['s-maxage'])) {
|
Chris@18
|
141 $sMaxage = $this->ageDirectives['s-maxage'] + $this->age;
|
Chris@18
|
142
|
Chris@18
|
143 if ($maxAge !== $sMaxage) {
|
Chris@18
|
144 $response->headers->addCacheControlDirective('s-maxage', $sMaxage);
|
Chris@18
|
145 }
|
Chris@18
|
146 }
|
Chris@18
|
147
|
Chris@18
|
148 if (\is_numeric($this->ageDirectives['expires'])) {
|
Chris@18
|
149 $date = clone $response->getDate();
|
Chris@18
|
150 $date->modify('+'.($this->ageDirectives['expires'] + $this->age).' seconds');
|
Chris@18
|
151 $response->setExpires($date);
|
Chris@18
|
152 }
|
Chris@18
|
153 }
|
Chris@18
|
154
|
Chris@18
|
155 /**
|
Chris@18
|
156 * RFC2616, Section 13.4.
|
Chris@18
|
157 *
|
Chris@18
|
158 * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.4
|
Chris@18
|
159 *
|
Chris@18
|
160 * @return bool
|
Chris@18
|
161 */
|
Chris@18
|
162 private function willMakeFinalResponseUncacheable(Response $response)
|
Chris@18
|
163 {
|
Chris@18
|
164 // RFC2616: A response received with a status code of 200, 203, 300, 301 or 410
|
Chris@18
|
165 // MAY be stored by a cache […] unless a cache-control directive prohibits caching.
|
Chris@18
|
166 if ($response->headers->hasCacheControlDirective('no-cache')
|
Chris@18
|
167 || $response->headers->getCacheControlDirective('no-store')
|
Chris@18
|
168 ) {
|
Chris@18
|
169 return true;
|
Chris@18
|
170 }
|
Chris@18
|
171
|
Chris@18
|
172 // Last-Modified and Etag headers cannot be merged, they render the response uncacheable
|
Chris@18
|
173 // by default (except if the response also has max-age etc.).
|
Chris@18
|
174 if (\in_array($response->getStatusCode(), [200, 203, 300, 301, 410])
|
Chris@18
|
175 && null === $response->getLastModified()
|
Chris@18
|
176 && null === $response->getEtag()
|
Chris@18
|
177 ) {
|
Chris@18
|
178 return false;
|
Chris@18
|
179 }
|
Chris@18
|
180
|
Chris@18
|
181 // RFC2616: A response received with any other status code (e.g. status codes 302 and 307)
|
Chris@18
|
182 // MUST NOT be returned in a reply to a subsequent request unless there are
|
Chris@18
|
183 // cache-control directives or another header(s) that explicitly allow it.
|
Chris@18
|
184 $cacheControl = ['max-age', 's-maxage', 'must-revalidate', 'proxy-revalidate', 'public', 'private'];
|
Chris@18
|
185 foreach ($cacheControl as $key) {
|
Chris@18
|
186 if ($response->headers->hasCacheControlDirective($key)) {
|
Chris@18
|
187 return false;
|
Chris@18
|
188 }
|
Chris@18
|
189 }
|
Chris@18
|
190
|
Chris@18
|
191 if ($response->headers->has('Expires')) {
|
Chris@18
|
192 return false;
|
Chris@18
|
193 }
|
Chris@18
|
194
|
Chris@18
|
195 return true;
|
Chris@18
|
196 }
|
Chris@18
|
197
|
Chris@18
|
198 /**
|
Chris@18
|
199 * Store lowest max-age/s-maxage/expires for the final response.
|
Chris@18
|
200 *
|
Chris@18
|
201 * The response might have been stored in cache a while ago. To keep things comparable,
|
Chris@18
|
202 * we have to subtract the age so that the value is normalized for an age of 0.
|
Chris@18
|
203 *
|
Chris@18
|
204 * If the value is lower than the currently stored value, we update the value, to keep a rolling
|
Chris@18
|
205 * minimal value of each instruction. If the value is NULL, the directive will not be set on the final response.
|
Chris@18
|
206 *
|
Chris@18
|
207 * @param string $directive
|
Chris@18
|
208 * @param int|null $value
|
Chris@18
|
209 * @param int $age
|
Chris@18
|
210 */
|
Chris@18
|
211 private function storeRelativeAgeDirective($directive, $value, $age)
|
Chris@18
|
212 {
|
Chris@18
|
213 if (null === $value) {
|
Chris@18
|
214 $this->ageDirectives[$directive] = false;
|
Chris@18
|
215 }
|
Chris@18
|
216
|
Chris@18
|
217 if (false !== $this->ageDirectives[$directive]) {
|
Chris@18
|
218 $value -= $age;
|
Chris@18
|
219 $this->ageDirectives[$directive] = null !== $this->ageDirectives[$directive] ? min($this->ageDirectives[$directive], $value) : $value;
|
Chris@18
|
220 }
|
Chris@0
|
221 }
|
Chris@0
|
222 }
|