Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Symfony\Component\HttpKernel\HttpCache; Chris@0: Chris@0: use Symfony\Component\HttpFoundation\Response; Chris@0: Chris@0: /** Chris@0: * ResponseCacheStrategy knows how to compute the Response cache HTTP header Chris@0: * based on the different response cache headers. Chris@0: * Chris@0: * This implementation changes the master response TTL to the smallest TTL received Chris@0: * or force validation if one of the surrogates has validation cache strategy. Chris@0: * Chris@0: * @author Fabien Potencier Chris@0: */ Chris@0: class ResponseCacheStrategy implements ResponseCacheStrategyInterface Chris@0: { Chris@18: /** Chris@18: * Cache-Control headers that are sent to the final response if they appear in ANY of the responses. Chris@18: */ Chris@18: private static $overrideDirectives = ['private', 'no-cache', 'no-store', 'no-transform', 'must-revalidate', 'proxy-revalidate']; Chris@18: Chris@18: /** Chris@18: * Cache-Control headers that are sent to the final response if they appear in ALL of the responses. Chris@18: */ Chris@18: private static $inheritDirectives = ['public', 'immutable']; Chris@18: Chris@0: private $embeddedResponses = 0; Chris@0: private $isNotCacheableResponseEmbedded = false; Chris@18: private $age = 0; Chris@18: private $flagDirectives = [ Chris@18: 'no-cache' => null, Chris@18: 'no-store' => null, Chris@18: 'no-transform' => null, Chris@18: 'must-revalidate' => null, Chris@18: 'proxy-revalidate' => null, Chris@18: 'public' => null, Chris@18: 'private' => null, Chris@18: 'immutable' => null, Chris@18: ]; Chris@18: private $ageDirectives = [ Chris@18: 'max-age' => null, Chris@18: 's-maxage' => null, Chris@18: 'expires' => null, Chris@18: ]; Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function add(Response $response) Chris@0: { Chris@18: ++$this->embeddedResponses; Chris@0: Chris@18: foreach (self::$overrideDirectives as $directive) { Chris@18: if ($response->headers->hasCacheControlDirective($directive)) { Chris@18: $this->flagDirectives[$directive] = true; Chris@0: } Chris@0: } Chris@0: Chris@18: foreach (self::$inheritDirectives as $directive) { Chris@18: if (false !== $this->flagDirectives[$directive]) { Chris@18: $this->flagDirectives[$directive] = $response->headers->hasCacheControlDirective($directive); Chris@18: } Chris@18: } Chris@18: Chris@18: $age = $response->getAge(); Chris@18: $this->age = max($this->age, $age); Chris@18: Chris@18: if ($this->willMakeFinalResponseUncacheable($response)) { Chris@18: $this->isNotCacheableResponseEmbedded = true; Chris@18: Chris@18: return; Chris@18: } Chris@18: Chris@18: $this->storeRelativeAgeDirective('max-age', $response->headers->getCacheControlDirective('max-age'), $age); Chris@18: $this->storeRelativeAgeDirective('s-maxage', $response->headers->getCacheControlDirective('s-maxage') ?: $response->headers->getCacheControlDirective('max-age'), $age); Chris@18: Chris@18: $expires = $response->getExpires(); Chris@18: $expires = null !== $expires ? $expires->format('U') - $response->getDate()->format('U') : null; Chris@18: $this->storeRelativeAgeDirective('expires', $expires >= 0 ? $expires : null, 0); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function update(Response $response) Chris@0: { Chris@0: // if we have no embedded Response, do nothing Chris@0: if (0 === $this->embeddedResponses) { Chris@0: return; Chris@0: } Chris@0: Chris@18: // Remove validation related headers of the master response, Chris@18: // because some of the response content comes from at least Chris@18: // one embedded response (which likely has a different caching strategy). Chris@18: $response->setEtag(null); Chris@18: $response->setLastModified(null); Chris@12: Chris@18: $this->add($response); Chris@0: Chris@18: $response->headers->set('Age', $this->age); Chris@18: Chris@18: if ($this->isNotCacheableResponseEmbedded) { Chris@18: $response->setExpires($response->getDate()); Chris@18: Chris@18: if ($this->flagDirectives['no-store']) { Chris@18: $response->headers->set('Cache-Control', 'no-cache, no-store, must-revalidate'); Chris@18: } else { Chris@18: $response->headers->set('Cache-Control', 'no-cache, must-revalidate'); Chris@18: } Chris@0: Chris@0: return; Chris@0: } Chris@0: Chris@18: $flags = array_filter($this->flagDirectives); Chris@0: Chris@18: if (isset($flags['must-revalidate'])) { Chris@18: $flags['no-cache'] = true; Chris@0: } Chris@18: Chris@18: $response->headers->set('Cache-Control', implode(', ', array_keys($flags))); Chris@18: Chris@18: $maxAge = null; Chris@18: $sMaxage = null; Chris@18: Chris@18: if (\is_numeric($this->ageDirectives['max-age'])) { Chris@18: $maxAge = $this->ageDirectives['max-age'] + $this->age; Chris@18: $response->headers->addCacheControlDirective('max-age', $maxAge); Chris@18: } Chris@18: Chris@18: if (\is_numeric($this->ageDirectives['s-maxage'])) { Chris@18: $sMaxage = $this->ageDirectives['s-maxage'] + $this->age; Chris@18: Chris@18: if ($maxAge !== $sMaxage) { Chris@18: $response->headers->addCacheControlDirective('s-maxage', $sMaxage); Chris@18: } Chris@18: } Chris@18: Chris@18: if (\is_numeric($this->ageDirectives['expires'])) { Chris@18: $date = clone $response->getDate(); Chris@18: $date->modify('+'.($this->ageDirectives['expires'] + $this->age).' seconds'); Chris@18: $response->setExpires($date); Chris@18: } Chris@18: } Chris@18: Chris@18: /** Chris@18: * RFC2616, Section 13.4. Chris@18: * Chris@18: * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.4 Chris@18: * Chris@18: * @return bool Chris@18: */ Chris@18: private function willMakeFinalResponseUncacheable(Response $response) Chris@18: { Chris@18: // RFC2616: A response received with a status code of 200, 203, 300, 301 or 410 Chris@18: // MAY be stored by a cache […] unless a cache-control directive prohibits caching. Chris@18: if ($response->headers->hasCacheControlDirective('no-cache') Chris@18: || $response->headers->getCacheControlDirective('no-store') Chris@18: ) { Chris@18: return true; Chris@18: } Chris@18: Chris@18: // Last-Modified and Etag headers cannot be merged, they render the response uncacheable Chris@18: // by default (except if the response also has max-age etc.). Chris@18: if (\in_array($response->getStatusCode(), [200, 203, 300, 301, 410]) Chris@18: && null === $response->getLastModified() Chris@18: && null === $response->getEtag() Chris@18: ) { Chris@18: return false; Chris@18: } Chris@18: Chris@18: // RFC2616: A response received with any other status code (e.g. status codes 302 and 307) Chris@18: // MUST NOT be returned in a reply to a subsequent request unless there are Chris@18: // cache-control directives or another header(s) that explicitly allow it. Chris@18: $cacheControl = ['max-age', 's-maxage', 'must-revalidate', 'proxy-revalidate', 'public', 'private']; Chris@18: foreach ($cacheControl as $key) { Chris@18: if ($response->headers->hasCacheControlDirective($key)) { Chris@18: return false; Chris@18: } Chris@18: } Chris@18: Chris@18: if ($response->headers->has('Expires')) { Chris@18: return false; Chris@18: } Chris@18: Chris@18: return true; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Store lowest max-age/s-maxage/expires for the final response. Chris@18: * Chris@18: * The response might have been stored in cache a while ago. To keep things comparable, Chris@18: * we have to subtract the age so that the value is normalized for an age of 0. Chris@18: * Chris@18: * If the value is lower than the currently stored value, we update the value, to keep a rolling Chris@18: * minimal value of each instruction. If the value is NULL, the directive will not be set on the final response. Chris@18: * Chris@18: * @param string $directive Chris@18: * @param int|null $value Chris@18: * @param int $age Chris@18: */ Chris@18: private function storeRelativeAgeDirective($directive, $value, $age) Chris@18: { Chris@18: if (null === $value) { Chris@18: $this->ageDirectives[$directive] = false; Chris@18: } Chris@18: Chris@18: if (false !== $this->ageDirectives[$directive]) { Chris@18: $value -= $age; Chris@18: $this->ageDirectives[$directive] = null !== $this->ageDirectives[$directive] ? min($this->ageDirectives[$directive], $value) : $value; Chris@18: } Chris@0: } Chris@0: }